Compare commits

...

16 Commits

Author SHA1 Message Date
3010469c8a Major progress on APU pulse channels & organization
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 9s
2026-03-16 03:02:33 -05:00
fa88c825a6 Actually disable sprite and background rendering based on mask 2026-03-16 03:01:37 -05:00
b9a30c286a Partial audio implementation
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 10s
2026-03-14 21:19:16 -05:00
3372559c19 Working Sprite implementation
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 9s
2026-02-10 22:17:51 -06:00
22c586f15a 2026-02-07 update
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 8s
2026-02-07 04:52:13 -06:00
825e245df1 Ignore memory dump files
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 27s
2026-01-26 01:25:46 -06:00
7b76026ade Minor refactors and bug fixes
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Has been cancelled
- Bit instruction now sets Z flag correctly
- DMA is no longer handled by cpu.rs
2026-01-26 01:25:23 -06:00
f861f75b21 Major refactor
- CPU is now it's own module
- Memory object is now shared to support mapper chips
- ROM is now stored as `Arc<[u8]>` to support mapper chips
2026-01-24 03:38:42 -06:00
b5e1d1a4c3 Fix 'oops cycle' timing, to pass timing test rom
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 25s
2026-01-20 00:14:17 -06:00
2e5e2ed1e7 Fix most basic timing issues 2026-01-19 17:30:34 -06:00
42c3af28b4 Add run to address to debugger 2026-01-19 17:28:52 -06:00
ac745f60e9 Implement more features for PPU and APU 2026-01-19 17:28:12 -06:00
13e4158b7b Remove some commented code
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 7s
2026-01-19 01:39:18 -06:00
cd3de5e361 Major work
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 10s
2026-01-19 01:36:58 -06:00
c535e4e76d Add new testcases with cc65 support
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 8s
2025-12-22 02:13:10 -06:00
c8d441297e Format and basic background impl 2025-12-21 14:03:01 -06:00
80 changed files with 12867 additions and 2800 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/target
*.nes
*.zip
*.dmp

182
Cargo.lock generated
View File

@@ -70,6 +70,28 @@ dependencies = [
"equator",
]
[[package]]
name = "alsa"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3"
dependencies = [
"alsa-sys",
"bitflags 2.10.0",
"cfg-if",
"libc",
]
[[package]]
name = "alsa-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "android-activity"
version = "0.6.0"
@@ -771,6 +793,20 @@ dependencies = [
"libm",
]
[[package]]
name = "coreaudio-rs"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
dependencies = [
"bitflags 1.3.2",
"libc",
"objc2-audio-toolbox",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
]
[[package]]
name = "cosmic-text"
version = "0.15.0"
@@ -794,6 +830,36 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cpal"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb"
dependencies = [
"alsa",
"coreaudio-rs",
"dasp_sample",
"jni",
"js-sys",
"libc",
"mach2",
"ndk",
"ndk-context",
"num-derive",
"num-traits",
"objc2 0.6.3",
"objc2-audio-toolbox",
"objc2-avf-audio",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows 0.62.2",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -858,6 +924,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
[[package]]
name = "dasp_sample"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -871,6 +943,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
"libc",
"objc2 0.6.3",
]
@@ -2004,6 +2078,15 @@ dependencies = [
"num-traits",
]
[[package]]
name = "mach2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -2194,7 +2277,12 @@ name = "nes-emu"
version = "0.1.0"
dependencies = [
"bitfield",
"bytes",
"cpal",
"iced",
"iced_core",
"rfd",
"ringbuf",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -2398,6 +2486,31 @@ dependencies = [
"objc2-quartz-core 0.3.2",
]
[[package]]
name = "objc2-audio-toolbox"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08"
dependencies = [
"bitflags 2.10.0",
"libc",
"objc2 0.6.3",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-avf-audio"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
dependencies = [
"objc2 0.6.3",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-cloud-kit"
version = "0.2.2"
@@ -2433,6 +2546,29 @@ dependencies = [
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-core-audio"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
dependencies = [
"dispatch2",
"objc2 0.6.3",
"objc2-core-audio-types",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-core-audio-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
dependencies = [
"bitflags 2.10.0",
"objc2 0.6.3",
]
[[package]]
name = "objc2-core-data"
version = "0.2.2"
@@ -2463,7 +2599,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
"dispatch2",
"libc",
"objc2 0.6.3",
]
@@ -2875,6 +3013,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "pollster"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "portable-atomic"
version = "1.11.1"
@@ -3172,12 +3316,50 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "rfd"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
dependencies = [
"block2 0.6.2",
"dispatch2",
"js-sys",
"libc",
"log",
"objc2 0.6.3",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"percent-encoding",
"pollster",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"web-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rgb"
version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
[[package]]
name = "ringbuf"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c"
dependencies = [
"crossbeam-utils",
"portable-atomic",
"portable-atomic-util",
]
[[package]]
name = "roxmltree"
version = "0.20.0"

View File

@@ -7,9 +7,14 @@ edition = "2024"
bitfield = "0.19.3"
# iced = { version = "0.14.0", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] }
iced = { path = "../iced", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] }
iced_core = { path = "../iced/core", features = ["advanced"] }
rfd = "0.17.2"
# iced_graphics = { version = "0.14.0", features = ["geometry", "image"] }
# iced_widget = { version = "0.13.4", features = ["canvas", "image"] }
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", features = ["ansi", "chrono", "env-filter", "json", "serde"] }
bytes = "*"
cpal = "0.17.1"
ringbuf = "0.4.8"

View File

@@ -1,4 +1,31 @@
use std::{io::Read, process::Stdio};
use std::{
fmt::Arguments,
io::Read,
process::{Command, Stdio},
};
fn run(name: Arguments, cmd: &mut Command) {
let mut proc = cmd
.current_dir("src/test_roms")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to start process");
let rc = proc.wait();
if rc.is_err() || rc.is_ok_and(|s| !s.success()) {
let mut stdout = String::new();
proc.stdout
.unwrap()
.read_to_string(&mut stdout)
.expect("Failed to read stdout");
let mut stderr = String::new();
proc.stderr
.unwrap()
.read_to_string(&mut stderr)
.expect("Failed to read stderr");
panic!("Failed to run {name}\n=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}");
}
}
fn main() {
println!("cargo::rerun-if-changed=src/test_roms/");
@@ -16,30 +43,31 @@ fn main() {
let file_name = file.file_name();
let file_name = file_name.to_str().unwrap();
if let Some(file_name) = file_name.strip_suffix(".asm") {
let mut proc = std::process::Command::new("./asm6f")
.arg(file.file_name())
.arg(format!("{name}/{file_name}.nes"))
.current_dir("src/test_roms")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to start process");
let rc = proc.wait();
if rc.is_err() || rc.is_ok_and(|s| !s.success()) {
let mut stdout = String::new();
proc.stdout
.unwrap()
.read_to_string(&mut stdout)
.expect("Failed to read stdout");
let mut stderr = String::new();
proc.stderr
.unwrap()
.read_to_string(&mut stderr)
.expect("Failed to read stderr");
panic!(
"Failed to compile {file_name}\n=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}"
);
}
run(
format_args!("asm6f {file_name}"),
Command::new("./asm6f")
.arg(file.file_name())
.arg(format!("{name}/{file_name}.nes")),
);
} else if let Some(file_name) = file_name.strip_suffix(".s") {
run(
format_args!("ca65 {file_name}"),
Command::new("/home/matthew/cc65/bin/ca65")
.arg("-I")
.arg("common")
.arg("-o")
.arg(format!("{name}/{file_name}.o"))
.arg(file.file_name()),
);
run(
format_args!("ld65 {file_name}"),
Command::new("/home/matthew/cc65/bin/ld65")
.arg("-C")
.arg("nes.cfg")
.arg("-o")
.arg(format!("{name}/{file_name}.nes"))
.arg(format!("{name}/{file_name}.o"))
);
}
}
}

View File

@@ -1,15 +1,82 @@
use std::iter::repeat_n;
use iced::{
Element, Font,
widget::{column, text},
};
use tracing::debug;
macro_rules! lut {
($name:ident: [$ty:ty; $len:expr] = |$n:ident| $expr:expr) => {
const $name: [$ty; $len] = {
let mut table = [0; $len];
let mut $n = 0;
while $n < $len {
table[$n] = $expr;
$n += 1;
}
table
};
};
}
bitfield::bitfield! {
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct LengthTimerHigh(u8);
impl Debug;
length, set_length: 7, 3;
u16;
timer_high, set_timer_high: 2, 0;
}
#[derive(Debug, Clone)]
struct LengthCounter {
current: u16,
silenced: bool,
}
impl LengthCounter {
pub fn new() -> Self {
Self {
current: 0,
silenced: true,
}
}
pub fn clock(&mut self) {
if self.current == 0 {
self.silenced = true;
} else {
self.current -= 1;
}
}
pub fn write(&mut self, length: u8) {
// self.reg.0 = val;
self.silenced = false;
const LENGTH_LUT: [u16; 0x20] = [
10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20,
96, 22, 192, 24, 72, 26, 16, 28, 32, 3,
];
self.current = LENGTH_LUT[length as usize] + 1; // I think?
}
pub fn silenced(&self) -> bool {
self.silenced
}
}
bitfield::bitfield! {
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct DutyVol(u8);
impl Debug;
duty, set_duty: 7, 6;
r#loop, set_loop: 5;
length_counter_halt, set_length_counter_halt: 5;
const_vol, set_const_vol: 4;
volume, set_volume: 3, 0;
}
bitfield::bitfield! {
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Sweep(u8);
impl Debug;
enable, set_enable: 7;
@@ -18,41 +85,449 @@ bitfield::bitfield! {
shift, set_shift: 2, 0;
}
bitfield::bitfield! {
pub struct LengthTimerHigh(u8);
impl Debug;
length, set_length: 7, 3;
timer_high, set_timer_high: 2, 0;
}
#[derive(Debug, Clone)]
struct PulseChannel {
enabled: bool,
duty_vol: DutyVol,
sweep: Sweep,
sweep_reload: bool,
counter: LengthCounter,
period: u16,
period_timer: u16,
cur: u8,
sample: u8,
envelope_start: bool,
envelope_counter: u8,
envelope_divider: u8,
}
impl PulseChannel {
pub fn new() -> Self {
Self {
enabled: false,
duty_vol: DutyVol(0),
sweep: Sweep(0),
sweep_reload: false,
counter: LengthCounter::new(),
period: 0,
period_timer: 0,
cur: 0,
sample: 0,
envelope_start: false,
envelope_counter: 0,
envelope_divider: 0,
}
}
pub fn write(&mut self, offset: u16, val: u8) {
match offset {
0x00 => {
self.duty_vol.0 = val;
self.envelope_start = true;
}
0x01 => {
self.sweep.0 = val;
self.sweep_reload = true;
},
0x02 => self.period = self.period & 0x700 | (val as u16),
0x03 => {
let reg = LengthTimerHigh(val);
self.counter.write(reg.length());
self.period = (self.period & 0xFF) | (reg.timer_high() << 8);
self.period_timer = self.period;
self.cur = 0;
}
_ => unreachable!(),
}
}
fn volume(&self) -> u8 {
if self.duty_vol.const_vol() {
self.duty_vol.volume()
} else {
self.envelope_counter
}
}
pub fn clock(&mut self) {
if !self.enabled {
self.sample = 0;
return;
}
if self.period_timer == 0 {
self.period_timer = self.period;
self.cur = (self.cur + 1) % 8;
const DUTY: [[bool; 8]; 4] = [
[false, true, false, false, false, false, false, false],
[false, true, true, false, false, false, false, false],
[false, true, true, true, true, false, false, false],
[true, false, false, true, true, true, true, true],
];
self.sample = if DUTY[self.duty_vol.duty() as usize][self.cur as usize] {
self.volume()
} else {
0x00
};
} else {
self.period_timer -= 1;
}
// TODO
}
pub fn cur_sample(&self) -> u8 {
if self.enabled && !self.counter.silenced() {
self.sample
} else {
0
}
}
pub fn q_frame_clock(&mut self) {
if !self.enabled {
return;
}
if !self.duty_vol.length_counter_halt() {
self.counter.clock();
} else {
self.counter.silenced = false;
}
if !self.duty_vol.const_vol() {
if self.envelope_start || self.envelope_counter == 0 {
self.envelope_counter = 0xF;
self.envelope_divider = self.duty_vol.volume();
self.envelope_start = false;
} else if self.envelope_divider == 0 {
self.envelope_divider = self.duty_vol.volume();
self.envelope_counter -= 1;
} else {
self.envelope_divider -= 1;
}
}
}
pub fn h_frame_clock(&mut self) {
self.q_frame_clock();
}
pub fn view<T>(&self) -> Element<'_, T> {
text!(
"Square Channel
Evelope Volume: {0:>3} ${0:02X}
Constant Volume: {1}
Length Counter - Halted: {2}
Duty: {3:>3} ${3:02X}
Sweep - Shift: {4:>3} ${4:02X}
Sweep - Negate: {5}
Sweep - Period: {6:>3} ${6:02X}
Sweep - Enabled: {7}
Period: {8:>3} ${8:04X}
Length Counter - Reload Value: {9:>3} ${9:04X}
Enabled: {10}
Timer: {11:>3} ${11:04X}
Duty Position: {12:>3} ${12:02X}
Length Counter - Counter: {13:>3} ${13:02X}
Envelope - Counter: {14:>3} ${14:02X}
Envelope - Divider: {15:>3} ${15:02X}
Output: {16:>3} ${16:02X}
",
self.duty_vol.volume(),
self.duty_vol.const_vol(),
self.duty_vol.length_counter_halt(),
self.duty_vol.duty(),
self.sweep.shift(),
self.sweep.negate(),
self.sweep.period(),
self.sweep.enable(),
self.period,
0,
self.enabled,
self.period_timer,
self.cur, // ?
self.counter.current,
self.envelope_counter,
self.envelope_divider,
self.cur_sample(),
)
.font(Font::MONOSPACE)
.into()
}
}
bitfield::bitfield! {
pub struct CounterLoad(u8);
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct LengthCounterReg(u8);
impl Debug;
halt, set_halt: 7;
value, set_value: 6, 0;
}
#[derive(Debug, Clone)]
struct TriangleChannel {
enabled: bool,
}
struct NoiseChannel {
enabled: bool,
}
struct DeltaChannel {
enabled: bool,
length: LengthCounterReg,
reload: bool,
counter: LengthCounter,
length_counter: u16,
cur: u8,
period: u16,
period_timer: u16,
sample: u8,
}
impl TriangleChannel {
pub fn new() -> Self {
Self {
length: LengthCounterReg(0),
counter: LengthCounter::new(),
reload: false,
enabled: false,
sample: 0,
period: 0,
period_timer: 0,
cur: 0,
length_counter: 0,
}
}
pub fn write(&mut self, offset: u16, val: u8) {
match offset {
0x00 => {
self.length.0 = val;
}
0x01 => (),
0x02 => self.period = self.period & 0x700 | (val as u16),
0x03 => {
// self.length_load.0 = val;
// self.reload = true;
let reg = LengthTimerHigh(val);
self.counter.write(reg.length());
self.period = (self.period & 0xFF) | (reg.timer_high() << 8);
self.period_timer = self.period;
self.cur = 0;
}
_ => unreachable!(),
}
}
pub fn cur_sample(&self) -> u8 {
self.sample
}
pub fn clock(&mut self) {
if self.length_counter > 0 && self.period > 0 {
if self.period_timer == 0 {
self.period_timer = self.period;
const SAMPLES: [u8; 32] = [
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14, 15,
];
self.cur = (self.cur + 1) % SAMPLES.len() as u8;
self.sample = SAMPLES[self.cur as usize];
} else {
self.period_timer -= 1;
}
}
}
pub fn q_frame_clock(&mut self) {
if self.reload {
self.length_counter = self.length.value() as u16;
self.reload = self.length.halt();
} else if self.length_counter == 0 {
} else {
self.length_counter -= 1;
}
}
pub fn h_frame_clock(&mut self) {
self.q_frame_clock();
}
pub fn view<T>(&self) -> Element<'_, T> {
text!(
"Triangle Channel
Linear Counter - Reload: {0:>3} ${0:02X}
Linear Counter - Halted: {1}
Period: {2:>3} ${2:04X}
Length Counter - Reload Value: {3:>3} ${3:04X}
Enabled: {4}
Timer: {5:>3} ${5:02X}
Frequency: ???
Sequence Position: {6:>3} ${6:02X}
Length Counter - Counter: {7:>3} ${7:02X}
Linear Counter - Counter: {8:>3} ${8:02X}
Linear Counter - Reload Flag: {9}
Output: {10:>3} ${10:02X}
",
self.length.value(),
self.length.halt(),
self.period,
0,
self.enabled,
self.period_timer,
self.cur,
0,
self.length_counter,
self.reload,
self.cur_sample(),
)
.font(Font::MONOSPACE)
.into()
}
}
bitfield::bitfield! {
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct NoiseEnvelope(u8);
impl Debug;
length_counter_halt, set_length_counter_halt: 5;
constant_volume, set_constant_volume: 4;
volume, set_volume: 3, 0;
}
bitfield::bitfield! {
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct ModePeriod(u8);
impl Debug;
mode, set_mode: 7;
period, set_period: 3, 0;
}
bitfield::bitfield! {
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct LengthCounterLoad(u8);
impl Debug;
length, set_length: 7, 3;
}
#[derive(Debug, Clone)]
struct NoiseChannel {
enabled: bool,
evelope: NoiseEnvelope,
mode_period: ModePeriod,
length_counter_load: LengthCounterLoad,
shr: u16,
count: u16,
length_counter: u8,
}
impl NoiseChannel {
pub fn new() -> Self {
Self {
enabled: false,
evelope: NoiseEnvelope(0),
mode_period: ModePeriod(0),
length_counter_load: LengthCounterLoad(0),
shr: 1,
count: 0,
length_counter: 0,
}
}
pub fn write(&mut self, offset: u16, val: u8) {
match offset {
0x00 => (),
0x01 => (),
0x02 => (),
0x03 => (),
_ => unreachable!(),
}
}
pub fn cur_sample(&self) -> u8 {
if self.length_counter == 0 || self.shr & 1 == 1 {
0
} else {
self.evelope.volume()
}
}
pub fn clock(&mut self) {
const PERIODS: &[u16] = &[
4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4096,
];
if self.count == PERIODS[self.mode_period.period() as usize] / 2 {
self.count = 0;
let bit_0 = (self.shr & 0b1) << 14;
let other = if self.mode_period.mode() {
(self.shr & 0b100_0000) << 8
} else {
(self.shr & 0b10) << 13
};
self.shr = (self.shr >> 1) | (bit_0 ^ other);
} else {
self.count += 1;
}
}
pub fn q_frame_clock(&mut self) {}
pub fn h_frame_clock(&mut self) {
self.q_frame_clock();
}
pub fn view<T>(&self) -> Element<'_, T> {
text!("").font(Font::MONOSPACE).into()
}
}
#[derive(Debug, Clone)]
struct DeltaChannel {
enabled: bool,
sample: u8,
}
impl DeltaChannel {
pub fn new() -> Self {
Self {
enabled: false,
sample: 0,
}
}
pub fn write(&mut self, offset: u16, val: u8) {
match offset {
0x00 => (),
0x01 => (),
0x02 => (),
0x03 => (),
_ => unreachable!(),
}
}
pub fn cur_sample(&self) -> u8 {
self.sample
}
pub fn clock(&mut self) {}
pub fn q_frame_clock(&mut self) {}
pub fn h_frame_clock(&mut self) {}
pub fn int(&self) -> bool {
false
}
pub fn view<T>(&self) -> Element<'_, T> {
text!("").font(Font::MONOSPACE).into()
}
}
#[derive(Debug, Clone)]
struct FrameCounter {
count: usize,
mode_5_step: bool,
interrupt_enabled: bool,
irq: bool,
}
#[derive(Clone)]
pub struct APU {
pulse_1: PulseChannel,
pulse_2: PulseChannel,
triangle: TriangleChannel,
noise: NoiseChannel,
dmc: DeltaChannel,
frame_counter: u8,
frame_counter: FrameCounter,
samples: Vec<u8>,
}
impl std::fmt::Debug for APU {
@@ -69,21 +544,53 @@ impl std::fmt::Debug for APU {
impl APU {
pub fn init() -> Self {
Self {
pulse_1: PulseChannel { enabled: false },
pulse_2: PulseChannel { enabled: false },
triangle: TriangleChannel { enabled: false },
noise: NoiseChannel { enabled: false },
dmc: DeltaChannel { enabled: false },
frame_counter: 0,
pulse_1: PulseChannel::new(),
pulse_2: PulseChannel::new(),
triangle: TriangleChannel::new(),
noise: NoiseChannel::new(),
dmc: DeltaChannel::new(),
frame_counter: FrameCounter {
mode_5_step: false,
interrupt_enabled: true,
count: 0,
irq: false,
},
samples: vec![0; 100],
}
}
pub fn reset(&mut self) {
*self = Self::init();
}
pub fn read_reg(&mut self, offset: u16) -> u8 {
// println!("APU read: {offset:02X}");
match offset {
0x15 => {
let val = (u8::from(self.dmc.int()) << 7)
| (u8::from(self.frame_counter.irq) << 6)
| (u8::from(self.dmc.enabled) << 4)
| (u8::from(self.noise.enabled) << 3)
| (u8::from(self.triangle.enabled) << 2)
| (u8::from(self.pulse_2.enabled) << 1)
| (u8::from(self.pulse_1.enabled) << 0);
self.frame_counter.irq = false;
val
}
_ => panic!("No register at {:X}", offset),
}
}
pub fn write_reg(&mut self, offset: u16, val: u8) {
// println!("APU write: {offset:02X} <= {val:02X}");
match offset {
0x00..0x04 => self.pulse_1.write(offset - 0x00, val),
0x04..0x08 => self.pulse_2.write(offset - 0x04, val),
0x08..0x0C => self.triangle.write(offset - 0x08, val),
0x0C..0x10 => self.noise.write(offset - 0x0C, val),
0x10..0x14 => self.dmc.write(offset - 0x10, val),
0x14 => (),
0x15 => {
self.dmc.enabled = val & 0b0001_0000 != 0;
self.noise.enabled = val & 0b0000_1000 != 0;
@@ -91,31 +598,112 @@ impl APU {
self.pulse_2.enabled = val & 0b0000_0010 != 0;
self.pulse_1.enabled = val & 0b0000_0001 != 0;
}
0x10 => {
assert_eq!(val, 0x00);
// TODO: implement this value
}
0x11 => {
// TODO: load dmc counter with (val & 7F)
0x17 => {
self.frame_counter.mode_5_step = val & 0b1000_0000 != 0;
self.frame_counter.interrupt_enabled = val & 0b0100_0000 == 0;
if !self.frame_counter.interrupt_enabled {
self.frame_counter.irq = false;
}
}
// _ => (),
_ => panic!("No register at {:X}", offset),
}
}
pub fn run_one_clock_cycle(&mut self) -> bool {
fn q_frame_clock(&mut self) {
self.pulse_1.q_frame_clock();
self.pulse_2.q_frame_clock();
self.triangle.q_frame_clock();
self.noise.q_frame_clock();
self.dmc.q_frame_clock();
}
fn h_frame_clock(&mut self) {
self.pulse_1.h_frame_clock();
self.pulse_2.h_frame_clock();
self.triangle.h_frame_clock();
self.noise.h_frame_clock();
self.dmc.h_frame_clock();
}
fn gen_sample(&mut self) {
lut!(P_LUT: [u8; 32] = |n| (95.52 / (8128.0 / n as f64 + 100.0) * 255.0) as u8);
let pulse_out = P_LUT[(self.pulse_1.cur_sample() + self.pulse_2.cur_sample()) as usize];
lut!(TND_LUT: [u8; 204] = |n| (163.67 / (24329.0 / n as f64 + 100.0) * 255.0) as u8);
let tnd_out = TND_LUT[3 * self.triangle.cur_sample() as usize
+ 2 * self.noise.cur_sample() as usize
+ self.dmc.cur_sample() as usize];
self.samples.push(pulse_out + tnd_out);
}
pub fn run_one_clock_cycle(&mut self, ppu_cycle: usize) -> bool {
if ppu_cycle % 6 == 1 {
// APU Frame Counter clock cycle
self.frame_counter.count += 1;
if self.frame_counter.count == 3728 {
self.q_frame_clock();
} else if self.frame_counter.count == 7456 {
self.h_frame_clock();
} else if self.frame_counter.count == 11185 {
self.q_frame_clock();
} else if self.frame_counter.count == 14914 && !self.frame_counter.mode_5_step {
self.frame_counter.irq = self.frame_counter.interrupt_enabled;
self.h_frame_clock();
} else if self.frame_counter.count == 14915 && !self.frame_counter.mode_5_step {
self.frame_counter.count = 0;
self.frame_counter.irq = self.frame_counter.interrupt_enabled;
} else if self.frame_counter.count == 18640 {
self.h_frame_clock();
self.frame_counter.count = 0;
}
self.pulse_1.clock();
self.pulse_2.clock();
self.noise.clock();
self.dmc.clock();
}
if ppu_cycle % 3 == 1 {
self.triangle.clock();
}
if ppu_cycle % (6 * 4) == 1 {
self.gen_sample();
}
false
}
pub fn get_frame_samples(&self) -> &[u8] {
// println!("'Frame' of samples: {}", self.samples.len());
&self.samples
}
pub fn reset_frame_samples(&mut self) {
self.samples.clear();
}
pub fn peek_nmi(&self) -> bool {
false
}
pub fn nmi_waiting(&mut self) -> bool {
false
}
pub fn peek_irq(&self) -> bool {
false
self.frame_counter.irq
}
pub fn irq_waiting(&mut self) -> bool {
// TODO: implement logic
false
self.frame_counter.irq
}
pub fn view<'s, T: 's>(&'s self) -> Element<'s, T> {
column![
self.pulse_1.view(),
self.pulse_2.view(),
self.triangle.view(),
self.noise.view(),
self.dmc.view(),
]
.into()
}
}

96
src/audio.rs Normal file
View File

@@ -0,0 +1,96 @@
use std::{
sync::{
atomic::AtomicBool, mpsc::{channel, Sender, TryRecvError}, Arc
},
time::Instant,
};
use cpal::{
Device, FrameCount, Host, SampleFormat, Stream,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
use ringbuf::{
HeapRb, SharedRb,
traits::{Consumer, Observer, Producer, Split},
wrap::caching::Caching,
};
type SampleTx = Caching<Arc<HeapRb<u8>>, true, false>;
// TODO: Audio should be run through a low-pass filter,
// a high-pass filter, as well as some kind of envelope
// around pause events
pub struct Audio {
_host: Host,
_device: Device,
_stream: Stream,
rb: SampleTx,
last: usize,
max: usize,
paused: Arc<AtomicBool>,
}
impl Audio {
pub fn init() -> Self {
const BUFFER_SIZE: usize = 1 << 10;
let host = cpal::default_host();
let device = host.default_output_device().unwrap();
// let mut configs = device.supported_output_configs().unwrap();
// dbg!(configs.find(|c| c.sample_format() == SampleFormat::U8));
// let v = dbg!(configs.next().unwrap().buffer_size());
// let mut t = 0;
let (prod, mut cons) = SharedRb::new(BUFFER_SIZE * 1024 * 1024).split();
let paused = Arc::new(AtomicBool::new(true));
let paused_inner = Arc::clone(&paused);
let stream = device
.build_output_stream(
&cpal::StreamConfig {
channels: 1,
sample_rate: 60 * 3723,
buffer_size: cpal::BufferSize::Fixed(BUFFER_SIZE as FrameCount),
},
move |a: &mut [u8], _b| {
if !paused_inner.load(std::sync::atomic::Ordering::Acquire) {
let taken = cons.pop_slice(a);
a[taken..].fill(128);
}
},
|e| eprintln!("Audio: {e}"),
None,
)
.unwrap();
stream.play().unwrap();
Self {
_host: host,
_device: device,
_stream: stream,
rb: prod,
paused,
last: 0,
max: 0,
}
}
pub fn pause(&mut self) {
self.paused.store(true, std::sync::atomic::Ordering::Release);
let _ = self._stream.pause();
}
pub fn submit(&mut self, samples: &[u8]) {
let start = self.rb.occupied_len();
self.max = self.max.max(self.last - start);
println!("Buffer size: {:07}, Max: {:07}", start, self.max);
println!(
"Adding: {:07}, Played: {:07}",
samples.len(),
self.last - start
);
self.rb.push_slice(samples);
self.last = self.rb.occupied_len();
if self.last > 9000 {
self.paused.store(false, std::sync::atomic::Ordering::Release);
let _ = self._stream.play();
}
}
}

View File

@@ -1,14 +1,98 @@
pub struct Controllers {}
use bitfield::bitfield;
bitfield! {
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
pub struct ControllerState(u8);
impl Debug;
pub a, set_a: 0;
pub b, set_b: 1;
pub select, set_select: 2;
pub start, set_start: 3;
pub up, set_up: 4;
pub down, set_down: 5;
pub left, set_left: 6;
pub right, set_right: 7;
}
#[derive(Debug, Clone, Copy)]
struct ControllerShiftReg {
state: ControllerState,
shift: u8,
}
impl ControllerShiftReg {
fn new() -> Self {
Self {
state: ControllerState(0),
shift: 0,
}
}
fn read(&mut self) -> u8 {
let val = self.shift & 1;
// Reads after the first 8 are always 1
self.shift = (self.shift >> 1) | 0x80;
val
}
fn load(&mut self) {
self.shift = self.state.0;
}
}
#[derive(Debug, Clone)]
pub struct Controllers {
strobe: bool,
primary: ControllerShiftReg,
secondary: ControllerShiftReg,
}
impl Controllers {
pub fn init() -> Self {
Self {}
Self {
strobe: false,
primary: ControllerShiftReg::new(),
secondary: ControllerShiftReg::new(),
}
}
pub fn reset(&mut self) {
*self = Self::init();
}
pub fn read_joy1(&mut self) -> u8 {
0
if self.strobe {
self.primary.load();
}
// TODO: Technically some kind of open-bus for all but the lowest 3 bits
self.primary.read() | 0x40
}
pub fn read_joy2(&mut self) -> u8 {
0
if self.strobe {
self.secondary.load();
}
self.secondary.read() | 0x40
}
pub fn write_joy_strobe(&mut self, val: u8) {
if val & 1 == 0 {
if self.strobe {
self.primary.load();
self.secondary.load();
}
self.strobe = false;
} else {
self.strobe = true;
self.primary.load();
self.secondary.load();
}
}
pub fn controller_1(&mut self) -> &mut ControllerState {
&mut self.primary.state
}
pub fn controller_2(&mut self) -> &mut ControllerState {
&mut self.secondary.state
}
pub fn write_joy_strobe(&mut self, val: u8) { }
}

2019
src/cpu.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
use std::num::NonZeroUsize;
#[derive(Debug, Clone)]
pub struct DebugLog {
current: String,
history: Vec<String>,
@@ -42,6 +43,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,30 @@
use std::rc::Rc;
use iced::{
Element,
Length::Fill,
Length::{self, Fill},
Point, Renderer, Size,
advanced::{
Widget,
layout::Node,
widget::{
Tree,
tree::{State, Tag},
},
},
mouse,
widget::{
self, button, checkbox, column, container::bordered_box, image, number_input, row,
self, Canvas, Text, button,
canvas::{Frame, Program},
checkbox, column,
container::bordered_box,
hex_input, image, number_input, row,
rule::horizontal,
scrollable, text,
},
};
use crate::{CycleResult, NES};
use crate::{Break, CycleResult, NES, PPU, mem::Mapped};
#[derive(Debug, Clone)]
pub struct DebuggerState {
@@ -17,12 +34,13 @@ pub struct DebuggerState {
scan_lines: usize,
to_scan_line: usize,
frames: usize,
breakpoint: usize,
}
#[derive(Debug, Clone)]
pub enum DebuggerMessage {
Run,
Pause,
// Run,
// Pause,
SetPPUCycles(usize),
RunPPUCycles,
SetCPUCycles(usize),
@@ -35,6 +53,37 @@ pub enum DebuggerMessage {
RunToScanLine,
SetFrames(usize),
RunFrames,
SetBreakpoint(usize),
RunBreakpoint,
}
pub fn hex16<'a, Theme, Renderer>(val: u16) -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: iced::advanced::text::Renderer,
{
text(format!("{val:04X}"))
}
pub fn hex8<'a, Theme, Renderer>(val: u8) -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: iced::advanced::text::Renderer,
{
text(format!("{val:02X}"))
}
pub fn bin8<'a, Theme, Renderer>(val: u8) -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: iced::advanced::text::Renderer,
{
text(format!("{val:08b}"))
}
pub fn bin32<'a, Theme, Renderer>(val: u32) -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: iced::advanced::text::Renderer,
{
text(format!("{val:032b}"))
}
impl DebuggerState {
@@ -46,18 +95,13 @@ impl DebuggerState {
scan_lines: 1,
to_scan_line: 1,
frames: 1,
breakpoint: 0xEA5A,
// cpu_cycles: 1,
}
}
pub fn view<'s>(&'s self, nes: &'s NES) -> Element<'s, DebuggerMessage> {
column![
row![
button(image("./images/ic_fluent_play_24_filled.png"))
.on_press(DebuggerMessage::Run),
button(image("./images/ic_fluent_pause_24_filled.png"))
.on_press(DebuggerMessage::Pause),
],
iced::widget::rule::horizontal(2.),
row![column![
text("Status"),
@@ -66,7 +110,7 @@ impl DebuggerState {
labelled("X:", text(format!("{:02X}", nes.cpu.x))),
labelled("Y:", text(format!("{:02X}", nes.cpu.y))),
labelled("PC:", text(format!("{:04X}", nes.cpu.pc))),
labelled("Cycle:", text(format!("{}", nes.cycle))),
labelled("Cycle:", text(format!("{}", nes.cpu_cycle()))),
labelled("SP:", text(format!("{:02X}", nes.cpu.sp))),
]
.spacing(5.),
@@ -85,38 +129,57 @@ 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(if nes.ppu().scanline == 261 { -1 } else { nes.ppu().scanline as isize })),
labelled("PPU Cycle", text(nes.ppu().cycle)),
labelled("Frame", text(nes.ppu().frame_count)),
labelled("V:", hex16(nes.ppu().background.v)),
labelled("T:", hex16(nes.ppu().background.t)),
labelled("X:", hex8(nes.ppu().background.x)),
text(""),
labelled("NT:", hex8(nes.ppu().background.cur_nametable)),
labelled2(
"AT:",
hex8(nes.ppu().background.next_attr),
hex16(
0x23C0
| (nes.ppu().background.v & 0x0C00)
| ((nes.ppu().background.v >> 4) & 0x38)
| ((nes.ppu().background.v >> 2) & 0x07)
)
),
labelled("AT:", hex8(nes.ppu().background.next_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("Sprite 0 Hit", nes.ppu().sprite_zero_hit),
labelled_box("Sprite 0 Overflow", nes.ppu().oam.overflow),
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(
@@ -155,16 +218,26 @@ impl DebuggerState {
DebuggerMessage::SetFrames,
DebuggerMessage::RunFrames
),
run_type_hex(
"To Address:",
self.breakpoint,
DebuggerMessage::SetBreakpoint,
DebuggerMessage::RunBreakpoint
),
],
]
.spacing(5.),
scrollable(column(
nes.debug_log()
.history()
.into_iter()
.rev()
.map(|s| text(s).line_height(0.9).into())
).spacing(0))
horizontal(2),
scrollable(
column(
nes.debug_log()
.history()
.into_iter()
.rev()
.map(|s| text(s).line_height(0.9).into())
)
.spacing(0)
)
.width(Fill),
],],
]
@@ -175,16 +248,43 @@ impl DebuggerState {
fn run_n_clock_cycles(nes: &mut NES, n: usize) {
for _ in 0..n {
nes.run_one_clock_cycle();
}
}
fn run_until(nes: &mut NES, mut f: impl FnMut(CycleResult, &NES) -> bool) {
loop {
if f(nes.run_one_clock_cycle(), nes) {
if nes.run_one_clock_cycle(&Break::default()).dbg_int || nes.halted() {
break;
}
}
}
fn run_until(
nes: &mut NES,
br: &Break,
mut f: impl FnMut(CycleResult, &NES) -> bool,
// mut count: usize,
) {
// Always run at least 1 cycle
let mut res = nes.run_one_clock_cycle(&Break::default());
while !nes.halted() && !res.dbg_int && !f(res, nes) {
// if res.dbg_int || f(res, nes) {
// count -= 1;
// if count <= 0 {
// break;
// }
// }
// if nes.halted() {
// break;
// }
res = nes.run_one_clock_cycle(br);
}
}
fn run_until_n(
nes: &mut NES,
br: &Break,
mut f: impl FnMut(CycleResult, &NES) -> bool,
mut count: usize,
) {
while count > 0 {
Self::run_until(nes, br, &mut f);
count -= 1;
}
}
pub fn update(&mut self, message: DebuggerMessage, nes: &mut NES) {
match message {
@@ -192,18 +292,50 @@ impl DebuggerState {
DebuggerMessage::SetCPUCycles(n) => self.cpu_cycles = n,
DebuggerMessage::SetInstructions(n) => self.instructions = n,
DebuggerMessage::SetScanLines(n) => self.scan_lines = n,
DebuggerMessage::SetToScanLine(n) => self.to_scan_line = n,
DebuggerMessage::SetToScanLine(n) => self.to_scan_line = n.min(261), // Max scanline is 261
DebuggerMessage::SetFrames(n) => self.frames = n,
DebuggerMessage::SetBreakpoint(n) => self.breakpoint = n,
DebuggerMessage::RunPPUCycles => Self::run_n_clock_cycles(nes, self.ppu_cycles),
DebuggerMessage::RunCPUCycles => Self::run_n_clock_cycles(nes, self.cpu_cycles * 3),
DebuggerMessage::RunInstructions => Self::run_until(nes, |c, _| c.cpu_exec),
// DebuggerMessage::RunInstructions => Self::run_until_n(
// nes,
// &Break {
// cpu_exec: true,
// ..Break::default()
// },
// |_, _| false,
// self.instructions,
// ),
DebuggerMessage::RunInstructions => Self::run_until_n(
nes,
&Break {
..Break::default()
},
|res, _| res.cpu_exec,
self.instructions,
),
DebuggerMessage::RunScanLines => Self::run_n_clock_cycles(nes, self.scan_lines * 341),
DebuggerMessage::RunToScanLine => {
Self::run_until(nes, |_, n| n.ppu.scanline == self.to_scan_line)
}
DebuggerMessage::RunToScanLine => Self::run_until(
nes,
&Break {
ppu_scanline: true,
..Break::default()
},
|_, n| n.ppu.scanline == self.to_scan_line,
),
DebuggerMessage::RunFrames => Self::run_n_clock_cycles(nes, self.frames * 341 * 262),
DebuggerMessage::Run => todo!(),
DebuggerMessage::Pause => todo!(),
DebuggerMessage::RunBreakpoint => Self::run_until(
nes,
&Break {
break_points: vec![self.breakpoint as u16],
..Break::default()
},
|_, nes| nes.cpu.pc as usize == self.breakpoint,
),
// DebuggerMessage::Run => {
// Self::run_until(nes, &Break { ..Break::default() }, |_, nes| nes.halted())
// }
// DebuggerMessage::Pause => todo!(),
}
}
}
@@ -223,6 +355,21 @@ fn run_type<'a, Message: Clone + 'a>(
.spacing(1.)
.into()
}
fn run_type_hex<'a, Message: Clone + 'a>(
label: &'a str,
val: usize,
update: impl Fn(usize) -> Message + 'a,
run: Message,
) -> Element<'a, Message> {
row![
widget::container(text(label)).padding(2.),
widget::container(hex_input(val).on_input(update).on_submit(run.clone())).padding(2.),
widget::container(button(image("./images/ic_fluent_play_24_filled.png")).on_press(run))
.padding(2.),
]
.spacing(1.)
.into()
}
pub fn labelled<'a, Message: 'a>(
label: &'a str,
@@ -236,8 +383,279 @@ 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 Mapped, &'a PPU),
PatternTable(&'a Mapped, &'a PPU),
Palette(&'a Mapped, &'a PPU),
}
impl<Message, T> Program<Message, T> for DbgImage<'_> {
type State = ();
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
_theme: &T,
_bounds: iced::Rectangle,
_cursor: mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
// const SIZE: f32 = 2.;
let mut name_table_frame =
Frame::new(renderer, Size::new(256. * 4. + 260. * 2., 256. * 4.));
name_table_frame.scale(2.);
// println!("Position: {:?}", cursor.position());
match self {
DbgImage::NameTable(mem, ppu) => ppu.render_name_table(mem, &mut name_table_frame),
DbgImage::PatternTable(mem, ppu) => {
ppu.render_pattern_tables(mem, &mut name_table_frame)
}
DbgImage::Palette(_, ppu) => ppu.render_palette(&mut name_table_frame),
};
vec![name_table_frame.into_geometry()]
}
}
impl DbgImage<'_> {
fn width(&self) -> Length {
match self {
DbgImage::NameTable(_, _) => Length::Fixed(512. * 2.),
DbgImage::PatternTable(_, _) => Length::Fixed(16. * 8. * 2.),
DbgImage::Palette(_, _) => Length::Fixed(40. * 2.),
}
}
fn height(&self) -> Length {
match self {
DbgImage::NameTable(_, _) => Length::Fixed(512. * 2.),
DbgImage::PatternTable(_, _) => Length::Fixed(16. * 8. * 2. * 2.),
DbgImage::Palette(_, _) => Length::Fixed(80. * 2.),
}
}
fn help(&self, cursor: Point) -> Option<String> {
match self {
DbgImage::NameTable(mem, ppu) => ppu.name_cursor_info(mem, cursor),
DbgImage::PatternTable(_, ppu) => ppu.pattern_cursor_info(cursor),
DbgImage::Palette(_, ppu) => ppu.palette_cursor_info(cursor),
}
}
}
struct DbgImageSetup<'a, M, T: text::Catalog> {
dbg: DbgImage<'a>,
image: Canvas<DbgImage<'a>, M, T>,
text: Text<'a, T>,
padding: f32,
drawn: bool,
// image: DbgImage<'a>,
// text: te,
}
// pub trait Container<Message, Theme, Renderer> {
// // Not ideal
// fn children(&self) -> &[&dyn Widget<Message, Theme, Renderer>];
// }
impl<'s, Message, Theme> Widget<Message, Theme, Renderer> for DbgImageSetup<'s, Message, Theme>
where
Theme: text::Catalog + 's,
{
fn size(&self) -> Size<iced::Length> {
// self.
Size::new(Fill, Fill)
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &iced::advanced::layout::Limits,
) -> Node {
let img_node = self.image.layout(&mut tree.children[0], renderer, limits);
let txt_node = Widget::<Message, Theme, _>::layout(
&mut self.text,
&mut tree.children[1],
renderer,
&limits.shrink(Size::new(
img_node.size().width + self.padding * 2.,
self.padding * 2.,
)),
)
.move_to(Point::new(
img_node.size().width + self.padding,
self.padding,
));
Node::with_children(limits.max(), vec![img_node, txt_node])
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &iced::advanced::renderer::Style,
layout: iced::advanced::Layout<'_>,
cursor: iced::advanced::mouse::Cursor,
viewport: &iced::Rectangle,
) {
self.image.draw(
&tree.children[0],
renderer,
theme,
style,
layout.child(0),
cursor,
viewport,
);
Widget::<Message, Theme, _>::draw(
&self.text,
&tree.children[1],
renderer,
theme,
style,
layout.child(1),
cursor,
viewport,
)
}
fn tag(&self) -> Tag {
Tag::of::<Rc<String>>()
}
fn state(&self) -> State {
State::new(Rc::new(String::new()))
}
fn children(&self) -> Vec<Tree> {
vec![
Tree::new(&self.image as &dyn Widget<Message, Theme, Renderer>),
Tree::new(&self.text as &dyn Widget<Message, Theme, Renderer>),
]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(&[
&self.image as &dyn Widget<Message, Theme, Renderer>,
&self.text as &dyn Widget<Message, Theme, Renderer>,
]);
}
fn operate(
&mut self,
tree: &mut Tree,
layout: iced::advanced::Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced::advanced::widget::Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |op| {
self.image
.operate(&mut tree.children[0], layout.child(0), renderer, op);
Widget::<Message, Theme, _>::operate(
&mut self.text,
&mut tree.children[1],
layout.child(1),
renderer,
op,
);
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &iced::Event,
layout: iced::advanced::Layout<'_>,
cursor: iced::advanced::mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn iced::advanced::Clipboard,
shell: &mut iced::advanced::Shell<'_, Message>,
viewport: &iced::Rectangle,
) {
self.image.update(
&mut tree.children[0],
event,
layout.child(0),
cursor,
renderer,
clipboard,
shell,
viewport,
);
if matches!(event, iced::Event::Mouse(mouse::Event::CursorMoved { .. })) || !self.drawn {
if let Some(help) = cursor
.position_in(layout.child(0).bounds())
.and_then(|pos| self.dbg.help(Point::new(pos.x / 2., pos.y / 2.)))
{
self.text = text(help);
shell.invalidate_layout();
shell.request_redraw();
}
self.drawn = true;
}
Widget::<Message, Theme, _>::update(
&mut self.text,
&mut tree.children[1],
event,
layout.child(1),
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn mouse_interaction(
&self,
_tree: &iced::advanced::widget::Tree,
_layout: iced::advanced::Layout<'_>,
_cursor: iced::advanced::mouse::Cursor,
_viewport: &iced::Rectangle,
_renderer: &Renderer,
) -> iced::advanced::mouse::Interaction {
iced::advanced::mouse::Interaction::default()
}
fn overlay<'a>(
&'a mut self,
_tree: &'a mut iced::advanced::widget::Tree,
_layout: iced::advanced::Layout<'a>,
_renderer: &Renderer,
_viewport: &iced::Rectangle,
_translation: iced::Vector,
) -> Option<iced::advanced::overlay::Element<'a, Message, Theme, Renderer>> {
None
}
}
pub fn dbg_image<'a, Message: 'a>(img: DbgImage<'a>) -> Element<'a, Message> {
Element::new(DbgImageSetup {
dbg: img,
image: Canvas::new(img).width(img.width()).height(img.height()),
text: text(""),
padding: 10.,
drawn: false,
})
}

View File

@@ -1,19 +1,103 @@
use std::fmt;
use std::{
fmt::{self, Display},
sync::Arc,
};
use iced::{widget::{column, lazy, row, text}, Element, Font, Length::Fill, Task};
use iced::{
Element, Fill, Font, Padding, Task,
advanced::graphics::text::{
FontSystem,
cosmic_text::{
Attrs, Family, Metrics,
fontdb::{ID, Query},
},
font_system,
},
keyboard::{Key, key::Named},
mouse::{Button, ScrollDelta},
widget::{column, lazy, row, text},
};
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 crate::{PPU, mem::Mapped, ppu::PPUMMRegisters};
pub trait Memory {
fn peek(&self, val: u16) -> Option<u8>;
fn peek(&self, val: usize) -> Option<u8>;
fn len(&self) -> usize;
fn edit_ver(&self) -> usize;
}
#[derive(Debug, Clone, Copy)]
struct Cpu<'a>(&'a Mapped);
impl Memory for Cpu<'_> {
fn peek(&self, val: usize) -> Option<u8> {
self.0.peek_cpu(val as u16)
}
fn len(&self) -> usize {
0x10000
}
fn edit_ver(&self) -> usize {
self.0.cpu_edit_ver()
}
}
#[derive(Debug, Clone, Copy)]
struct Ppu<'a>(&'a Mapped, &'a PPU);
impl Memory for Ppu<'_> {
fn peek(&self, val: usize) -> Option<u8> {
match self.0.peek_ppu(val as u16) {
Ok(v) => Some(v),
Err(Some((PPUMMRegisters::Palette, off))) => Some(self.1.palette.ram(off as u8)),
Err(None) => None,
}
}
fn len(&self) -> usize {
0x4000
}
fn edit_ver(&self) -> usize {
self.0.ppu_edit_ver()
}
}
#[derive(Debug, Clone, Copy)]
pub struct Oam<'a>(pub &'a PPU);
impl Memory for Oam<'_> {
fn peek(&self, val: usize) -> Option<u8> {
Some(self.0.peek_oam(val as u8))
}
fn len(&self) -> usize {
0x100
}
fn edit_ver(&self) -> usize {
self.0.oam_edit_ver()
}
}
#[derive(Debug, Clone)]
pub enum HexEvent {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HexView {
}
pub struct HexView {}
struct Val(Option<u8>);
impl fmt::Display for Val {
@@ -31,9 +115,9 @@ impl HexView {
Self {}
}
pub fn render<'a>(&self, mem: &'a impl Memory) -> Element<'a, HexEvent> {
struct Row<'a, M>(u16, &'a M);
impl<M: Memory> fmt::Display for Row<'_, M> {
pub fn render_any<'a, M: Memory + Copy + 'a>(&self, mem: M) -> Element<'a, HexEvent> {
struct Row<M: Memory>(usize, M);
impl<'a, M: Memory + 'a> fmt::Display for Row<M> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Val(self.1.peek(self.0)))?;
for i in 1..16 {
@@ -44,15 +128,477 @@ impl HexView {
}
column![
text!("Hex view"),
iced::widget::scrollable(lazy(mem.edit_ver(), |_| column([
text!(" | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F").font(Font::MONOSPACE).into()
].into_iter().chain((0..u16::MAX).step_by(16).map(|off| {
text!(" {off:04X} | {}", Row(off, mem)).font(Font::MONOSPACE).into()
}))))).width(Fill),
].width(Fill).into()
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")
.font(Font::MONOSPACE)
.into()
]
.into_iter()
.chain((0..mem.len()).step_by(16).map(|off| {
text!(" {off:04X} | {}", Row(off, mem))
.font(Font::MONOSPACE)
.into()
}))
)))
.width(Fill),
]
.width(Fill)
.into()
}
pub fn render_cpu<'a>(&self, mem: &'a Mapped) -> Element<'a, HexEvent> {
// self.render_any(Cpu(mem))
Element::new(
hex_editor::<Cpu<'a>, HexEvent, iced::Renderer>(Cpu(mem)).font(Font::MONOSPACE),
)
}
pub fn render_ppu<'a>(&self, mem: &'a Mapped, ppu: &'a PPU) -> Element<'a, HexEvent> {
// self.render_any(Ppu(mem, ppu))
Element::new(
hex_editor::<Ppu<'a>, HexEvent, iced::Renderer>(Ppu(mem, ppu)).font(Font::MONOSPACE),
)
}
pub fn render_oam<'a>(&self, ppu: &'a PPU) -> Element<'a, HexEvent> {
Element::new(
hex_editor::<Oam<'a>, HexEvent, iced::Renderer>(Oam(ppu)).font(Font::MONOSPACE),
)
}
pub fn update(&mut self, ev: HexEvent) -> Task<HexEvent> {
todo!()
}
}
pub trait Buffer {
fn peek(&self, val: usize) -> Option<u8>;
fn len(&self) -> usize;
}
impl Buffer for &[u8] {
fn peek(&self, val: usize) -> Option<u8> {
self.get(val).copied()
}
fn len(&self) -> usize {
self.iter().len()
}
}
impl<M: Memory> Buffer for M {
fn peek(&self, val: usize) -> Option<u8> {
self.peek(val)
}
fn len(&self) -> usize {
self.len()
}
}
pub fn hex_editor<B: Buffer, M, R: iced_core::text::Renderer>(raw: B) -> HexEditor<B, M, R> {
HexEditor {
val: raw,
on_edit: None,
font: None,
font_size: None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct HexEdit {
position: usize,
old_value: u8,
new_value: u8,
}
pub struct HexEditor<B, M, R: iced_core::text::Renderer> {
val: B,
font_size: Option<Pixels>,
font: Option<R::Font>,
on_edit: Option<Box<dyn Fn(HexEdit) -> M>>,
}
impl<B, M, R> HexEditor<B, M, R>
where
R: iced_core::text::Renderer,
{
pub fn font(mut self, font: R::Font) -> Self {
self.font = Some(font);
self
}
}
impl<B: Buffer, M, R> HexEditor<B, M, R>
where
R: iced_core::text::Renderer,
{
fn value_bounds(&self, renderer: &R, layout: Layout<'_>) -> Rectangle {
let size = self.font_size.unwrap_or(renderer.default_size());
layout
.bounds()
.shrink(Padding::new(0.).top(size.0 * 1.5).right(10.))
}
fn scrollbar_bounds(&self, renderer: &R, layout: Layout<'_>) -> Rectangle {
let size = self.font_size.unwrap_or(renderer.default_size());
layout.bounds().shrink(
Padding::new(0.)
.top(size.0 * 1.5)
.left(layout.bounds().width - 10.),
)
}
fn row_height(&self, renderer: &R) -> f32 {
self.font_size.unwrap_or(renderer.default_size()).0 * LINE_HEIGHT
}
fn scroll_max(&self, renderer: &R, layout: Layout<'_>) -> f32 {
let size = self.font_size.unwrap_or(renderer.default_size());
let bounds = layout.bounds().shrink(Padding::new(0.).top(size.0 * 1.5));
let rows = self.val.len().div_ceil(0x10);
(self.row_height(renderer) * rows as f32 - bounds.height).max(0.)
}
fn scroll(
&self,
state: &mut HexEditorState,
renderer: &R,
layout: Layout<'_>,
delta: &ScrollDelta,
) {
// let size = self.font_size.unwrap_or(renderer.default_size());
// let bounds = layout.bounds().shrink(Padding::new(0.).top(size.0 * 1.5));
// let rows = self.val.len().div_ceil(0x10);
let max = self.scroll_max(renderer, layout);
// println!("max: {max}, rows: {rows}");
match delta {
ScrollDelta::Lines { y, .. } => {
state.offset_y += y * self.row_height(renderer);
}
ScrollDelta::Pixels { y, .. } => {
state.offset_y += y;
}
}
if state.offset_y > 0. {
state.offset_y = 0.;
} else if state.offset_y < -max {
state.offset_y = -max;
}
}
}
#[derive(Default)]
struct HexEditorState {
offset_y: f32,
selected: usize,
dragging: bool,
}
const LINE_HEIGHT: f32 = 1.3;
impl<B: Buffer, M, T, R> Widget<M, T, R> for HexEditor<B, M, R>
where
R: iced_core::text::Renderer,
{
fn tag(&self) -> Tag {
Tag::of::<HexEditorState>()
}
fn state(&self) -> State {
State::new(HexEditorState::default())
}
fn size(&self) -> Size<Length> {
Size::new(Length::Fill, Length::Fill)
}
fn layout(&mut self, _tree: &mut Tree, _renderer: &R, limits: &Limits) -> Node {
Node::new(limits.max())
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &T,
style: &Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
let state: &HexEditorState = tree.state.downcast_ref();
let size = self.font_size.unwrap_or(renderer.default_size());
let font = self.font.unwrap_or(renderer.default_font());
// let fonts = font_system();
// let mut font_sys = fonts.write().unwrap();
// let id = font_sys.raw().db().query(&Query {
// families: &[Family::Monospace],
// ..Query::default()
// });
// let f = font_sys.raw().get_font(id.unwrap(), iced::advanced::graphics::text::cosmic_text::Weight(1)).unwrap();
// let width = f.metrics();
// println!("Width: {width:?}");
// iced::advanced::graphics::text::cosmic_text::Buffer::new(
// font_sys.raw(),
// Metrics::new(size.0, size.0),
// ).layout_runs();
// TODO: this needs to be computed from the font
let col_width = 0.6 * size.0;
let rows = self.val.len().div_ceil(0x10);
let row_label_length = rows.ilog2().div_ceil(4) as usize;
let mut draw = |v: &str, pos: Point| {
renderer.fill_text(
Text {
content: format!("{}", v),
bounds: viewport.size(),
size,
line_height: size.into(),
font,
align_x: iced_core::text::Alignment::Left,
align_y: iced_core::alignment::Vertical::Top,
shaping: iced_core::text::Shaping::Basic,
wrapping: iced_core::text::Wrapping::None,
hint_factor: None,
},
pos,
Color::WHITE,
layout.bounds(),
);
};
draw("0", Point::new(0., 0.));
for i in 0..0x10 {
draw(
"0",
layout.position()
+ Vector::new((3 + row_label_length + 3 * i) as f32 * col_width, 0.),
);
draw(
"0",
layout.position()
+ Vector::new((4 + row_label_length + 3 * i) as f32 * col_width, 0.),
);
}
// renderer.fill_text(
// Text {
// content: format!(
// "{:width$} 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F {}",
// "",
// state.offset_y,
// width = row_label_length
// ),
// bounds: viewport.size(),
// size,
// line_height: size.into(),
// font,
// align_x: iced_core::text::Alignment::Left,
// align_y: iced_core::alignment::Vertical::Top,
// shaping: iced_core::text::Shaping::Basic,
// wrapping: iced_core::text::Wrapping::None,
// hint_factor: None,
// },
// layout.position(),
// Color::WHITE,
// layout.bounds(),
// );
struct HexV(Option<u8>);
impl Display for HexV {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Some(v) => write!(f, "{:02X}", v),
None => write!(f, "XX"),
}
}
}
// if rows > 0 {
// rows.ilog2()
// }
let bounds = self.value_bounds(renderer, layout);
let mut pos = bounds.position() + Vector::new(0., state.offset_y);
for row in 0..rows {
if bounds.contains(pos) || bounds.contains(pos + Vector::new(0., size.0 * LINE_HEIGHT))
{
renderer.fill_text(
Text {
content: format!(
"{:0width$X}0: {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}",
row,
HexV(self.val.peek(row * 0x10 + 0x0)),
HexV(self.val.peek(row * 0x10 + 0x1)),
HexV(self.val.peek(row * 0x10 + 0x2)),
HexV(self.val.peek(row * 0x10 + 0x3)),
HexV(self.val.peek(row * 0x10 + 0x4)),
HexV(self.val.peek(row * 0x10 + 0x5)),
HexV(self.val.peek(row * 0x10 + 0x6)),
HexV(self.val.peek(row * 0x10 + 0x7)),
HexV(self.val.peek(row * 0x10 + 0x8)),
HexV(self.val.peek(row * 0x10 + 0x9)),
HexV(self.val.peek(row * 0x10 + 0xA)),
HexV(self.val.peek(row * 0x10 + 0xB)),
HexV(self.val.peek(row * 0x10 + 0xC)),
HexV(self.val.peek(row * 0x10 + 0xD)),
HexV(self.val.peek(row * 0x10 + 0xE)),
HexV(self.val.peek(row * 0x10 + 0xF)),
width = row_label_length,
),
bounds: viewport.size(),
size,
line_height: size.into(),
font,
align_x: iced_core::text::Alignment::Left,
align_y: iced_core::alignment::Vertical::Top,
shaping: iced_core::text::Shaping::Basic,
wrapping: iced_core::text::Wrapping::None,
hint_factor: None,
},
pos,
Color::WHITE,
bounds,
);
}
pos += Vector::new(0., size.0 * LINE_HEIGHT);
}
let scrollbar = self.scrollbar_bounds(renderer, layout);
renderer.fill_quad(
Quad {
bounds: scrollbar,
..Quad::default()
},
Color::BLACK,
);
let pos = state.offset_y / self.scroll_max(renderer, layout);
renderer.fill_quad(
Quad {
bounds: Rectangle::new(
Point::new(scrollbar.x, scrollbar.y - pos * (scrollbar.height - 20.)),
Size::new(10., 20.),
),
..Quad::default()
},
Color::WHITE,
);
}
fn operate(
&mut self,
_tree: &mut Tree,
_layout: Layout<'_>,
_renderer: &R,
_operation: &mut dyn Operation,
) {
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &R,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, M>,
_viewport: &Rectangle,
) {
// if !matches!(event, Event::Window(_)) {
// println!("Event: {:#?}", event);
// }
match event {
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Named(Named::PageUp),
..
}) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
self.scroll(
state,
renderer,
layout,
&ScrollDelta::Pixels {
x: 0.,
y: self.value_bounds(renderer, layout).height,
},
);
shell.request_redraw();
shell.capture_event();
}
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Named(Named::PageDown),
..
}) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
self.scroll(
state,
renderer,
layout,
&ScrollDelta::Pixels {
x: 0.,
y: -self.value_bounds(renderer, layout).height,
},
);
shell.request_redraw();
shell.capture_event();
}
Event::Mouse(iced::mouse::Event::WheelScrolled { delta }) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
let bounds = self.value_bounds(renderer, layout);
if cursor.is_over(bounds) {
self.scroll(state, renderer, layout, delta);
shell.request_redraw();
shell.capture_event();
}
}
Event::Mouse(iced::mouse::Event::ButtonPressed(Button::Left)) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
let bounds = self.scrollbar_bounds(renderer, layout);
if let Some(pos) = cursor.position_in(bounds) {
state.offset_y = -(pos.y / bounds.height * self.scroll_max(renderer, layout));
state.dragging = true;
shell.request_redraw();
shell.capture_event();
}
}
Event::Mouse(iced::mouse::Event::ButtonReleased(Button::Left)) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
state.dragging = false;
}
// Event::Mouse(iced::mouse::Event::CursorLeft) => {
// let state: &mut HexEditorState = tree.state.downcast_mut();
// state.dragging = false;
// }
Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => {
let state: &mut HexEditorState = tree.state.downcast_mut();
if state.dragging {
let bounds = self.scrollbar_bounds(renderer, layout);
if let Some(pos) = cursor.position_in(bounds) {
state.offset_y =
-(pos.y / bounds.height * self.scroll_max(renderer, layout));
shell.request_redraw();
shell.capture_event();
}
}
}
_ => (),
}
}
fn mouse_interaction(
&self,
_tree: &Tree,
_layout: Layout<'_>,
_cursor: Cursor,
_viewport: &Rectangle,
_renderer: &R,
) -> Interaction {
Interaction::None
}
fn overlay<'a>(
&'a mut self,
_tree: &'a mut Tree,
_layout: Layout<'a>,
_renderer: &R,
_viewport: &Rectangle,
_translation: Vector,
) -> Option<overlay::Element<'a, M, T, R>> {
None
}
}

2245
src/lib.rs

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,51 @@
use std::{collections::HashMap, fmt, time::Duration};
use std::{
collections::HashMap,
fmt,
time::{Duration, Instant},
};
use iced::{
Color, Element, Font,
Element,
Length::{Fill, Shrink},
Point, Renderer, Size, Subscription, Task, Theme, mouse,
Point, Rectangle, Renderer, Size, Subscription, Task, Theme,
keyboard::{self, Key, Modifiers, key::Named},
mouse, time,
widget::{
Canvas, button,
self, Canvas, button,
canvas::{Frame, Program},
column, container, row, text,
column, container, image, row,
},
window::{self, Id, Settings},
};
use nes_emu::{
NES, PPU,
debugger::{DebuggerMessage, DebuggerState},
Break, NES,
audio::Audio,
debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image},
header_menu::header_menu,
hex_view::{HexEvent, HexView},
resize_watcher::resize_watcher,
};
use tokio::runtime::Runtime;
use tokio::{io::AsyncWriteExt, 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"), "/", "even_odd.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 = concat!(env!("ROM_DIR"), "/", "render-updating.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_palette_shared.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "input_test.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "scrolling.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "scrolling_colors.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_evelope.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 = "./Super Mario Bros. (World).nes";
// const ROM_FILE: &str = "./cpu_timing_test.nes";
// const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.nes";
extern crate nes_emu;
@@ -39,9 +62,17 @@ fn main() -> Result<(), iced::Error> {
.run()
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MemoryTy {
Cpu,
PPU,
OAM,
}
impl fmt::Display for MemoryTy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Export {self:?} Memory")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -52,6 +83,7 @@ enum WindowType {
TileViewer,
Palette,
Debugger,
Apu,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -61,6 +93,7 @@ enum HeaderButton {
// OpenTileViewer,
// OpenDebugger,
Open(WindowType),
OpenRom,
Reset,
PowerCycle,
}
@@ -69,36 +102,49 @@ 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::Memory(MemoryTy::OAM, _)) => write!(f, "Open OAM 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"),
Self::Open(WindowType::Palette) => write!(f, "Open Palette"),
Self::Open(WindowType::Main) => write!(f, "Create new Main window"),
Self::Open(WindowType::Apu) => write!(f, "Open APU info"),
Self::Reset => write!(f, "Reset"),
Self::PowerCycle => write!(f, "Power Cycle"),
Self::OpenRom => write!(f, "Open ROM file"),
}
}
}
struct Emulator {
running: bool,
nes: NES,
windows: HashMap<Id, WindowType>,
debugger: DebuggerState,
main_win_size: Size,
prev: [Instant; 2],
audio: Audio,
}
#[derive(Debug, Clone)]
enum Message {
Tick(usize),
Frame,
DMA,
CPU,
DebugInt,
OpenRom(NES),
// Tick(usize),
// Frame,
// DMA,
// CPU,
// DebugInt,
WindowClosed(Id),
WindowOpened(Id),
Key(keyboard::Event),
Periodic(Instant),
SetRunning(bool),
Header(HeaderButton),
Hex(Id, HexEvent),
Debugger(DebuggerMessage),
SetSize(window::Id, Size),
Export(MemoryTy),
}
impl Emulator {
@@ -109,24 +155,39 @@ impl Emulator {
min_size: None,
..Settings::default()
});
// let (win_2, task_2) = iced::window::open(Settings {
// min_size: None,
// ..Settings::default()
// });
(
Self {
nes,
windows: HashMap::from_iter([(win, WindowType::Main)]),
windows: HashMap::from_iter([
(win, WindowType::Main),
// (win_2, WindowType::Debugger),
]),
debugger: DebuggerState::new(),
main_win_size: Size::new(0., 0.),
running: false,
prev: [Instant::now(); 2],
audio: Audio::init(),
},
task.discard(),
Task::batch([
task,
// task_2
])
.discard(), // task.discard(),
)
}
fn title(&self, win: Id) -> String {
match self.windows.get(&win) {
Some(WindowType::Main) => "NES emu".into(),
Some(WindowType::Memory(_, _)) => "NES MemoryView".into(),
Some(WindowType::Memory(ty, _)) => format!("NES {ty:?} Memory"),
Some(WindowType::TileMap) => "NES TileMap".into(),
Some(WindowType::TileViewer) => "NES Tile Viewer".into(),
Some(WindowType::Palette) => "NES Palette Viewer".into(),
Some(WindowType::Debugger) => "NES Debugger".into(),
Some(WindowType::Apu) => "NES APU Debugger".into(),
None => todo!(),
}
}
@@ -139,23 +200,45 @@ impl Emulator {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Tick(count) => {
for _ in 0..count {
self.nes.run_one_clock_cycle();
}
}
Message::Frame => while !self.nes.run_one_clock_cycle().ppu_frame {},
Message::DMA => while !self.nes.run_one_clock_cycle().dma {},
Message::CPU => while !self.nes.run_one_clock_cycle().cpu_exec {},
Message::DebugInt => while !self.nes.run_one_clock_cycle().dbg_int {},
// Message::Tick(count) => {
// for _ in 0..count {
// self.nes.run_one_clock_cycle();
// }
// }
// Message::Frame => while !self.nes.run_one_clock_cycle().ppu_frame {},
// Message::DMA => while !self.nes.run_one_clock_cycle().dma {},
// Message::CPU => while !self.nes.run_one_clock_cycle().cpu_exec {},
// Message::DebugInt => while !self.nes.run_one_clock_cycle().dbg_int {},
Message::WindowClosed(id) => {
if let Some(WindowType::Main) = self.windows.remove(&id) {
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);
}
Message::Header(HeaderButton::OpenRom) => {
return Task::future(
rfd::AsyncFileDialog::new()
.set_directory(".")
.add_filter("NES", &["nes"])
.set_title("Open NES Rom file")
.pick_file(),
)
.and_then(|p| {
Task::future(async move {
// println!("Opening: {}", p.path().display());
NES::async_load_nes_file(p.path()).await.ok()
})
})
.and_then(|n| Task::done(Message::OpenRom(n)));
}
Message::Hex(id, val) => {
if let Some(WindowType::Memory(_, view)) = self.windows.get_mut(&id) {
return view.update(val).map(move |e| Message::Hex(id, e));
@@ -174,16 +257,138 @@ impl Emulator {
if let Some(WindowType::Main) = self.windows.get(&id) {
self.main_win_size = size;
}
println!("New size for {:?}, {:?}", id, size);
// return iced::window::set_min_size(id, size.into())
// .then(move |_: ()| iced::window::resize(id, size));
return Task::future(async {
tokio::time::sleep(Duration::from_millis(50)).await;
})
.then(move |_| {
iced::window::resize(id, size)
});
.then(move |_| iced::window::resize(id, size));
}
Message::OpenRom(nes) => {
self.nes = nes;
self.nes.power_cycle();
}
Message::Export(ty) => {
let raw: Vec<_> = match ty {
MemoryTy::Cpu => (0..=0xFFFF)
.map(|i| self.nes.mem().peek_cpu(i).unwrap_or(0))
.collect(),
MemoryTy::PPU => (0..=0xFFFF)
.map(|i| self.nes.mem().peek_ppu(i).unwrap_or(0))
.collect(),
MemoryTy::OAM => (0..=0xFF).map(|i| self.nes.ppu().peek_oam(i)).collect(),
};
return Task::future(async move {
if let Some(file) = rfd::AsyncFileDialog::new()
.set_directory(".")
.set_file_name("output.dmp")
.set_title("Save memory dump")
.save_file()
.await
{
tokio::fs::File::create(file.path())
.await
.expect("Failed to save file")
.write_all(&raw)
.await
.expect("Failed to write dump");
}
})
.discard();
}
Message::Key(key) => match key {
keyboard::Event::KeyPressed {
key: Key::Character(val),
modifiers: Modifiers::CTRL,
repeat: false,
..
} => {
if val == "t" {
self.nes.reset();
}
}
keyboard::Event::KeyPressed {
key,
modifiers: Modifiers::NONE,
repeat: false,
..
} => {
if key == Key::Character("z".into()) {
self.nes.controller_1().set_a(true);
} else if key == Key::Character("x".into()) {
self.nes.controller_1().set_b(true);
} else if key == Key::Character("a".into()) {
self.nes.controller_1().set_select(true);
} else if key == Key::Character("s".into()) {
self.nes.controller_1().set_start(true);
} else if key == Key::Named(Named::ArrowDown) {
self.nes.controller_1().set_down(true);
} else if key == Key::Named(Named::ArrowUp) {
self.nes.controller_1().set_up(true);
} else if key == Key::Named(Named::ArrowLeft) {
self.nes.controller_1().set_left(true);
} else if key == Key::Named(Named::ArrowRight) {
self.nes.controller_1().set_right(true);
} else if key == Key::Named(Named::Play) || key == Key::Named(Named::MediaPlay)
{
self.running = true;
} else if key == Key::Named(Named::Pause)
|| key == Key::Named(Named::MediaPause)
{
self.running = false;
} else if key == Key::Named(Named::MediaPlayPause)
|| key == Key::Named(Named::Space)
{
self.running = !self.running;
}
}
keyboard::Event::KeyReleased {
key,
modifiers: Modifiers::NONE,
..
} => {
if key == Key::Character("z".into()) {
self.nes.controller_1().set_a(false);
} else if key == Key::Character("x".into()) {
self.nes.controller_1().set_b(false);
} else if key == Key::Character("a".into()) {
self.nes.controller_1().set_select(false);
} else if key == Key::Character("s".into()) {
self.nes.controller_1().set_start(false);
} else if key == Key::Named(Named::ArrowDown) {
self.nes.controller_1().set_down(false);
} else if key == Key::Named(Named::ArrowUp) {
self.nes.controller_1().set_up(false);
} else if key == Key::Named(Named::ArrowLeft) {
self.nes.controller_1().set_left(false);
} else if key == Key::Named(Named::ArrowRight) {
self.nes.controller_1().set_right(false);
}
}
_ => (),
},
Message::Periodic(_i) => {
if self.running {
// TODO: Smarter frame skip
if self.prev[0].elapsed() >= Duration::from_millis(2) {
self.nes.run_one_clock_cycle(&Break::default());
let mut count = 0;
while !self.nes.run_one_clock_cycle(&Break::default()).ppu_frame
&& !self.nes.halted()
{
count += 1;
if count > 100000 {
println!("Loop overran...");
break;
}
}
self.prev[0] = Instant::now();
self.audio.submit(self.nes.apu().get_frame_samples());
self.nes.apu_mut().reset_frame_samples();
}
} else {
self.audio.pause();
}
}
Message::SetRunning(running) => self.running = running,
}
// self.image.0.clone_from(self.nes.image());
Task::none()
@@ -192,12 +397,14 @@ 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),
keyboard::listen().map(Message::Key),
time::every(Duration::from_secs_f64(1./60.)).map(Message::Periodic),
])
}
fn view(&self, win: Id) -> Element<'_, Message> {
// println!("Running view");
match self.windows.get(&win) {
Some(WindowType::Main) => {
let content = column![
@@ -216,65 +423,58 @@ impl Emulator {
Some(WindowType::Memory(ty, view)) => {
let hex = match ty {
MemoryTy::Cpu => view
.render(self.nes.cpu_mem())
.render_cpu(self.nes.mem())
.map(move |e| Message::Hex(win, e)),
MemoryTy::PPU => view
.render_ppu(self.nes.mem(), self.nes.ppu())
.map(move |e| Message::Hex(win, e)),
MemoryTy::OAM => view
.render_oam(self.nes.ppu())
.map(move |e| Message::Hex(win, e)),
};
let content = column![hex].width(Fill).height(Fill);
let content = column![row![header_menu("Export", [*ty], Message::Export)], 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()
dbg_image(DbgImage::NameTable(self.nes.mem(), 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.mem(), self.nes.ppu())).into()
}
Some(WindowType::Palette) => container(Canvas::new(DbgImage::Palette(self.nes.ppu())))
.width(Fill)
.height(Fill)
.into(),
Some(WindowType::Debugger) => {
container(self.debugger.view(&self.nes).map(Message::Debugger))
.width(Fill)
.height(Fill)
.into()
Some(WindowType::Palette) => {
dbg_image(DbgImage::Palette(self.nes.mem(), self.nes.ppu())).into()
}
Some(WindowType::Debugger) => column![
row![
button(image("./images/ic_fluent_play_24_filled.png"))
.on_press(Message::SetRunning(true)),
button(image("./images/ic_fluent_pause_24_filled.png"))
.on_press(Message::SetRunning(false)),
],
self.debugger.view(&self.nes).map(Message::Debugger)
]
.width(Fill)
.height(Fill)
.into(),
Some(WindowType::Apu) => {
self.nes.apu().view()
}
None => panic!("Window not found"),
// _ => todo!(),
}
}
// fn cpu_state(&self) -> Element<'_, Message> {
// row![column![
// // text!("Registers").font(Font::MONOSPACE),
// text!("{:?}", self.nes).font(Font::MONOSPACE),
// ],]
// .width(Fill)
// .into()
// }
// fn controls(&self) -> Element<'_, Message> {
// row![
// button("Clock tick").on_press(Message::Tick(1)),
// button("CPU tick").on_press(Message::CPU),
// button("Next Frame").on_press(Message::Frame),
// button("Next DMA").on_press(Message::DMA),
// button("Next DBG").on_press(Message::DebugInt),
// ]
// .width(Fill)
// .into()
// }
fn dropdowns(&self) -> Element<'_, Message> {
row![
header_menu(
"Console",
[HeaderButton::Reset, HeaderButton::PowerCycle,],
[
HeaderButton::OpenRom,
HeaderButton::Reset,
HeaderButton::PowerCycle,
],
Message::Header
),
header_menu(
@@ -282,9 +482,12 @@ impl Emulator {
[
HeaderButton::Open(WindowType::Debugger),
HeaderButton::Open(WindowType::Memory(MemoryTy::Cpu, HexView {})),
HeaderButton::Open(WindowType::Memory(MemoryTy::PPU, HexView {})),
HeaderButton::Open(WindowType::Memory(MemoryTy::OAM, HexView {})),
HeaderButton::Open(WindowType::TileMap),
HeaderButton::Open(WindowType::TileViewer),
HeaderButton::Open(WindowType::Palette),
HeaderButton::Open(WindowType::Apu),
],
Message::Header
)
@@ -305,55 +508,18 @@ impl Program<Message> for Emulator {
bounds: iced::Rectangle,
_cursor: mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
// const SIZE: f32 = 2.;
let mut frame = Frame::new(
renderer,
bounds.size(), // iced::Size {
// width: 256. * 2.,
// height: 240. * 2.,
// },
);
let mut frame = Frame::new(renderer, bounds.size());
frame.scale(2.);
for y in 0..240 {
for x in 0..256 {
let c = self.nes.image().read(y, x);
frame.fill_rectangle(
Point::new(x as f32, y as f32),
Size::new(1., 1.),
Color::from_rgb8(c.r, c.g, c.b),
);
}
}
frame.draw_image(
Rectangle::new(Point::new(0., 0.), Size::new(256., 240.)),
widget::canvas::Image::new(widget::image::Handle::from_rgba(
256,
240,
self.nes.image().image(),
))
.filter_method(image::FilterMethod::Nearest)
.snap(true),
);
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,12 +1,18 @@
use crate::hex_view::Memory;
use std::{fmt, sync::Arc};
pub enum Value<'a, R> {
use crate::{
CPUMMRegisters, PPU, apu::APU, controllers::Controllers, cpu::DmaState, hex_view::Memory,
ppu::PPUMMRegisters,
};
#[derive(Debug, Clone)]
pub enum Value<R> {
Value(u8),
Register { reg: &'a R, offset: u16 },
Register { reg: R, offset: u16 },
}
impl<R> Value<'_, R> {
pub fn reg_map(self, f: impl FnOnce(&R, u16) -> u8) -> u8 {
impl<R> Value<R> {
pub fn reg_map(self, f: impl FnOnce(R, u16) -> u8) -> u8 {
match self {
Value::Value(v) => v,
Value::Register { reg, offset } => f(reg, offset),
@@ -14,23 +20,29 @@ impl<R> Value<'_, R> {
}
}
#[derive(Debug, Clone)]
pub enum Data<R> {
RAM(Vec<u8>),
ROM(Vec<u8>),
Mirror(usize),
ROM(Arc<[u8]>),
Mirror(u16),
Reg(R),
// Disabled(),
Disabled,
}
#[derive(Debug, Clone)]
pub struct Segment<R> {
name: &'static str,
name: SegmentId,
position: u16,
size: u16,
mem: Data<R>,
}
impl<R> Segment<R> {
pub fn ram(name: &'static str, position: u16, size: u16) -> Self {
fn is(&self, id: SegmentId) -> bool {
self.name == id
}
fn ram(name: SegmentId, position: u16, size: u16) -> Self {
Self {
name,
position,
@@ -38,15 +50,19 @@ impl<R> Segment<R> {
mem: Data::RAM(vec![0u8; size as usize]),
}
}
pub fn rom(name: &'static str, position: u16, raw: &[u8]) -> Self {
fn rom<Raw>(name: SegmentId, position: u16, raw: Raw) -> Self
where
Arc<[u8]>: From<Raw>,
{
let raw = Arc::from(raw);
Self {
name,
position,
size: raw.len() as u16,
mem: Data::ROM(Vec::from(raw)),
mem: Data::ROM(raw),
}
}
pub fn reg(name: &'static str, position: u16, size: u16, reg: R) -> Self {
fn reg(name: SegmentId, position: u16, size: u16, reg: R) -> Self {
Self {
name,
position,
@@ -54,7 +70,7 @@ impl<R> Segment<R> {
mem: Data::Reg(reg),
}
}
pub fn mirror(name: &'static str, position: u16, size: u16, of: usize) -> Self {
fn mirror(name: SegmentId, position: u16, size: u16, of: u16) -> Self {
Self {
name,
position,
@@ -62,18 +78,46 @@ impl<R> Segment<R> {
mem: Data::Mirror(of),
}
}
// fn take_rom(&mut self) -> Vec<u8> {
// match std::mem::replace(&mut self.mem, Data::Disabled) {
// Data::ROM(items) => items,
// _ => panic!("Cannot take rom since memory is not rom"),
// }
// }
// fn set_rom(&mut self, rom: Vec<u8>) {
// assert!(
// matches!(self.mem, Data::Disabled),
// "Cannot set non-disabled memory to rom"
// );
// self.mem = Data::ROM(rom);
// }
fn swap_rom(&mut self, rom: Arc<[u8]>) {
match &mut self.mem {
Data::ROM(items) => *items = rom,
_ => panic!("Cannot swap rom since memory is not rom"),
}
}
pub fn power_cycle(&mut self) {
match &mut self.mem {
Data::RAM(v) => v.fill(0),
_ => (),
}
}
}
#[derive(Debug, Clone)]
pub struct MemoryMap<R> {
edit_ver: usize,
segments: Vec<Segment<R>>,
// map: Remapper,
}
impl<R> MemoryMap<R> {
pub fn new(segments: Vec<Segment<R>>) -> Self {
Self { edit_ver: 0, segments }
}
pub fn read(&self, addr: u16) -> Value<'_, R> {
impl<R: Copy> MemoryMap<R> {
pub fn read(&self, addr: u16) -> Value<R> {
// self.edit_ver += 1;
for segment in &self.segments {
if segment.position <= addr && addr - segment.position < segment.size {
@@ -81,42 +125,50 @@ impl<R> MemoryMap<R> {
Data::RAM(items) => Value::Value(items[(addr - segment.position) as usize]),
Data::ROM(items) => Value::Value(items[(addr - segment.position) as usize]),
Data::Reg(reg) => Value::Register {
reg,
reg: *reg,
offset: addr - segment.position,
},
Data::Mirror(index) => {
Data::Mirror(pos) => {
let offset = addr - segment.position;
let s = &self.segments[*index];
self.read(s.position + offset % s.size)
self.read(pos + offset)
// 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)) {
pub fn write(&mut self, addr: u16, val: u8) -> Option<(R, u16, u8)> {
self.edit_ver += 1;
for segment in &mut self.segments {
if segment.position <= addr && addr - segment.position < segment.size {
return match &mut segment.mem {
Data::RAM(items) => {
items[(addr - segment.position) as usize] = val;
None
}
Data::ROM(_items) => (),
Data::Reg(reg) => reg_fn(reg, addr - segment.position, val),
Data::Mirror(index) => {
Data::ROM(_items) => None,
Data::Reg(reg) => Some((*reg, addr - segment.position, val)),
Data::Mirror(pos) => {
let pos = *pos;
let offset = addr - segment.position;
let index = *index;
let s = &self.segments[index];
self.write(s.position + offset % s.size, val, reg_fn)
self.write(pos + offset, val)
// let index = *index;
// let s = &self.segments[index];
// self.write(s.position + offset % s.size, val)
}
// Data::Disabled() => todo!(),
Data::Disabled => todo!(),
};
}
}
todo!("Open bus")
// iirc, open bus just drops writes
None
// todo!("Open bus (${addr:04X})")
}
pub fn clear(&mut self) {
@@ -128,36 +180,525 @@ 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) {
Some(val)
} else {
None
pub fn power_cycle(&mut self) -> Self {
for seg in &mut self.segments {
seg.power_cycle();
}
let segments = std::mem::take(&mut self.segments);
Self {
edit_ver: self.edit_ver + 1,
segments,
// map: self.map.power_cycle(),
}
}
}
impl<R> Memory for MemoryMap<R> {
fn peek(&self, addr: u16) -> Option<u8> {
for segment in &self.segments {
if segment.position <= addr && addr - segment.position < segment.size {
return match &segment.mem {
Data::RAM(items) => Some(items[(addr - segment.position) as usize]),
Data::ROM(items) => Some(items[(addr - segment.position) as usize]),
Data::Reg(_) => None,
Data::Mirror(index) => {
let offset = addr - segment.position;
let s = &self.segments[*index];
self.peek(s.position + offset % s.size)
}
// Data::Disabled() => todo!(),
};
fn find(&mut self, id: SegmentId) -> Option<&mut Segment<R>> {
for s in &mut self.segments {
if s.is(id) {
return Some(s);
}
}
None
}
fn peek_val(&self, addr: u16) -> Result<u8, Option<(R, u16)>> {
for segment in &self.segments {
if segment.position <= addr && addr - segment.position < segment.size {
return match &segment.mem {
Data::RAM(items) => Ok(items[(addr - segment.position) as usize]),
Data::ROM(items) => Ok(items[(addr - segment.position) as usize]),
Data::Reg(r) => Err(Some((*r, addr - segment.position))),
Data::Mirror(pos) => {
let offset = addr - segment.position;
self.peek_val(pos + offset)
}
Data::Disabled => Err(None),
};
}
}
Err(None)
}
}
impl<R: Copy> Memory for MemoryMap<R> {
fn peek(&self, addr: usize) -> Option<u8> {
self.peek_val(addr as u16).ok()
}
fn len(&self) -> usize {
self.segments
.iter()
.map(|s| s.position as usize + s.size as usize)
.max()
.unwrap_or(0)
}
fn edit_ver(&self) -> usize {
self.edit_ver
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct Mapper {
horizontal_name_table: bool,
mapper: u16,
sub_mapper: u8,
prg_rom_size: u8,
chr_rom_size: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum SegmentId {
#[allow(unused)]
TestRam,
InternalRam,
InternalRamMirror,
PpuRegisters,
ApuIoRegisters,
PrgRom,
Nametable0,
Nametable1,
Nametable2,
Nametable3,
VramMirror,
PaletteControl,
PaletteMirror,
PpuRom,
PpuRam,
// CpuBank(u32),
PrgBank0,
PrgBank1,
ChrBank0,
ChrBank1,
}
impl fmt::Display for SegmentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Debug, Clone)]
pub struct Mapped {
cpu: MemoryMap<CPUMMRegisters>,
ppu: MemoryMap<PPUMMRegisters>,
mapper: Remapper,
}
impl Mapped {
pub fn from_rom(rom: &[u8]) -> Self {
let (header, rom) = rom.split_at(0x10);
let nes_20 = header[7] & 0x0C == 0x08;
// assert!(nes_20, "Only supports nes 2.0 format");
// if header[6] & 0b11111110 != 0 {
// todo!("Support other mapper flags, {:08b}", header[6]);
// }
let mapper = if nes_20 {
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[11], 0, "No support for CHR-RAM");
assert!(header[12] == 0 || header[12] == 2, "Only support NTSC NES");
Mapper {
horizontal_name_table: header[6] & (1 << 0) == 1,
mapper: ((header[6] as u16 & 0xF0) >> 4)
| ((header[7] as u16 & 0xF0) >> 0)
| ((header[8] as u16 & 0x0F) << 8),
sub_mapper: (header[8] & 0xF0) >> 4,
prg_rom_size: header[4],
chr_rom_size: header[5],
}
} else {
Mapper {
horizontal_name_table: header[6] & (1 << 0) == 1,
mapper: ((header[6] as u16 & 0xF0) >> 4) | ((header[7] as u16 & 0xF0) >> 0),
sub_mapper: 0,
prg_rom_size: header[4],
chr_rom_size: header[5],
}
};
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);
// assert_eq!(rom.len(), 0);
// let prg_rom = &file[self.cpu_offset()..self.ppu_offset()];
let mut cpu_segments = vec![
Segment::ram(SegmentId::InternalRam, 0x0000, 0x0800),
Segment::mirror(SegmentId::InternalRamMirror, 0x0800, 0x1800, 0x0000),
Segment::reg(SegmentId::PpuRegisters, 0x2000, 0x0008, CPUMMRegisters::PPU),
Segment::reg(
SegmentId::ApuIoRegisters,
0x4000,
0x0018,
CPUMMRegisters::APU,
),
];
let mut ppu_segments = vec![
Segment::mirror(SegmentId::VramMirror, 0x3000, 0x0F00, 0x0000),
Segment::reg(
SegmentId::PaletteControl,
0x3F00,
0x0020,
PPUMMRegisters::Palette,
),
Segment::mirror(SegmentId::PaletteMirror, 0x3F20, 0x00E0, 0x3F00),
];
if mapper.horizontal_name_table {
ppu_segments.push(Segment::ram(SegmentId::Nametable0, 0x2000, 0x400));
ppu_segments.push(Segment::ram(SegmentId::Nametable1, 0x2400, 0x400));
ppu_segments.push(Segment::mirror(
SegmentId::Nametable2,
0x2800,
0x400,
0x2000,
));
ppu_segments.push(Segment::mirror(
SegmentId::Nametable3,
0x2C00,
0x400,
0x2400,
));
} else {
ppu_segments.push(Segment::ram(SegmentId::Nametable0, 0x2000, 0x400));
ppu_segments.push(Segment::mirror(
SegmentId::Nametable1,
0x2400,
0x400,
0x2000,
));
ppu_segments.push(Segment::ram(SegmentId::Nametable2, 0x2800, 0x400));
ppu_segments.push(Segment::mirror(
SegmentId::Nametable3,
0x2C00,
0x400,
0x2800,
));
}
let remap = if mapper.mapper == 0 {
cpu_segments.push(Segment::rom(
SegmentId::PrgRom,
0x8000 + (0x8000 - prg_rom.len() as u16),
prg_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
} else if mapper.mapper == 1 && mapper.sub_mapper == 0 {
let prg_banks: Vec<Arc<[u8]>> =
prg_rom.chunks(0x4000).map(|ch| Arc::from(ch)).collect();
for (i, b) in prg_banks.iter().enumerate() {
println!("{i}: {:X?}", &b[0x3FF0..]);
}
cpu_segments.push(Segment::rom(
SegmentId::PrgBank0,
0x8000,
prg_banks.first().unwrap().clone(),
));
cpu_segments.push(Segment::rom(
SegmentId::PrgBank1,
0xC000,
prg_banks.last().unwrap().clone(),
));
println!("CHR_ROM: {}", chr_rom.len());
let chr_banks: Vec<Arc<[u8]>> =
chr_rom.chunks(0x1000).map(|ch| Arc::from(ch)).collect();
if chr_rom.len() == 0 {
ppu_segments.push(Segment::ram(SegmentId::PpuRam, 0, 0x2000));
} else {
ppu_segments.push(Segment::rom(
SegmentId::ChrBank0,
0x0000,
chr_banks[0].clone(),
));
ppu_segments.push(Segment::rom(
SegmentId::ChrBank1,
0x1000,
chr_banks[1].clone(),
));
}
Remapper::MMC1 {
shift_reg: 0,
count: 0,
prg_banks,
chr_banks,
mode: MMC1Mode::LastFixed,
cur_prg_bank: 0,
}
} else {
todo!()
};
Self {
cpu: MemoryMap {
edit_ver: 0,
segments: cpu_segments,
},
ppu: MemoryMap {
edit_ver: 0,
segments: ppu_segments,
},
mapper: remap,
}
}
pub fn power_cylce(&mut self) -> Self {
// TODO: mapper needs to reset cpu and ppu mem maps
let mut cpu = self.cpu.power_cycle();
let mut ppu = self.ppu.power_cycle();
Self {
mapper: self.mapper.power_cycle(&mut cpu, &mut ppu),
cpu,
ppu,
}
}
pub fn peek_ppu(&self, addr: u16) -> Result<u8, Option<(PPUMMRegisters, u16)>> {
self.ppu.peek_val(addr)
}
pub fn ppu_edit_ver(&self) -> usize {
self.ppu.edit_ver
}
pub fn peek_cpu(&self, addr: u16) -> Option<u8> {
self.cpu.peek_val(addr).ok()
}
pub fn cpu_edit_ver(&self) -> usize {
self.cpu.edit_ver
}
#[cfg(test)]
pub fn test_ram() -> Self {
Self {
cpu: MemoryMap {
edit_ver: 0,
segments: vec![Segment::ram(SegmentId::TestRam, 0, 0x8000)],
},
ppu: MemoryMap {
edit_ver: 0,
segments: vec![Segment::ram(SegmentId::TestRam, 0, 0x8000)],
},
mapper: Remapper::None,
}
}
}
pub struct CpuMem<'a> {
mem: &'a mut Mapped,
ppu: &'a mut PPU,
apu: &'a mut APU,
dma: &'a mut DmaState,
controllers: &'a mut Controllers,
}
impl<'a> CpuMem<'a> {
pub fn new(
mem: &'a mut Mapped,
ppu: &'a mut PPU,
apu: &'a mut APU,
dma: &'a mut DmaState,
controllers: &'a mut Controllers,
) -> Self {
Self {
mem,
ppu,
apu,
dma,
controllers,
}
}
pub fn read(&mut self, addr: u16) -> u8 {
match self.mem.mapper {
_ => (),
}
match self.mem.cpu.read(addr) {
Value::Value(v) => v,
Value::Register { reg, offset } => match reg {
CPUMMRegisters::PPU => self.ppu.read_reg(&mut PpuMem::new(self.mem), offset),
CPUMMRegisters::APU => {
if offset == 0x014 {
todo!("OAM DMA")
} else if offset == 0x16 {
self.controllers.read_joy1()
} else if offset == 0x17 {
self.controllers.read_joy2()
} else {
self.apu.read_reg(offset)
}
}
},
}
}
pub fn write(&mut self, addr: u16, val: u8) {
match &mut self.mem.mapper {
Remapper::MMC1 {
shift_reg,
count,
mode,
prg_banks,
chr_banks,
cur_prg_bank,
} if addr & 0x8000 != 0 => {
if val & 0x80 != 0 {
*shift_reg = 0;
*count = 0;
} else if *count == 4 {
let val = (*shift_reg << 1) | (val & 0x01);
if (addr & 0x6000) >> 13 == 0 {
// TODO: fix mem layout if it's changed
if val & 0b01100 == 0b01000 {
*mode = MMC1Mode::FirstFixed;
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
bank0.swap_rom(prg_banks[0].clone());
let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap();
bank1.swap_rom(prg_banks[*cur_prg_bank].clone());
} else if val & 0b01100 == 0b01100 {
*mode = MMC1Mode::LastFixed;
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
bank0.swap_rom(prg_banks[*cur_prg_bank].clone());
let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap();
bank1.swap_rom(prg_banks.last().unwrap().clone());
} else {
*mode = MMC1Mode::Full;
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
bank0.swap_rom(prg_banks[*cur_prg_bank & !1].clone());
let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap();
bank1.swap_rom(prg_banks[(*cur_prg_bank & !1) + 1].clone());
}
if val & 0b10000 == 0b10000 {
// TODO: CHR-ROM mode
}
// TODO: Set name table mirroring
if val & 0b00011 == 0b00000 {
} else if val & 0b00011 == 0b00001 {
} else if val & 0b00011 == 0b00010 {
} else {
}
} else if (addr & 0x6000) >> 13 == 1 {
if chr_banks.len() != 0 {
todo!("Swap CHR bank 0")
}
} else if (addr & 0x6000) >> 13 == 2 {
if chr_banks.len() != 0 {
todo!("Swap CHR bank 1")
}
} else if (addr & 0x6000) >> 13 == 3 {
*cur_prg_bank = (val & 0x0F) as usize;
match mode {
MMC1Mode::Full => {
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
bank0.swap_rom(prg_banks[*cur_prg_bank & !1].clone());
let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap();
bank1.swap_rom(prg_banks[(*cur_prg_bank & !1) + 1].clone());
}
MMC1Mode::FirstFixed => {
let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap();
bank1.swap_rom(prg_banks[(val & 0x0F) as usize].clone());
}
MMC1Mode::LastFixed => {
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
bank0.swap_rom(prg_banks[(val & 0x0F) as usize].clone());
}
}
// TODO: handle MSB, changes some stuff...
} else {
todo!("Handle reg write {} => {}", val, (addr & 0x6000) >> 13,)
}
*shift_reg = 0;
*count = 0;
} else {
*shift_reg = (*shift_reg << 1) | (val & 0x01);
*count += 1;
}
}
_ => (),
}
match self.mem.cpu.write(addr, val) {
Some((CPUMMRegisters::PPU, offset, val)) => {
self.ppu.write_reg(&mut PpuMem::new(self.mem), offset, val)
}
Some((CPUMMRegisters::APU, offset, val)) => {
if offset == 0x014 {
*self.dma = DmaState::Idle((val as u16) << 8);
} else if offset == 0x16 {
self.controllers.write_joy_strobe(val);
} else {
self.apu.write_reg(offset, val);
}
}
_ => (),
}
}
}
pub struct PpuMem<'a>(&'a mut Mapped);
impl<'a> PpuMem<'a> {
pub fn new(mem: &'a mut Mapped) -> Self {
Self(mem)
}
pub fn read(&mut self, addr: u16) -> Value<PPUMMRegisters> {
self.0.ppu.read(addr)
}
pub fn write(&mut self, addr: u16, val: u8, reg_fn: impl FnOnce(&PPUMMRegisters, u16, u8)) {
match self.0.ppu.write(addr, val) {
Some((r, o, v)) => reg_fn(&r, o, v),
None => (),
}
}
}
#[derive(Debug, Clone)]
enum MMC1Mode {
Full,
FirstFixed,
LastFixed,
}
#[derive(Debug, Clone)]
enum Remapper {
None,
MMC1 {
shift_reg: u8,
count: u8,
prg_banks: Vec<Arc<[u8]>>,
chr_banks: Vec<Arc<[u8]>>,
mode: MMC1Mode,
cur_prg_bank: usize,
},
}
impl Remapper {
fn power_cycle(
&mut self,
cpu: &mut MemoryMap<CPUMMRegisters>,
_ppu: &mut MemoryMap<PPUMMRegisters>,
) -> Self {
match self {
Remapper::None => Remapper::None,
Remapper::MMC1 {
prg_banks,
chr_banks,
..
} => {
let prg_banks = std::mem::take(prg_banks);
let chr_banks = std::mem::take(chr_banks);
cpu.find(SegmentId::PrgBank0)
.unwrap()
.swap_rom(prg_banks[0].clone());
cpu.find(SegmentId::PrgBank1)
.unwrap()
.swap_rom(prg_banks.last().unwrap().clone());
Remapper::MMC1 {
shift_reg: 0,
count: 0,
prg_banks,
chr_banks,
mode: MMC1Mode::LastFixed,
cur_prg_bank: 0,
}
}
}
}
}

1259
src/ppu.rs

File diff suppressed because it is too large Load Diff

36
src/test_roms/alu_bit.s Normal file
View File

@@ -0,0 +1,36 @@
; Verifies that reset doesn't alter any RAM.
.include "testing.s"
zp_res VAL
RAM = $300
reset:
sei
cld
ldx #$FF
txs
lda #$F0
sta VAL
lda #$0F
bit VAL
php
pla
sta RAM+0
lda #$F0
sta $400
lda #$0F
bit $400
php
pla
sta RAM+1
stp
nmi:
stp
irq:
stp

View File

@@ -0,0 +1,97 @@
.include "testing.s"
.include "audio_inp.s"
zp_res TIMER_LOW
; zp_res TIMER_LOW
; zp_res TIMER_LOW
; zp_res TIMER_LOW
reset:
sei
cld
ldx #$FF
txs
lda #$01
sta SNDCHN
lda #$AF ; Duty 2, LC halted, evelope enabled, volume = F
sta PULSE_CH1_DLCV
lda #$7F ;
sta PULSE_CH1_SWEEP
lda #$6F
sta PULSE_CH1_TLOW
sta TIMER_LOW
lda #$F0
sta PULSE_CH1_LCTH
; PULSE_CH1_DLCV = $4000
; PULSE_CH1_SWEEP = $4001
; PULSE_CH1_TLOW = $4002
; PULSE_CH1_LCTH = $4003
jsr init_view
load_ppu_addr $2041
write_char_x 't'
write_char_x 'i'
write_char_x 'm'
write_char_x 'e'
write_char_x 'r'
write_char_x ' '
write_char_x 'l'
write_char_x 'o'
write_char_x 'w'
write_char_x ':'
load_ppu_addr $2061
; write_char_x 't'
; write_char_x 'i'
; write_char_x 'm'
; write_char_x 'e'
; write_char_x 'r'
; write_char_x ' '
; write_char_x 'h'
; write_char_x 'o'
; write_char_x 'w'
; write_char_x ':'
load_ppu_addr $204C
lda TIMER_LOW
jsr write_hex_a
jsr enable_rendering
loop:
jmp loop
; Joystick is in A and JOYTEMP
zp_res JOY_PREV
audio_nmi:
EOR JOY_PREV
STA JOY_PREV
lda JOYTEMP
and #JOY_UP_MASK
and JOY_PREV
beq next_1
load_ppu_addr $204C
lda #$01
adc TIMER_LOW
sta TIMER_LOW
sta PULSE_CH1_TLOW
jsr write_hex_a
next_1:
lda JOYTEMP
and #JOY_DOWN_MASK
and JOY_PREV
beq next_2
load_ppu_addr $204C
lda #$FF
adc TIMER_LOW
sta TIMER_LOW
sta PULSE_CH1_TLOW
jsr write_hex_a
next_2:
lda JOYTEMP
STA JOY_PREV
rts
irq:
rti

View File

@@ -0,0 +1,104 @@
.include "testing.s"
.include "minimal_ppu.s"
reset:
sei
cld
ldx #$FF
txs
lda #$01
sta SNDCHN
lda #$BF ; Duty 2, LC halted, Constant volume, volume = F
sta PULSE_CH1_DLCV
lda #$7F ;
sta PULSE_CH1_SWEEP
lda #$6F
sta PULSE_CH1_TLOW
sta TIMER_LOW
lda #$00
sta PULSE_CH1_LCTH
; PULSE_CH1_DLCV = $4000
; PULSE_CH1_SWEEP = $4001
; PULSE_CH1_TLOW = $4002
; PULSE_CH1_LCTH = $4003
jmp init_ppu_and_wait
; zp_res
zp_res TIMER_LOW
update_audio:
; adc TIMER_LOW
; sta PULSE_CH1_TLOW
; sta TIMER_LOW
rts
zp_res PRESSED_A
zp_res PRESSED_B
nmi:
pha
txa
pha
tya
pha
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
lda #$01
sta JOY1
lda #$00
sta JOY1
lda JOY1 ; A
and #$01
tax
eor PRESSED_A
beq done
stx PRESSED_A
txa
and #$01
beq done
lda #$08
jsr update_audio
done:
bit JOY1 ; B
and #$01
tax
eor PRESSED_B
beq done_b
stx PRESSED_B
txa
and #$01
beq done_b
lda #$F7
jsr update_audio
done_b:
bit JOY1 ; Select
bit JOY1 ; Start
bit JOY1 ; Up
bit JOY1 ; Down
bit JOY1 ; Left
bit JOY1 ; Right
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK ; Enable rendering
pla
tay
pla
tax
pla
rti
irq:
rti

View File

@@ -0,0 +1,60 @@
.include "testing.s"
.include "joysticks.s"
.include "minimal_ppu.s"
reset:
sei
cld
ldx #$FF
txs
lda #$01
sta SNDCHN
lda #$AF ; Duty 2, LC halted, evelope enabled, volume = F
sta PULSE_CH1_DLCV
lda #$7F ;
sta PULSE_CH1_SWEEP
lda #$6F
sta PULSE_CH1_TLOW
sta TIMER_LOW
lda #$F0
sta PULSE_CH1_LCTH
; PULSE_CH1_DLCV = $4000
; PULSE_CH1_SWEEP = $4001
; PULSE_CH1_TLOW = $4002
; PULSE_CH1_LCTH = $4003
jmp init_ppu_and_wait
; zp_res
zp_res TIMER_LOW
update_audio:
adc TIMER_LOW
sta PULSE_CH1_TLOW
sta TIMER_LOW
rts
zp_res PRESSED_A
zp_res PRESSED_B
nmi:
nmi_start
jsr read_joy1
and #$80
beq not_a
lda #$08
jsr update_audio
not_a:
lda JOYTEMP
and #$40
beq not_b
lda #$01
jsr update_audio
not_b:
nmi_end
irq:
rti

View File

@@ -0,0 +1,135 @@
.include "testing.s"
reset:
sei
cld
ldx #$FF
txs
; Init PPU
bit PPUSTATUS
vwait1:
bit PPUSTATUS
bpl vwait1
vwait2:
bit PPUSTATUS
bpl vwait2
lda #$04
sta SNDCHN
lda #$FF
sta TRIANGLE_LINEAR_C
lda #$37
sta TRIANGLE_TIMER_LOW
lda #$00
sta TRIANGLE_LEN_T_HIGH
; TRIANGLE_LINEAR_C = $4008
; TRIANGLE_TIMER_LOW = $400A
; TRIANGLE_LEN_T_HIGH = $400B
lda #$80
sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses
lda #$00
loop:
; sta SNDMODE
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
; nop
jmp loop
; zp_res
zp_res TIMER_LOW
update_audio:
; adc TIMER_LOW
; sta PULSE_CH1_TLOW
; sta TIMER_LOW
rts
zp_res PRESSED_A
zp_res PRESSED_B
nmi:
pha
txa
pha
tya
pha
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
lda #$01
sta JOY1
lda #$00
sta JOY1
lda JOY1 ; A
and #$01
tax
eor PRESSED_A
beq done
stx PRESSED_A
txa
and #$01
beq done
lda #$08
jsr update_audio
done:
bit JOY1 ; B
and #$01
tax
eor PRESSED_B
beq done_b
stx PRESSED_B
txa
and #$01
beq done_b
lda #$F7
jsr update_audio
done_b:
bit JOY1 ; Select
bit JOY1 ; Start
bit JOY1 ; Up
bit JOY1 ; Down
bit JOY1 ; Left
bit JOY1 ; Right
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK ; Enable rendering
pla
tay
pla
tax
pla
rti
irq:
rti

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

Binary file not shown.

View File

@@ -0,0 +1,109 @@
.include "joysticks.s"
; Characters for view
.include "bitmap_font.s"
zp_res TEMP
.macro load_ppu_addr addr
lda #.hibyte(addr)
sta PPUADDR
lda #.lobyte(addr)
sta PPUADDR ; Load $2000
.endmacro
init_view:
; 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
; Fill NT0
load_ppu_addr $2000
ldx #$00
ldy #$04
lda #EMPTY
fill_loop:
sta PPUDATA
dex
bne fill_loop
dey
bne fill_loop
load_ppu_addr $23C0
ldx #$C0
lda #$00
palette_loop:
sta PPUDATA
dex
bne palette_loop
load_ppu_addr $3F00
lda #$0f
sta PPUDATA
lda #$20
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$09
sta PPUDATA
sta PPUDATA
sta PPUDATA
rts
enable_rendering:
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK
lda #$80
sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses
rts
nmi:
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
jsr read_joy1
jsr audio_nmi
.macro bit_lsr val
.scope
lsr
bcs carry_set
ldx #EMPTY
jmp carry_clear
carry_set:
ldx val
carry_clear:
stx PPUDATA
.endscope
.endmacro
load_ppu_addr $2008
lda JOYTEMP
bit_lsr #RIGHT
bit_lsr #LEFT
bit_lsr #DOWN
bit_lsr #UP
bit_lsr #PILL
bit_lsr #PILL
bit_lsr #$0B
bit_lsr #$0A
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK ; Enable rendering
rti

View File

@@ -0,0 +1,530 @@
.pushseg
.segment "CHARS"
.repeat 2 ; 0
.byte %00000000
.byte %00111000
.byte %01000100
.byte %01001100
.byte %01010100
.byte %01100100
.byte %01000100
.byte %00111000
.endrepeat
.repeat 2 ; 1
.byte %00000000
.byte %00010000
.byte %00110000
.byte %01010000
.byte %00010000
.byte %00010000
.byte %00010000
.byte %01111000
.endrepeat
.repeat 2 ; 2
.byte %00000000
.byte %00111000
.byte %01000100
.byte %00000100
.byte %00001000
.byte %00010000
.byte %00100000
.byte %01111100
.endrepeat
.repeat 2 ; 3
.byte %00000000
.byte %00111000
.byte %01000100
.byte %00000100
.byte %00011000
.byte %00000100
.byte %01000100
.byte %00111000
.endrepeat
.repeat 2 ; 4
.byte %00000000
.byte %00001000
.byte %00011000
.byte %00101000
.byte %01001000
.byte %11111100
.byte %00001000
.byte %00001000
.endrepeat
.repeat 2 ; 5
.byte %00000000
.byte %01111110
.byte %01000000
.byte %01000000
.byte %01111100
.byte %00000010
.byte %01000010
.byte %00111100
.endrepeat
.repeat 2 ; 6
.byte %00000000
.byte %00111100
.byte %01000010
.byte %01000000
.byte %01111100
.byte %01000010
.byte %01000010
.byte %00111100
.endrepeat
.repeat 2 ; 7
.byte %00000000
.byte %01111110
.byte %00000100
.byte %00000100
.byte %00001000
.byte %00001000
.byte %00010000
.byte %00010000
.endrepeat
.repeat 2 ; 8
.byte %00000000
.byte %00111000
.byte %01000100
.byte %01000100
.byte %00111000
.byte %01000100
.byte %01000100
.byte %00111000
.endrepeat
.repeat 2 ; 9
.byte %00000000
.byte %00111000
.byte %01000100
.byte %01000100
.byte %00111100
.byte %00000100
.byte %00000100
.byte %00000100
.endrepeat
.repeat 2 ; A
.byte %00000000
.byte %01111000
.byte %10000100
.byte %10000100
.byte %11111100
.byte %10000100
.byte %10000100
.byte %10000100
.endrepeat
.repeat 2 ; B
.byte %00000000
.byte %11111000
.byte %10000100
.byte %10000100
.byte %11111000
.byte %10000100
.byte %10000100
.byte %11111000
.endrepeat
.repeat 2 ; C
.byte %00000000
.byte %01111000
.byte %10000100
.byte %10000000
.byte %10000000
.byte %10000000
.byte %10000100
.byte %01111000
.endrepeat
.repeat 2 ; D
.byte %00000000
.byte %11111000
.byte %10000100
.byte %10000100
.byte %10000100
.byte %10000100
.byte %10000100
.byte %11111000
.endrepeat
.repeat 2 ; E
.byte %00000000
.byte %11111100
.byte %10000000
.byte %10000000
.byte %11111000
.byte %10000000
.byte %10000000
.byte %11111100
.endrepeat
.repeat 2 ; F
.byte %00000000
.byte %11111100
.byte %10000000
.byte %10000000
.byte %11110000
.byte %10000000
.byte %10000000
.byte %10000000
.endrepeat
.repeat 2 ; G
.byte %00000000
.byte %01111100
.byte %10000010
.byte %10000000
.byte %10001110
.byte %10000010
.byte %10000010
.byte %01111110
.endrepeat
.repeat 2 ; H
.byte %00000000
.byte %01000010
.byte %01000010
.byte %01000010
.byte %01111110
.byte %01000010
.byte %01000010
.byte %01000010
.endrepeat
.repeat 2 ; I
.byte %00000000
.byte %01111100
.byte %00010000
.byte %00010000
.byte %00010000
.byte %00010000
.byte %00010000
.byte %01111100
.endrepeat
.repeat 2 ; J
.byte %00000000
.byte %01111100
.byte %00001000
.byte %00001000
.byte %00001000
.byte %00001000
.byte %01001000
.byte %00110000
.endrepeat
.repeat 2 ; K
.byte %00000000
.byte %01000100
.byte %01001000
.byte %01010000
.byte %01100000
.byte %01010000
.byte %01001000
.byte %01000100
.endrepeat
.repeat 2 ; L
.byte %00000000
.byte %01000000
.byte %01000000
.byte %01000000
.byte %01000000
.byte %01000000
.byte %01000000
.byte %01111100
.endrepeat
.repeat 2 ; M
.byte %00000000
.byte %01000010
.byte %01100110
.byte %01011010
.byte %01000010
.byte %01000010
.byte %01000010
.byte %01000010
.endrepeat
.repeat 2 ; N
.byte %00000000
.byte %01000010
.byte %01100010
.byte %01010010
.byte %01001010
.byte %01000110
.byte %01000010
.byte %01000010
.endrepeat
.repeat 2 ; O
.byte %00000000
.byte %00111000
.byte %01000100
.byte %01000100
.byte %01000100
.byte %01000100
.byte %01000100
.byte %00111000
.endrepeat
.repeat 2 ; P
.byte %00000000
.byte %01111100
.byte %01000010
.byte %01000010
.byte %01111100
.byte %01000000
.byte %01000000
.byte %01000000
.endrepeat
.repeat 2 ; Q
.byte %00000000
.byte %00111000
.byte %01000100
.byte %01000100
.byte %01000100
.byte %01000100
.byte %01001100
.byte %00111100
.endrepeat
.repeat 2 ; R
.byte %00000000
.byte %01111100
.byte %01000010
.byte %01000010
.byte %01111100
.byte %01010000
.byte %01001000
.byte %01000100
.endrepeat
.repeat 2 ; S
.byte %00000000
.byte %00111100
.byte %01000010
.byte %00100000
.byte %00011000
.byte %00000100
.byte %01000100
.byte %00111000
.endrepeat
.repeat 2 ; T
.byte %00000000
.byte %01111100
.byte %00010000
.byte %00010000
.byte %00010000
.byte %00010000
.byte %00010000
.byte %00010000
.endrepeat
.repeat 2 ; U
.byte %00000000
.byte %01000010
.byte %01000010
.byte %01000010
.byte %01000010
.byte %01000010
.byte %01000010
.byte %00111100
.endrepeat
.repeat 2 ; V
.byte %00000000
.byte %01000100
.byte %01000100
.byte %01000100
.byte %00101000
.byte %00101000
.byte %00101000
.byte %00010000
.endrepeat
.repeat 2 ; W
.byte %00000000
.byte %01000100
.byte %01000100
.byte %01000100
.byte %01000100
.byte %01010100
.byte %01101100
.byte %01000100
.endrepeat
.repeat 2 ; X
.byte %00000000
.byte %01000100
.byte %01000100
.byte %00101000
.byte %00010000
.byte %00101000
.byte %01000100
.byte %01000100
.endrepeat
.repeat 2 ; Y
.byte %00000000
.byte %01000100
.byte %01000100
.byte %00101000
.byte %00010000
.byte %00010000
.byte %00010000
.byte %00010000
.endrepeat
.repeat 2 ; Z
.byte %00000000
.byte %01111110
.byte %00000010
.byte %00000100
.byte %00001000
.byte %00010000
.byte %00100000
.byte %01111110
.endrepeat
HEX = $24
.repeat 2 ; $
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00010100
.byte %00001000
.byte %00010100
.byte %00000000
.endrepeat
COLON = $25
.repeat 2 ; $
.byte %00000000
.byte %00011000
.byte %00011000
.byte %00000000
.byte %00000000
.byte %00011000
.byte %00011000
.byte %00000000
.endrepeat
UP = $26
.repeat 2 ; $
.byte %00000000
.byte %00010000
.byte %00111000
.byte %01010100
.byte %00010000
.byte %00010000
.byte %00010000
.byte %00000000
.endrepeat
DOWN = $27
.repeat 2 ; $
.byte %00000000
.byte %00010000
.byte %00010000
.byte %00010000
.byte %01010100
.byte %00111000
.byte %00010000
.byte %00000000
.endrepeat
LEFT = $28
.repeat 2 ; $
.byte %00000000
.byte %00000000
.byte %00010000
.byte %00100000
.byte %01111110
.byte %00100000
.byte %00010000
.byte %00000000
.endrepeat
RIGHT = $29
.repeat 2 ; $
.byte %00000000
.byte %00000000
.byte %00001000
.byte %00000100
.byte %01111110
.byte %00000100
.byte %00001000
.byte %00000000
.endrepeat
PILL = $2A
.repeat 2 ; $
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00111100
.byte %01111110
.byte %00111100
.byte %00000000
.endrepeat
EMPTY = $2B
.repeat 2 ; $
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.endrepeat
FONT_END = $2C
.popseg
.macro write_char_x chr
.if chr >= 'a' && chr <= 'z'
ldx #(chr - 'a' + 10)
.elseif chr >= 'A' && chr <= 'Z'
ldx #(chr - 'A' + 10)
.elseif chr >= '0' && chr <= '9'
ldx #(chr - '0')
.elseif chr = ' '
ldx #EMPTY
.elseif chr = ':'
ldx #COLON
.elseif chr = '$'
ldx #HEX
.else
.error "Unhandled character chr: " chr
.endif
stx PPUDATA
.endmacro
zp_res HEX_TEMP
write_hex_a:
sta HEX_TEMP
lsr
lsr
lsr
lsr
and #$F
sta PPUDATA
lda HEX_TEMP
and #$F
sta PPUDATA
rts

View File

@@ -0,0 +1,118 @@
; Builds program as iNES ROM
; Default is 32K PRG and 8K CHR ROM, NROM (0)
.if 0 ; Options to set before .include "shell.inc":
CHR_RAM=1 ; Use CHR-RAM instead of CHR-ROM
CART_WRAM=1 ; Use mapper that supports 8K WRAM in cart
CUSTOM_MAPPER=n ; Specify mapper number
.endif
.ifndef CUSTOM_MAPPER
.ifdef CART_WRAM
CUSTOM_MAPPER = 2 ; UNROM
.else
CUSTOM_MAPPER = 0 ; NROM
.endif
.endif
;;;; iNES header
.ifndef CUSTOM_HEADER
.segment "HEADER"
.byte $4E,$45,$53,26 ; "NES" EOF
.ifdef CHR_RAM
.byte 2,0 ; 32K PRG, CHR RAM
.else
.byte 2,1 ; 32K PRG, 8K CHR
.endif
.byte CUSTOM_MAPPER*$10+$01 ; vertical mirroring
.endif
.ifndef CUSTOM_VECTORS
.segment "VECTORS"
.word $FFFF,$FFFF,$FFFF, nmi, reset, irq
.endif
;;;; CHR-RAM/ROM
.ifdef CHR_RAM
.define CHARS "CHARS_PRG"
.segment CHARS
ascii_chr:
.segment "CHARS_PRG_ASCII"
.align $200
.incbin "ascii.chr"
ascii_chr_end:
.else
.define CHARS "CHARS"
.segment "CHARS_ASCII"
.align $200
.incbin "ascii.chr"
.res $1800
.endif
.segment "CODE"
.ifndef CUSTOM_RESET
reset:
.endif
std_reset:
jmp main
zp_byte nmi_count
zp_byte flags_from_nmi
zp_byte pclo_from_nmi
zp_byte nmi_temp
.ifndef CUSTOM_NMI
nmi:
.endif
std_nmi:
sta nmi_temp
pla
sta flags_from_nmi
pla
sta pclo_from_nmi
pha
lda flags_from_nmi
pha
lda nmi_temp
inc nmi_count
rti
zp_byte flags_from_irq
zp_byte pclo_from_irq
zp_byte irq_count
.ifndef CUSTOM_IRQ
irq:
.endif
std_irq: ; Record flags and PC low byte from stack
pla
sta flags_from_irq
pla
sta pclo_from_irq
pha
lda flags_from_irq
pha
inc irq_count
bit SNDCHN ; clear frame IRQ flag
rti
fail:
lda #$01
.ifndef CUSTOM_EXIT
exit:
.endif
std_exit:
.byte $02
.segment CHARS
.res $10,0
; Move code to $C000
.segment "CODE"
.res $4000
.code
.align 256

View File

@@ -0,0 +1,331 @@
; Scrolling text console with line wrapping, 30x29 characters.
; Buffers lines for speed. Will work even if PPU doesn't
; support scrolling (until text reaches bottom). Keeps border
; along bottom in case TV cuts it off.
;
; Defers most initialization until first newline, at which
; point it clears nametable and makes palette non-black.
;
; ** ASCII font must already be in CHR, and mirroring
; must be vertical or single-screen.
.ifndef CONSOLE_COLOR
CONSOLE_COLOR = $30 ; white
.endif
console_screen_width = 32 ; if lower than 32, left-justifies
; Number of characters of margin on left and right, to avoid
; text getting cut off by common TVs. OK if either/both are 0.
console_left_margin = 1
console_right_margin = 1
console_width = console_screen_width - console_left_margin - console_right_margin
zp_byte console_pos ; 0 to console_width
zp_byte console_scroll
zp_byte console_temp
bss_res console_buf,console_width
; Initializes console
console_init:
; Flag that console hasn't been initialized
setb console_scroll,$FF
setb console_pos,0
rts
; Hides console by disabling PPU rendering and blacking out
; first four entries of palette.
; Preserved: A, X, Y
console_hide:
pha
jsr console_wait_vbl_
setb PPUMASK,0
lda #$0F
jsr console_load_palette_
pla
rts
; Shows console display
; Preserved: X, Y
console_show:
pha
lda #CONSOLE_COLOR
jsr console_show_custom_color_
pla
rts
; Prints char A to console. Will not appear until
; a newline or flush occurs.
; Preserved: A, X, Y
console_print:
cmp #10
beq console_newline
sty console_temp
ldy console_pos
cpy #console_width
beq console_full_
sta console_buf,y
iny
sty console_pos
ldy console_temp
rts
; Displays current line and starts new one
; Preserved: A, X, Y
console_newline:
pha
jsr console_wait_vbl_
jsr console_flush_
jsr console_scroll_up_
setb console_pos,0
pla
rts
; Displays current line's contents without scrolling.
; Preserved: A, X, Y
console_flush:
pha
jsr console_wait_vbl_
jsr console_flush_
jsr console_apply_scroll_
pla
rts
;**** Internal routines ****
console_full_:
ldy console_temp
; Line is full
; If space, treat as newline
cmp #' '
beq console_newline
; Wrap current line at appropriate point
pha
tya
pha
jsr console_wrap_
pla
tay
pla
jmp console_print
; Inserts newline into buffer at appropriate position, leaving
; next line ready in buffer
; Preserved: X, console_temp
console_wrap_:
; Find beginning of last word
ldy #console_width
lda #' '
: dey
bmi console_newline
cmp console_buf,y
bne :-
; y = 0 to console_width-1
; Flush through current word and put remaining
; in buffer for next line
jsr console_wait_vbl_
; Time to last PPU write: 207 + 32*(26 + 10)
lda console_scroll
jsr console_set_ppuaddr_
stx console_pos ; save X
ldx #0
; Print everything before last word
: lda console_buf,x
sta PPUDATA
inx
dey
bpl :-
; x = 1 to console_width
; Move last word to beginning of buffer, and
; print spaces for rest of line
ldy #0
beq :++
: lda #' '
sta PPUDATA
lda console_buf,x
inx
sta console_buf,y
iny
: cpx #console_width
bne :--
ldx console_pos ; restore X
; Append new text after that
sty console_pos
; FALL THROUGH
; Scrolls up 8 pixels and clears one line BELOW new line
; Preserved: X, console_temp
console_scroll_up_:
; Scroll up 8 pixels
lda console_scroll
jsr console_add_8_to_scroll_
sta console_scroll
; Clear line AFTER that on screen
jsr console_add_8_to_scroll_
jsr console_set_ppuaddr_
ldy #console_width
lda #' '
: sta PPUDATA
dey
bne :-
; FALL THROUGH
; Applies current scrolling position to PPU
; Preserved: X, Y, console_temp
console_apply_scroll_:
lda #0
sta PPUADDR
sta PPUADDR
sta PPUSCROLL
lda console_scroll
jsr console_add_8_to_scroll_
jsr console_add_8_to_scroll_
sta PPUSCROLL
rts
; Sets PPU address for row
; In: A = scroll position
; Preserved: X, Y
console_set_ppuaddr_:
sta console_temp
lda #$08
asl console_temp
rol a
asl console_temp
rol a
sta PPUADDR
lda console_temp
ora #console_left_margin
sta PPUADDR
rts
; A = (A + 8) % 240
; Preserved: X, Y
console_add_8_to_scroll_:
cmp #240-8
bcc :+
adc #16-1;+1 for set carry
: adc #8
rts
console_show_custom_color_:
pha
jsr console_wait_vbl_
setb PPUMASK,PPUMASK_BG0
pla
jsr console_load_palette_
jmp console_apply_scroll_
console_load_palette_:
pha
setb PPUADDR,$3F
setb PPUADDR,$00
setb PPUDATA,$0F ; black
pla
sta PPUDATA
sta PPUDATA
sta PPUDATA
rts
; Initializes PPU if necessary, then waits for VBL
; Preserved: A, X, Y, console_temp
console_wait_vbl_:
lda console_scroll
cmp #$FF
bne @already_initialized
; Deferred initialization of PPU until first use of console
; In case PPU doesn't support scrolling, start a
; couple of lines down
setb console_scroll,16
jsr console_hide
tya
pha
; Fill nametable with spaces
setb PPUADDR,$20
setb PPUADDR,$00
ldy #240
lda #' '
: sta PPUDATA
sta PPUDATA
sta PPUDATA
sta PPUDATA
dey
bne :-
; Clear attributes
lda #0
ldy #$40
: sta PPUDATA
dey
bne :-
pla
tay
jsr console_show
@already_initialized:
jmp wait_vbl_optional
; Flushes current line
; Preserved: X, Y
console_flush_:
lda console_scroll
jsr console_set_ppuaddr_
sty console_temp
; Copy line
ldy #0
beq :++
: lda console_buf,y
sta PPUDATA
iny
: cpy console_pos
bne :--
ldy console_temp
rts

118
src/test_roms/common/crc.s Normal file
View File

@@ -0,0 +1,118 @@
; CRC-32 checksum calculation
zp_res checksum,4
zp_byte checksum_temp
zp_byte checksum_off_
; Turns CRC updating on/off. Allows nesting.
; Preserved: A, X, Y
crc_off:
dec checksum_off_
rts
crc_on: inc checksum_off_
beq :+
jpl internal_error ; catch unbalanced crc calls
: rts
; Initializes checksum module. Might initialize tables
; in the future.
init_crc:
jmp reset_crc
; Clears checksum and turns it on
; Preserved: X, Y
reset_crc:
lda #0
sta checksum_off_
lda #$FF
sta checksum
sta checksum + 1
sta checksum + 2
sta checksum + 3
rts
; Updates checksum with byte in A (unless disabled via crc_off)
; Preserved: A, X, Y
; Time: 357 clocks average
update_crc:
bit checksum_off_
bmi update_crc_off
update_crc_:
pha
stx checksum_temp
eor checksum
ldx #8
@bit: lsr checksum+3
ror checksum+2
ror checksum+1
ror a
bcc :+
sta checksum
lda checksum+3
eor #$ED
sta checksum+3
lda checksum+2
eor #$B8
sta checksum+2
lda checksum+1
eor #$83
sta checksum+1
lda checksum
eor #$20
: dex
bne @bit
sta checksum
ldx checksum_temp
pla
update_crc_off:
rts
; Prints checksum as 8-character hex value
print_crc:
jsr crc_off
; Print complement
ldx #3
: lda checksum,x
eor #$FF
jsr print_hex
dex
bpl :-
jmp crc_on
; EQ if checksum matches CRC
; Out: A=0 and EQ if match, A>0 and NE if different
; Preserved: X, Y
.macro is_crc crc
jsr_with_addr is_crc_,{.dword crc}
.endmacro
is_crc_:
tya
pha
; Compare with complemented checksum
ldy #3
: lda (ptr),y
sec
adc checksum,y
bne @wrong
dey
bpl :-
pla
tay
lda #0
rts
@wrong:
pla
tay
lda #1
rts

View File

@@ -0,0 +1,124 @@
; Fast table-based CRC-32
zp_res checksum,4
zp_byte checksum_temp
zp_byte checksum_off_
; Clears checksum and turns it on
; Preserved: X, Y
reset_crc:
lda #0
sta checksum_off_
lda #$FF
sta checksum
sta checksum + 1
sta checksum + 2
sta checksum + 3
rts
; Initializes fast CRC tables and resets checksum.
; Preserved: Y
init_crc_fast = reset_crc
; Updates checksum with byte from A
; Preserved: X, Y
; Time: 54 clocks
update_crc_fast:
stx checksum_temp
; Updates checksum with byte from A
; Preserved: Y
; Time: 42 clocks
.macro update_crc_fast
eor checksum
tax
lda checksum+1
eor checksum_t0,x
sta checksum
lda checksum+2
eor checksum_t1,x
sta checksum+1
lda checksum+3
eor checksum_t2,x
sta checksum+2
lda checksum_t3,x
sta checksum+3
.endmacro
update_crc_fast
ldx checksum_temp
rts
.pushseg
.segment "RODATA"
.align 256
checksum_t0:
.byte $8D,$1B,$A1,$37,$94,$02,$B8,$2E,$BF,$29,$93,$05,$A6,$30,$8A,$1C
.byte $E9,$7F,$C5,$53,$F0,$66,$DC,$4A,$DB,$4D,$F7,$61,$C2,$54,$EE,$78
.byte $45,$D3,$69,$FF,$5C,$CA,$70,$E6,$77,$E1,$5B,$CD,$6E,$F8,$42,$D4
.byte $21,$B7,$0D,$9B,$38,$AE,$14,$82,$13,$85,$3F,$A9,$0A,$9C,$26,$B0
.byte $1D,$8B,$31,$A7,$04,$92,$28,$BE,$2F,$B9,$03,$95,$36,$A0,$1A,$8C
.byte $79,$EF,$55,$C3,$60,$F6,$4C,$DA,$4B,$DD,$67,$F1,$52,$C4,$7E,$E8
.byte $D5,$43,$F9,$6F,$CC,$5A,$E0,$76,$E7,$71,$CB,$5D,$FE,$68,$D2,$44
.byte $B1,$27,$9D,$0B,$A8,$3E,$84,$12,$83,$15,$AF,$39,$9A,$0C,$B6,$20
.byte $AD,$3B,$81,$17,$B4,$22,$98,$0E,$9F,$09,$B3,$25,$86,$10,$AA,$3C
.byte $C9,$5F,$E5,$73,$D0,$46,$FC,$6A,$FB,$6D,$D7,$41,$E2,$74,$CE,$58
.byte $65,$F3,$49,$DF,$7C,$EA,$50,$C6,$57,$C1,$7B,$ED,$4E,$D8,$62,$F4
.byte $01,$97,$2D,$BB,$18,$8E,$34,$A2,$33,$A5,$1F,$89,$2A,$BC,$06,$90
.byte $3D,$AB,$11,$87,$24,$B2,$08,$9E,$0F,$99,$23,$B5,$16,$80,$3A,$AC
.byte $59,$CF,$75,$E3,$40,$D6,$6C,$FA,$6B,$FD,$47,$D1,$72,$E4,$5E,$C8
.byte $F5,$63,$D9,$4F,$EC,$7A,$C0,$56,$C7,$51,$EB,$7D,$DE,$48,$F2,$64
.byte $91,$07,$BD,$2B,$88,$1E,$A4,$32,$A3,$35,$8F,$19,$BA,$2C,$96,$00
checksum_t1:
.byte $EF,$DF,$8E,$BE,$2B,$1B,$4A,$7A,$67,$57,$06,$36,$A3,$93,$C2,$F2
.byte $FF,$CF,$9E,$AE,$3B,$0B,$5A,$6A,$77,$47,$16,$26,$B3,$83,$D2,$E2
.byte $CF,$FF,$AE,$9E,$0B,$3B,$6A,$5A,$47,$77,$26,$16,$83,$B3,$E2,$D2
.byte $DF,$EF,$BE,$8E,$1B,$2B,$7A,$4A,$57,$67,$36,$06,$93,$A3,$F2,$C2
.byte $AE,$9E,$CF,$FF,$6A,$5A,$0B,$3B,$26,$16,$47,$77,$E2,$D2,$83,$B3
.byte $BE,$8E,$DF,$EF,$7A,$4A,$1B,$2B,$36,$06,$57,$67,$F2,$C2,$93,$A3
.byte $8E,$BE,$EF,$DF,$4A,$7A,$2B,$1B,$06,$36,$67,$57,$C2,$F2,$A3,$93
.byte $9E,$AE,$FF,$CF,$5A,$6A,$3B,$0B,$16,$26,$77,$47,$D2,$E2,$B3,$83
.byte $6C,$5C,$0D,$3D,$A8,$98,$C9,$F9,$E4,$D4,$85,$B5,$20,$10,$41,$71
.byte $7C,$4C,$1D,$2D,$B8,$88,$D9,$E9,$F4,$C4,$95,$A5,$30,$00,$51,$61
.byte $4C,$7C,$2D,$1D,$88,$B8,$E9,$D9,$C4,$F4,$A5,$95,$00,$30,$61,$51
.byte $5C,$6C,$3D,$0D,$98,$A8,$F9,$C9,$D4,$E4,$B5,$85,$10,$20,$71,$41
.byte $2D,$1D,$4C,$7C,$E9,$D9,$88,$B8,$A5,$95,$C4,$F4,$61,$51,$00,$30
.byte $3D,$0D,$5C,$6C,$F9,$C9,$98,$A8,$B5,$85,$D4,$E4,$71,$41,$10,$20
.byte $0D,$3D,$6C,$5C,$C9,$F9,$A8,$98,$85,$B5,$E4,$D4,$41,$71,$20,$10
.byte $1D,$2D,$7C,$4C,$D9,$E9,$B8,$88,$95,$A5,$F4,$C4,$51,$61,$30,$00
checksum_t2:
.byte $02,$05,$0C,$0B,$6F,$68,$61,$66,$D9,$DE,$D7,$D0,$B4,$B3,$BA,$BD
.byte $B5,$B2,$BB,$BC,$D8,$DF,$D6,$D1,$6E,$69,$60,$67,$03,$04,$0D,$0A
.byte $6C,$6B,$62,$65,$01,$06,$0F,$08,$B7,$B0,$B9,$BE,$DA,$DD,$D4,$D3
.byte $DB,$DC,$D5,$D2,$B6,$B1,$B8,$BF,$00,$07,$0E,$09,$6D,$6A,$63,$64
.byte $DE,$D9,$D0,$D7,$B3,$B4,$BD,$BA,$05,$02,$0B,$0C,$68,$6F,$66,$61
.byte $69,$6E,$67,$60,$04,$03,$0A,$0D,$B2,$B5,$BC,$BB,$DF,$D8,$D1,$D6
.byte $B0,$B7,$BE,$B9,$DD,$DA,$D3,$D4,$6B,$6C,$65,$62,$06,$01,$08,$0F
.byte $07,$00,$09,$0E,$6A,$6D,$64,$63,$DC,$DB,$D2,$D5,$B1,$B6,$BF,$B8
.byte $BA,$BD,$B4,$B3,$D7,$D0,$D9,$DE,$61,$66,$6F,$68,$0C,$0B,$02,$05
.byte $0D,$0A,$03,$04,$60,$67,$6E,$69,$D6,$D1,$D8,$DF,$BB,$BC,$B5,$B2
.byte $D4,$D3,$DA,$DD,$B9,$BE,$B7,$B0,$0F,$08,$01,$06,$62,$65,$6C,$6B
.byte $63,$64,$6D,$6A,$0E,$09,$00,$07,$B8,$BF,$B6,$B1,$D5,$D2,$DB,$DC
.byte $66,$61,$68,$6F,$0B,$0C,$05,$02,$BD,$BA,$B3,$B4,$D0,$D7,$DE,$D9
.byte $D1,$D6,$DF,$D8,$BC,$BB,$B2,$B5,$0A,$0D,$04,$03,$67,$60,$69,$6E
.byte $08,$0F,$06,$01,$65,$62,$6B,$6C,$D3,$D4,$DD,$DA,$BE,$B9,$B0,$B7
.byte $BF,$B8,$B1,$B6,$D2,$D5,$DC,$DB,$64,$63,$6A,$6D,$09,$0E,$07,$00
checksum_t3:
.byte $D2,$A5,$3C,$4B,$D5,$A2,$3B,$4C,$DC,$AB,$32,$45,$DB,$AC,$35,$42
.byte $CF,$B8,$21,$56,$C8,$BF,$26,$51,$C1,$B6,$2F,$58,$C6,$B1,$28,$5F
.byte $E9,$9E,$07,$70,$EE,$99,$00,$77,$E7,$90,$09,$7E,$E0,$97,$0E,$79
.byte $F4,$83,$1A,$6D,$F3,$84,$1D,$6A,$FA,$8D,$14,$63,$FD,$8A,$13,$64
.byte $A4,$D3,$4A,$3D,$A3,$D4,$4D,$3A,$AA,$DD,$44,$33,$AD,$DA,$43,$34
.byte $B9,$CE,$57,$20,$BE,$C9,$50,$27,$B7,$C0,$59,$2E,$B0,$C7,$5E,$29
.byte $9F,$E8,$71,$06,$98,$EF,$76,$01,$91,$E6,$7F,$08,$96,$E1,$78,$0F
.byte $82,$F5,$6C,$1B,$85,$F2,$6B,$1C,$8C,$FB,$62,$15,$8B,$FC,$65,$12
.byte $3F,$48,$D1,$A6,$38,$4F,$D6,$A1,$31,$46,$DF,$A8,$36,$41,$D8,$AF
.byte $22,$55,$CC,$BB,$25,$52,$CB,$BC,$2C,$5B,$C2,$B5,$2B,$5C,$C5,$B2
.byte $04,$73,$EA,$9D,$03,$74,$ED,$9A,$0A,$7D,$E4,$93,$0D,$7A,$E3,$94
.byte $19,$6E,$F7,$80,$1E,$69,$F0,$87,$17,$60,$F9,$8E,$10,$67,$FE,$89
.byte $49,$3E,$A7,$D0,$4E,$39,$A0,$D7,$47,$30,$A9,$DE,$40,$37,$AE,$D9
.byte $54,$23,$BA,$CD,$53,$24,$BD,$CA,$5A,$2D,$B4,$C3,$5D,$2A,$B3,$C4
.byte $72,$05,$9C,$EB,$75,$02,$9B,$EC,$7C,$0B,$92,$E5,$7B,$0C,$95,$E2
.byte $6F,$18,$81,$F6,$68,$1F,$86,$F1,$61,$16,$8F,$F8,$66,$11,$88,$FF
.popseg

View File

@@ -0,0 +1,190 @@
; Delays in CPU clocks, milliseconds, etc. All routines are re-entrant
; (no global data). No routines touch X or Y during execution.
; Code generated by macros is relocatable; it contains no JMPs to itself.
zp_byte delay_temp_ ; only written to
; Delays n clocks, from 2 to 16777215
; Preserved: A, X, Y, flags
.macro delay n
.if (n) < 0 .or (n) = 1 .or (n) > 16777215
.error "Delay out of range"
.endif
delay_ (n)
.endmacro
; Delays n milliseconds (1/1000 second)
; n can range from 0 to 1100.
; Preserved: A, X, Y, flags
.macro delay_msec n
.if (n) < 0 .or (n) > 1100
.error "time out of range"
.endif
delay ((n)*CLOCK_RATE+500)/1000
.endmacro
; Delays n microseconds (1/1000000 second).
; n can range from 0 to 100000.
; Preserved: A, X, Y, flags
.macro delay_usec n
.if (n) < 0 .or (n) > 100000
.error "time out of range"
.endif
delay ((n)*((CLOCK_RATE+50)/100)+5000)/10000
.endmacro
.align 64
; Delays A clocks + overhead
; Preserved: X, Y
; Time: A+25 clocks (including JSR)
: sbc #7 ; carry set by CMP
delay_a_25_clocks:
cmp #7
bcs :- ; do multiples of 7
lsr a ; bit 0
bcs :+
: ; A=clocks/2, either 0,1,2,3
beq @zero ; 0: 5
lsr a
beq :+ ; 1: 7
bcc :+ ; 2: 9
@zero: bne :+ ; 3: 11
: rts ; (thanks to dclxvi for the algorithm)
; Delays A*256 clocks + overhead
; Preserved: X, Y
; Time: A*256+16 clocks (including JSR)
delay_256a_16_clocks:
cmp #0
bne :+
rts
delay_256a_11_clocks_:
: pha
lda #256-19-22
jsr delay_a_25_clocks
pla
clc
adc #$FF
bne :-
rts
; Delays A*65536 clocks + overhead
; Preserved: X, Y
; Time: A*65536+16 clocks (including JSR)
delay_65536a_16_clocks:
cmp #0
bne :+
rts
delay_65536a_11_clocks_:
: pha
lda #256-19-22-13
jsr delay_a_25_clocks
lda #255
jsr delay_256a_11_clocks_
pla
clc
adc #$FF
bne :-
rts
max_short_delay = 41
; delay_short_ macro jumps into these
.res (max_short_delay-12)/2,$EA ; NOP
delay_unrolled_:
rts
.macro delay_short_ n
.if n < 0 .or n = 1 .or n > max_short_delay
.error "Internal delay error"
.endif
.if n = 0
; nothing
.elseif n = 2
nop
.elseif n = 3
sta <delay_temp_
.elseif n = 4
nop
nop
.elseif n = 5
sta <delay_temp_
nop
.elseif n = 6
nop
nop
nop
.elseif n = 7
php
plp
.elseif n = 8
nop
nop
nop
nop
.elseif n = 9
php
plp
nop
.elseif n = 10
sta <delay_temp_
php
plp
.elseif n = 11
php
plp
nop
nop
.elseif n = 13
php
plp
nop
nop
nop
.elseif n & 1
sta <delay_temp_
jsr delay_unrolled_-((n-15)/2)
.else
jsr delay_unrolled_-((n-12)/2)
.endif
.endmacro
.macro delay_nosave_ n
; 65536+17 = maximum delay using delay_256a_11_clocks_
; 255+27 = maximum delay using delay_a_25_clocks
; 27 = minimum delay using delay_a_25_clocks
.if n > 65536+17
lda #^(n - 15)
jsr delay_65536a_11_clocks_
; +2 ensures remaining clocks is never 1
delay_nosave_ (((n - 15) & $FFFF) + 2)
.elseif n > 255+27
lda #>(n - 15)
jsr delay_256a_11_clocks_
; +2 ensures remaining clocks is never 1
delay_nosave_ (<(n - 15) + 2)
.elseif n >= 27
lda #<(n - 27)
jsr delay_a_25_clocks
.else
delay_short_ n
.endif
.endmacro
.macro delay_ n
.if n > max_short_delay
php
pha
delay_nosave_ (n - 14)
pla
plp
.else
delay_short_ n
.endif
.endmacro

Binary file not shown.

View File

@@ -0,0 +1,245 @@
REGION_FREE = 1
.include "shell.inc"
.include "crc_fast.s"
zp_byte in_p
zp_byte in_a
zp_byte in_x
zp_byte in_y
zp_byte in_s
.macro entry opcode,name,crc_0,crc_1,crc_2,crc_3
jsr reset_crc
lda #opcode
sta opcode_byte - testcase_entry + testcase_instance
jsr run_testcase
; testcase_0 OPERANDS + 4,{testcase opcode}
lda checksum
cmp #crc_0
jne fail
lda checksum+1
cmp #crc_1
jne fail
lda checksum+2
cmp #crc_2
jne fail
lda checksum+3
cmp #crc_3
jne fail
.endmacro
testcase_entry:
lda in_p
pha
lda in_a
ldx in_x
ldy in_y
plp
opcode_byte: .byte 0
.if OPERANDS > 0
; .out .sprintf(" 1: %s", .string(op_1))
.byte 0
.endif
.if OPERANDS > 1
; .out .sprintf(" 2: %s", .string(op_2))
.byte 0
.endif
php
pha
txa
pha
tya
pha
; Stack: <y, x, a, p>
pla
jsr update_crc_fast
pla
jsr update_crc_fast
pla
jsr update_crc_fast
pla
jsr update_crc_fast
rts
testcase_entry_size = * - testcase_entry
; .macro testcase opcode, reg_p, reg_a, reg_x, reg_y, op_1, op_2
; jsr crc_calc
; .out .sprintf("OP: %s", .string(opcode))
; .out .sprintf(" S: %s", .string(reg_p))
; .out .sprintf(" A: %s", .string(reg_a))
; .out .sprintf(" X: %s", .string(reg_x))
; .out .sprintf(" Y: %s", .string(reg_y))
; .endmacro
; .macro testcase_6 opcode, ops
; testcase opcode, ops
; ; .mid(0, 1, ops), .mid(1, 1, ops), .mid(2, 1, ops), .mid(3, 1, ops), .mid(4, 1, ops), .mid(5, 1, ops)
; .endmacro
; .macro testcase_0 count,ins
; .if count > 0
; testcase_0 count - 1, {ins,0}
; testcase_0 count - 1, {ins,1}
; testcase_0 count - 1, {ins,2}
; testcase_0 count - 1, {ins,$40}
; testcase_0 count - 1, {ins,$7F}
; testcase_0 count - 1, {ins,$80}
; testcase_0 count - 1, {ins,$81}
; testcase_0 count - 1, {ins,$FF}
; ; .byte 0,1,2,$40,$7F,$80,$81,$FF
; .else
; ins
; .endif
; .endmacro
.pushseg
.segment "BSS"
testcase_instance: .res testcase_entry_size
.popseg
testcase_init:
ldx #0
@loop: lda testcase_entry,x
sta testcase_instance,x
inx
cpx #testcase_entry_size
bmi @loop
rts
; values:
; .byte 0,1,2,$40,$7F,$80,$81,$FF
; values_size = * - values
; .byte 0,1,2,$40,$7F,$80,$81,$FF
.macro increment pos, next, done
lda pos
cmp #0
bne @a_1
lda #1
sta pos
jmp done
@a_1: cmp #1
bne @a_2
lda #2
sta pos
jmp done
@a_2: cmp #2
bne @a_40
lda #40
sta pos
jmp done
@a_40: cmp #$40
bne @a_7F
lda #$7F
sta pos
jmp done
@a_7F: cmp #$7F
bne @a_80
lda #$80
sta pos
jmp done
@a_80: cmp #$80
bne @a_81
lda #$81
sta pos
jmp done
@a_81: cmp #$81
bne @a_FF
lda #$FF
sta pos
jmp done
@a_FF: cmp #$FF
lda #0
sta pos
jmp next
.endmacro
run_testcase:
increment in_a, increment_x, done
increment_x:
increment in_x, increment_y, done
increment_y:
increment in_y, increment_p, done
increment_p:
increment in_p, ret, done
done:
jsr testcase_instance
jmp run_testcase
ret:
rts
; Defines instruction to test
; Values set_paxyso sets registers to. Set
; before calling test_values, which can then
; overwrite them if desired.
; Temporary space for check_paxyso
zp_byte out_a
zp_byte out_x
zp_byte out_s
; Values to cycle through for registers
; ; Sets bytes on stack around in_s
; .macro set_stack
; ldx in_s
; inx
; inx
; ldy #6
; : txa
; asl
; eor #$A5
; sta $100,x
; dex
; dey
; bne :-
; .endmacro
; ; Checksums bytes on stack around in_s
; .macro check_stack
; ldx in_s
; inx
; inx
; ldy #6
; : lda $100,x
; dex
; jsr update_crc
; dey
; bne :-
; .endmacro
; ; Sets P, A, X, Y, S, and operand
; .macro set_paxyso
; ldx in_s
; txs
; lda values,y
; sta operand
; lda in_p
; pha
; lda in_a
; ldx in_x
; ldy in_y
; plp
; .endmacro
; ; Checksums P, A, X, Y, S, and operand
; .macro check_paxyso
; php
; sta out_a
; pla
; stx out_x
; tsx
; stx out_s
; ldx saved_s
; txs
; cld
; jsr update_crc_fast
; lda out_a
; jsr update_crc_fast
; lda out_x
; jsr update_crc_fast
; tya
; jsr update_crc_fast
; lda out_s
; jsr update_crc_fast
; lda operand
; jsr update_crc_fast
; .endmacro

View File

@@ -0,0 +1,199 @@
; Offset of current instruction
zp_byte instrs_idx
zp_byte failed_count
main:
; Stack slightly lower than top
ldx #$A2
txs
jsr init_crc_fast
; Test each instruction
lda #0
@loop: sta instrs_idx
tay
jsr reset_crc
lda instrs,y
jsr test_instr
jsr check_result
lda instrs_idx
clc
adc #4
cmp #instrs_size
bne @loop
.ifdef BUILD_DEVCART
lda #0
jmp exit
.endif
lda failed_count
jne test_failed
jmp tests_passed
; Check result of test
check_result:
.ifdef BUILD_DEVCART
; Print correct CRC
jsr crc_off
print_str ".dword $"
ldx #0
: lda checksum,x
jsr print_hex
inx
cpx #4
bne :-
jsr print_newline
jsr crc_on
.else
; Verify CRC
ldx #3
ldy instrs_idx
: lda checksum,x
cmp correct_checksums,y
bne @wrong
iny
dex
bpl :-
.endif
rts
; Print failed opcode and name
@wrong:
ldy instrs_idx
lda instrs,y
jsr print_a
jsr play_byte
lda instrs+2,y
sta addr
lda instrs+3,y
sta addr+1
jsr print_str_addr
jsr print_newline
inc failed_count
rts
; Place where instruction is executed
instr = $3A0
; Tests instr A
test_instr:
sta instr
jsr avoid_silent_nsf
; Copy rest of template
ldx #instr_template_size - 1
: lda instr_template,x
sta instr,x
dex
bne :-
; Disable and be sure APU IRQs are clear, since
; I flag gets cleared during testing.
setb SNDMODE,$C0
setb $4010,0
nop
lda SNDCHN
; Default stack
lda #$90
sta in_s
; Test with different flags
lda #$00
jsr test_flags
lda #$FF
jsr test_flags
rts
; Position in operand table
zp_byte operand_idx
test_flags:
sta in_p
ldy #values_size-1
: sty operand_idx
lda values,y
sta in_a
lda values+1,y
sta in_x
lda values+2,y
sta in_y
jsr test_values
ldy operand_idx
dey
bpl :-
rts
.ifndef values2
values2 = values
values2_size = values_size
.endif
.macro test_normal
zp_byte a_idx
zp_byte saved_s
tsx
stx saved_s
set_stack
ldy #values2_size-1
inner: sty a_idx
lda values2,y
sta operand
set_in
; For debugging
.if 0
; P A X Y S O (z,x) (z),y
jsr print_p
jsr print_a
jsr print_x
jsr print_y
jsr print_s
lda operand
jsr print_a
.ifdef address
lda (address,x)
jsr print_a
lda (address),y
jsr print_a
.else
lda operand,x
jsr print_a
lda operand,y
jsr print_a
.endif
jsr print_newline
.endif
jmp instr
instr_done:
check_out
ldy a_idx
dey
bpl inner
check_stack
ldx saved_s
txs
.endmacro

View File

@@ -0,0 +1,30 @@
JOY_RIGHT_MASK = %00000001
JOY_LEFT_MASK = %00000010
JOY_DOWN_MASK = %00000100
JOY_UP_MASK = %00001000
JOY_START_MASK = %00010000
JOY_SELECT_MASK = %00100000
JOY_B_MASK = %01000000
JOY_A_MASK = %10000000
zp_res JOYTEMP
; Reads joy_stick 1 into a (and JOYTEMP)
read_joy1:
; Init joysticks for reading
lda #$01
sta JOY1
lda #$00
sta JOY1
; Read JOY1
.repeat 8
asl A
sta JOYTEMP
lda JOY1
and #$01
ora JOYTEMP
.endrepeat
sta JOYTEMP
rts

View File

@@ -0,0 +1,169 @@
; jxx equivalents to bxx
.macpack longbranch
; blt, bge equivalents to bcc, bcs
.define blt bcc
.define bge bcs
.define jge jcs
.define jlt jcc
; Puts data in another segment
.macro seg_data seg,data
.pushseg
.segment seg
data
.popseg
.endmacro
; Reserves size bytes in zeropage/bss for name.
; If size is omitted, reserves one byte.
.macro zp_res name,size
.ifblank size
zp_res name,1
.else
seg_data "ZEROPAGE",{name: .res size}
.endif
.endmacro
.macro bss_res name,size
.ifblank size
bss_res name,1
.else
seg_data "BSS",{name: .res size}
.endif
.endmacro
.macro nv_res name,size
.ifblank size
nv_res name,1
.else
seg_data "NVRAM",{name: .res size}
.endif
.endmacro
; Reserves one byte in zeropage for name (very common)
.macro zp_byte name
seg_data "ZEROPAGE",{name: .res 1}
.endmacro
; Passes constant data to routine in addr
; Preserved: A, X, Y
.macro jsr_with_addr routine,data
.local Addr
pha
lda #<Addr
sta addr
lda #>Addr
sta addr+1
pla
jsr routine
seg_data "RODATA",{Addr: data}
.endmacro
; Calls routine multiple times, with A having the
; value 'start' the first time, 'start+step' the
; second time, up to 'end' for the last time.
.macro for_loop routine,start,end,step
lda #start
: pha
jsr routine
pla
clc
adc #step
cmp #<((end)+(step))
bne :-
.endmacro
; Calls routine n times. The value of A in the routine
; counts from 0 to n-1.
.macro loop_n_times routine,n
for_loop routine,0,n-1,+1
.endmacro
; Same as for_loop, except uses 16-bit value in YX.
; -256 <= step <= 255
.macro for_loop16 routine,start,end,step
.if (step) < -256 || (step) > 255
.error "Step must be within -256 to 255"
.endif
ldy #>(start)
lda #<(start)
: tax
pha
tya
pha
jsr routine
pla
tay
pla
clc
adc #step
.if (step) > 0
bcc :+
iny
.else
bcs :+
dey
.endif
: cmp #<((end)+(step))
bne :--
cpy #>((end)+(step))
bne :--
.endmacro
; Copies byte from in to out
; Preserved: X, Y
.macro mov out, in
lda in
sta out
.endmacro
; Stores byte at addr
; Preserved: X, Y
.macro setb addr, byte
lda #byte
sta addr
.endmacro
; Stores word at addr
; Preserved: X, Y
.macro setw addr, word
lda #<(word)
sta addr
lda #>(word)
sta addr+1
.endmacro
; Loads XY with 16-bit immediate or value at address
.macro ldxy Arg
.if .match( .left( 1, {Arg} ), # )
ldy #<(.right( .tcount( {Arg} )-1, {Arg} ))
ldx #>(.right( .tcount( {Arg} )-1, {Arg} ))
.else
ldy (Arg)
ldx (Arg)+1
.endif
.endmacro
; Increments word at Addr and sets Z flag appropriately
; Preserved: A, X, Y
.macro incw Addr
.local @incw_skip ; doesn't work, so HOW THE HELL DO YOU MAKE A LOCAL LABEL IN A MACRO THAT DOESN"T DISTURB INVOKING CODE< HUH?????? POS
inc Addr
bne @incw_skip
inc Addr+1
@incw_skip:
.endmacro
; Increments XY as 16-bit register, in CONSTANT time.
; Z flag set based on entire result.
; Preserved: A
; Time: 7 clocks
.macro inxy
iny ; 2
beq *+4 ; 3
; -1
bne *+3 ; 3
; -1
inx ; 2
.endmacro

View File

@@ -0,0 +1,37 @@
init_ppu_and_wait:
; 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
lda #$00
infinite_loop:
jmp infinite_loop
.macro nmi_start
pha
txa
pha
tya
pha
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
.endmacro
.macro nmi_end
lda #%00001110
sta PPUMASK ; Enable rendering
pla
tay
pla
tax
pla
rti
.endmacro

View File

@@ -0,0 +1,55 @@
; NES I/O locations and masks
; Clocks per second
.ifndef CLOCK_RATE
CLOCK_RATE = 1789773 ; NTSC
; CLOCK_RATE = 1662607 ; PAL
.endif
.ifndef BUILD_NSF
; PPU
PPUCTRL = $2000
PPUMASK = $2001
PPUSTATUS = $2002
SPRADDR = $2003
SPRDATA = $2004
PPUSCROLL = $2005
PPUADDR = $2006
PPUDATA = $2007
SPRDMA = $4014
PPUCTRL_NMI = $80
PPUMASK_BG0 = $0A
PPUCTRL_8X8 = $00
PPUCTRL_8X16 = $20
PPUMASK_SPR = $14
PPUMASK_BG0CLIP = $08
.endif
; APU
SNDCHN = $4015
JOY1 = $4016
JOY2 = $4017
SNDMODE = $4017
PULSE_CH1_DLCV = $4000
PULSE_CH1_SWEEP = $4001
PULSE_CH1_TLOW = $4002
PULSE_CH1_LCTH = $4003
PULSE_CH2_DLCV = $4004
PULSE_CH2_SWEEP = $4005
PULSE_CH2_TLOW = $4006
PULSE_CH2_LCTH = $4007
TRIANGLE_LINEAR_C = $4008
TRIANGLE_TIMER_LOW = $400A
TRIANGLE_LEN_T_HIGH = $400B
SNDMODE_NOIRQ = $40
.if CLOCK_RATE = 1789773
PPU_FRAMELEN = 29781
.elseif CLOCK_RATE = 1662607
PPU_FRAMELEN = 33248
.endif

142
src/test_roms/common/ppu.s Normal file
View File

@@ -0,0 +1,142 @@
; PPU utilities
bss_res ppu_not_present
; Sets PPUADDR to w
; Preserved: X, Y
.macro set_ppuaddr w
bit PPUSTATUS
setb PPUADDR,>w
setb PPUADDR,<w
.endmacro
; Delays by no more than n scanlines
.macro delay_scanlines n
.if CLOCK_RATE <> 1789773
.error "Currently only supports NTSC"
.endif
delay ((n)*341)/3
.endmacro
; Waits for VBL then disables PPU rendering.
; Preserved: A, X, Y
disable_rendering:
pha
jsr wait_vbl_optional
setb PPUMASK,0
pla
rts
; Fills first nametable with $00
; Preserved: Y
clear_nametable:
ldx #$20
bne clear_nametable_
clear_nametable2:
ldx #$24
clear_nametable_:
lda #0
jsr fill_screen_
; Clear pattern table
ldx #64
: sta PPUDATA
dex
bne :-
rts
; Fills screen with tile A
; Preserved: A, Y
fill_screen:
ldx #$20
bne fill_screen_
; Same as fill_screen, but fills other nametable
fill_screen2:
ldx #$24
fill_screen_:
stx PPUADDR
ldx #$00
stx PPUADDR
ldx #240
: sta PPUDATA
sta PPUDATA
sta PPUDATA
sta PPUDATA
dex
bne :-
rts
; Fills palette with $0F
; Preserved: Y
clear_palette:
set_ppuaddr $3F00
ldx #$20
lda #$0F
: sta PPUDATA
dex
bne :-
; Fills OAM with $FF
; Preserved: Y
clear_oam:
lda #$FF
; Fills OAM with A
; Preserved: A, Y
fill_oam:
ldx #0
stx SPRADDR
: sta SPRDATA
dex
bne :-
rts
; Initializes wait_vbl_optional. Must be called before
; using it.
.align 32
init_wait_vbl:
; Wait for VBL flag to be set, or ~60000
; clocks (2 frames) to pass
ldy #24
ldx #1
bit PPUSTATUS
: bit PPUSTATUS
bmi @set
dex
bne :-
dey
bpl :-
@set:
; Be sure flag didn't stay set (in case
; PPUSTATUS always has high bit set)
tya
ora PPUSTATUS
sta ppu_not_present
rts
; Same as wait_vbl, but returns immediately if PPU
; isn't working or doesn't support VBL flag
; Preserved: A, X, Y
.align 16
wait_vbl_optional:
bit ppu_not_present
bmi :++
; FALL THROUGH
; Clears VBL flag then waits for it to be set.
; Preserved: A, X, Y
wait_vbl:
bit PPUSTATUS
: bit PPUSTATUS
bpl :-
: rts

View File

@@ -0,0 +1,235 @@
; Prints values in various ways to output,
; including numbers and strings.
newline = 10
zp_byte print_temp_
; Prints indicated register to console as two hex
; chars and space
; Preserved: A, X, Y, flags
print_a:
php
pha
print_reg_:
jsr print_hex
lda #' '
jsr print_char_
pla
plp
rts
print_x:
php
pha
txa
jmp print_reg_
print_y:
php
pha
tya
jmp print_reg_
print_p:
php
pha
php
pla
jmp print_reg_
print_s:
php
pha
txa
tsx
inx
inx
inx
inx
jsr print_x
tax
pla
plp
rts
; Prints A as two hex characters, NO space after
; Preserved: A, X, Y
print_hex:
jsr update_crc
pha
lsr a
lsr a
lsr a
lsr a
jsr @nibble
pla
pha
and #$0F
jsr @nibble
pla
rts
@nibble:
cmp #10
blt @digit
adc #6;+1 since carry is set
@digit: adc #'0'
jmp print_char_
; Prints character and updates checksum UNLESS
; it's a newline.
; Preserved: A, X, Y
print_char:
cmp #newline
beq :+
jsr update_crc
: pha
jsr print_char_
pla
rts
; Prints space. Does NOT update checksum.
; Preserved: A, X, Y
print_space:
pha
lda #' '
jsr print_char_
pla
rts
; Advances to next line. Does NOT update checksum.
; Preserved: A, X, Y
print_newline:
pha
lda #newline
jsr print_char_
pla
rts
; Prints string
; Preserved: A, X, Y
.macro print_str str,str2
jsr print_str_
.byte str
.ifnblank str2
.byte str2
.endif
.byte 0
.endmacro
print_str_:
sta print_temp_
pla
sta addr
pla
sta addr+1
jsr inc_addr
jsr print_str_addr
lda print_temp_
jmp (addr)
; Prints string at addr and leaves addr pointing to
; byte AFTER zero terminator.
; Preserved: A, X, Y
print_str_addr:
pha
tya
pha
ldy #0
beq :+ ; always taken
@loop: jsr print_char
jsr inc_addr
: lda (addr),y
bne @loop
pla
tay
pla
; FALL THROUGH
; Increments 16-bit value in addr.
; Preserved: A, X, Y
inc_addr:
inc addr
beq :+
rts
: inc addr+1
rts
; Prints A as 1-3 digit decimal value, NO space after.
; Preserved: A, X, Y
print_dec:
pha
sta print_temp_
txa
pha
lda print_temp_
; Hundreds
cmp #10
blt @ones
cmp #100
blt @tens
ldx #'0'-1
: inx
sbc #100
bge :-
adc #100
jsr @digit
; Tens
@tens: sec
ldx #'0'-1
: inx
sbc #10
bge :-
adc #10
jsr @digit
; Ones
@ones: ora #'0'
jsr print_char
pla
tax
pla
rts
; Print a single digit
@digit: pha
txa
jsr print_char
pla
rts
; Prints one of two characters based on condition.
; SEC; print_cc bcs,'C','-' prints 'C'.
; Preserved: A, X, Y, flags
.macro print_cc cond,yes,no
; Avoids labels since they're not local
; to macros in ca65.
php
pha
cond *+6
lda #no
bne *+4
lda #yes
jsr print_char
pla
plp
.endmacro

View File

@@ -0,0 +1,64 @@
; Keeps track of number of times reset, and prompts user.
power_flag_value = $42
nv_res power_flag_
nv_res num_resets_
; Out: A = number of times NES has been reset since turned on
; Preserved: X, Y
num_resets:
lda power_flag_
cmp #power_flag_value
bne :+
lda num_resets_
rts
: lda #0
rts
; Prompts user to press reset after message disappears,
; then hides message, increments reset count, and asks
; emulator to reset NES.
; Preserved: X, Y
prompt_to_reset:
print_str {newline,newline,"Press reset AFTER this message",newline,"disappears"}
; Add "again" if this isn't first requested reset
jsr num_resets
beq :+
print_str ", again"
:
; Show for a few seconds
print_str {newline,newline,newline}
jsr console_show
delay_msec 1000
delay_msec 1000
jsr inc_reset_count
; Tell emulator that NES should be reset now
lda #$81
jsr set_final_result
jsr console_hide
rts
; Increments reset count and marks it as valid
; Preserved: X, Y
inc_reset_count:
jsr num_resets
clc
adc #1
bcc :+
lda #$FF ; don't wrap around
: sta num_resets_
setb power_flag_,power_flag_value
rts
; Waits in infinite loop for reset
; Preserved: A, X, Y, flags
wait_reset:
jmp wait_reset

View File

@@ -0,0 +1,26 @@
; Included at beginning of program
.macro stp
.byte $02
.endmacro
.include "macros.inc"
.include "neshw.inc"
; .ifdef CUSTOM_PREFIX
; .include "custom_prefix.s"
; .endif
; ; Devcart
; .ifdef BUILD_DEVCART
; .include "build_devcart.s"
; .endif
; NES internal RAM
; .ifdef BUILD_NOCART
; .include "build_nocart.s"
; .endif
; NES ROM (default)
.ifndef SHELL_INCLUDED
.include "build_rom.s"
.endif

View File

@@ -0,0 +1,384 @@
; Common routines and runtime
; Detect inclusion loops (otherwise ca65 goes crazy)
.ifdef SHELL_INCLUDED
.error "shell.s included twice"
.end
.endif
SHELL_INCLUDED = 1
; Temporary variables that ANY routine might modify, so
; only use them between routine calls.
temp = <$A
temp2 = <$B
temp3 = <$C
addr = <$E
ptr = addr
; Move code to $E200 ($200 bytes for text output in devcarts
; where WRAM is mirrored to $E000)
.segment "CODE"
.res $2200
; Put shell code after user code, so user code is in more
; consistent environment
.segment "CODE2"
; Any user code which runs off end might end up here,
; so catch that mistake.
nop ; in case there was three-byte opcode before this
nop
jmp internal_error
;**** Common routines ****
.include "macros.inc"
.include "neshw.inc"
.include "delay.s"
.include "print.s"
.include "crc.s"
.include "testing.s"
.ifdef NEED_CONSOLE
.include "console.s"
.else
; Stubs so code doesn't have to care whether
; console exists
console_init:
console_show:
console_hide:
console_print:
console_flush:
rts
.endif
.ifndef CUSTOM_PRINT
.include "text_out.s"
print_char_:
jsr write_text_out
jmp console_print
stop_capture:
rts
.endif
;**** Shell core ****
.ifndef CUSTOM_RESET
reset:
sei
jmp std_reset
.endif
; Sets up hardware then runs main
run_shell:
sei
cld ; unnecessary on NES, but might help on clone
ldx #$FF
txs
jsr init_shell
set_test $FF
jmp run_main
; Initializes shell
init_shell:
jsr clear_ram
jsr init_wait_vbl ; waits for VBL once here,
jsr wait_vbl_optional ; so only need to wait once more
; jsr init_text_out
jsr init_testing
.byte $02
jsr init_runtime
jsr console_init
rts
; Runs main in consistent PPU/APU environment, then exits
; with code 0
run_main:
jsr pre_main
jsr main
lda #0
jmp exit
; Sets up environment for main to run in
pre_main:
.ifndef BUILD_NSF
jsr disable_rendering
setb PPUCTRL,0
jsr clear_palette
jsr clear_nametable
jsr clear_nametable2
jsr clear_oam
.endif
lda #$34
pha
lda #0
tax
tay
jsr wait_vbl_optional
plp
sta SNDMODE
rts
.ifndef CUSTOM_EXIT
exit:
.endif
; Reports result and ends program
std_exit:
.byte $02
sei
cld
ldx #$FF
txs
ldx #0
stx SNDCHN
.ifndef BUILD_NSF
stx PPUCTRL
.endif
jsr report_result
jmp post_exit
; Reports final result code in A
report_result:
jsr :+
jmp play_byte
: jsr print_newline
jsr console_show
; 0: ""
cmp #1
bge :+
rts
:
; 1: "Failed"
bne :+
print_str {"Failed",newline}
rts
; n: "Failed #n"
: print_str "Failed #"
jsr print_dec
jsr print_newline
rts
;**** Other routines ****
; Reports internal error and exits program
internal_error:
print_str newline,"Internal error"
lda #255
jmp exit
.import __NVRAM_LOAD__, __NVRAM_SIZE__
.macro fill_ram_ Begin, End
.local Neg_size
Neg_size = (Begin) - (End)
ldxy #(Begin) - <Neg_size
sty addr
stx addr+1
ldxy #Neg_size
: sta (addr),y
iny
bne :-
inc addr+1
inx
bne :-
.endmacro
; Clears 0 through ($100+S), $200 through __NVRAM_LOAD__-1, and
; __NVRAM_LOAD__+__NVRAM_SIZE__ through $7FF
clear_ram:
lda #0
bss_begin = $200
fill_ram_ bss_begin,__NVRAM_LOAD__
fill_ram_ __NVRAM_LOAD__+__NVRAM_SIZE__,$800
; Zero-page
tax
: sta 0,x
inx
bne :-
; Stack below S
tsx
inx
: dex
sta $100,x
bne :-
rts
nv_res unused_nv_var ; to avoid size=0
; Clears nvram
clear_nvram:
lda #0
fill_ram_ __NVRAM_LOAD__,__NVRAM_LOAD__+__NVRAM_SIZE__
rts
; Prints filename and newline, if available, otherwise nothing.
; Preserved: A, X, Y
print_filename:
.ifdef FILENAME_KNOWN
pha
jsr print_newline
setw addr,filename
jsr print_str_addr
jsr print_newline
pla
.endif
rts
.pushseg
.segment "RODATA"
; Filename terminated with zero byte.
filename:
.ifdef FILENAME_KNOWN
.incbin "ram:nes_temp"
.endif
.byte 0
.popseg
;**** ROM-specific ****
.ifndef BUILD_NSF
.include "ppu.s"
avoid_silent_nsf:
play_byte:
rts
; Loads ASCII font into CHR RAM
.macro load_ascii_chr
bit PPUSTATUS
setb PPUADDR,$00
setb PPUADDR,$00
setb addr,<ascii_chr
ldx #>ascii_chr
ldy #0
@page:
stx addr+1
: lda (addr),y
sta PPUDATA
iny
bne :-
inx
cpx #>ascii_chr_end
bne @page
.endmacro
; Disables interrupts and loops forever
.ifndef CUSTOM_FOREVER
forever:
sei
lda #0
sta PPUCTRL
: beq :-
.res $10,$EA ; room for code to run loader
.endif
; Default NMI
.ifndef CUSTOM_NMI
zp_byte nmi_count
nmi:
inc nmi_count
rti
; Waits for NMI. Must be using NMI handler that increments
; nmi_count, with NMI enabled.
; Preserved: X, Y
wait_nmi:
lda nmi_count
: cmp nmi_count
beq :-
rts
.endif
; Default IRQ
.ifndef CUSTOM_IRQ
irq:
bit SNDCHN ; clear APU IRQ flag
rti
.endif
.endif
; Reports A in binary as high and low tones, with
; leading low tone for reference. Omits leading
; zeroes. Doesn't hang if no APU is present.
; Preserved: A, X, Y
play_hex:
pha
; Make low reference beep
clc
jsr @beep
; Remove high zero bits
sec
: rol a
bcc :-
; Play remaining bits
beq @zero
: jsr @beep
asl a
bne :-
@zero:
delay_msec 300
pla
rts
; Plays low/high beep based on carry
; Preserved: A, X, Y
@beep:
pha
; Set up square
lda #1
sta SNDCHN
sta $4001
sta $4003
adc #$FE ; period=$100 if carry, $1FF if none
sta $4002
; Fade volume
lda #$0F
: ora #$30
sta $4000
delay_msec 8
sec
sbc #$31
bpl :-
; Silence
sta SNDCHN
delay_msec 160
pla
rts

View File

@@ -0,0 +1,36 @@
; 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
.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
.macro patterns_bin name
.pushseg
.segment "CHARS"
.incbin name
.popseg
.endmacro
.segment "CODE"

View File

@@ -0,0 +1,61 @@
; Text output as expanding zero-terminated string at text_out_base
; The final exit result byte is written here
final_result = $6000
; Text output is written here as an expanding
; zero-terminated string
text_out_base = $6004
bss_res text_out_temp
zp_res text_out_addr,2
init_text_out:
ldx #0
; Put valid data first
setb text_out_base,0
lda #$80
jsr set_final_result
; Now fill in signature that tells emulator there's
; useful data there
setb text_out_base-3,$DE
setb text_out_base-2,$B0
setb text_out_base-1,$61
ldx #>text_out_base
stx text_out_addr+1
setb text_out_addr,<text_out_base
rts
; Sets final result byte in memory
set_final_result:
sta final_result
rts
; Writes character to text output
; In: A=Character to write
; Preserved: A, X, Y
write_text_out:
sty text_out_temp
; Write new terminator FIRST, then new char before it,
; in case emulator looks at string in middle of this routine.
ldy #1
pha
lda #0
sta (text_out_addr),y
dey
pla
sta (text_out_addr),y
inc text_out_addr
bne :+
inc text_out_addr+1
:
ldy text_out_temp
rts

View File

@@ -0,0 +1,17 @@
use super::rom_test;
// TODO: need to implement soft reset behavior
// rom_test!(basic_cpu, "cpu_reset_ram.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.cpu.sp, 0xFD);
// // assert_eq!(nes.cpu.a, 0x00);
// // assert_eq!(nes.cpu.x, 0x00);
// // assert_eq!(nes.cpu.y, 0x00);
// });

View File

@@ -0,0 +1,84 @@
; Verifies that reset doesn't alter any RAM.
CUSTOM_RESET=1
.include "shell.inc"
.include "run_at_reset.s"
nv_res bad_addr,2
reset: sei
cld
ldx #$FF
txs
ldx #7
ldy #0
; Check first byte, and assume just powered
; if not as expected
lda <0
cmp #$DB
jne std_reset
iny
; Check second byte
lda <1
cmp #$B6
bne failed
iny
; Rest of internal memory
setb <0,0
setb <1,0
lda #$6D
clc
: eor (<0),y
bne failed
lda (<0),y
rol a
iny
bne :-
inc <1
dex
bpl :-
jmp tests_passed
failed:
sty bad_addr
txa
eor #$07
sta bad_addr+1
jsr init_shell
print_str "Addr: "
lda bad_addr+1
jsr print_hex
lda bad_addr
jsr print_a
set_test 3,"Reset shouldn't modify RAM"
; stp
main:
jsr prompt_to_reset
; Fill RAM with pattern
setb <0,0
setb <1,0
ldx #8
ldy #2
lda #$6D
clc
: sta (<0),y
rol a
iny
bne :-
inc <1
dex
bne :-
setb <0,$DB
setb <1,$B6
jmp wait_reset

63
src/test_roms/crc_check.s Normal file
View File

@@ -0,0 +1,63 @@
.include "shell.inc"
.include "crc_fast.s"
run_crc:
pha
jsr reset_crc
pla
jsr update_crc_fast
ldy #0
lda checksum
sta ($0),y
iny
lda checksum+1
sta ($0),y
iny
lda checksum+2
sta ($0),y
iny
lda checksum+3
sta ($0),y
rts
.macro crc_check crc_val,val
lda #.lobyte(crc_val)
sta $0
lda #.hibyte(crc_val)
sta $1
lda #val
jsr run_crc
.endmacro
main:
crc_check crc_1,$00
crc_check crc_2,$01
crc_check crc_3,$02
crc_check crc_4,$7F
crc_check crc_5,$80
crc_check crc_6,$81
crc_check crc_7,$FF
lda #$00
jmp exit
.org $20
crc_1: .res 4
crc_2: .res 4
crc_3: .res 4
crc_4: .res 4
crc_5: .res 4
crc_6: .res 4
crc_7: .res 4
crc_8: .res 4
crc_9: .res 4
crc_10: .res 4
crc_11: .res 4
crc_12: .res 4
crc_13: .res 4
crc_14: .res 4
crc_15: .res 4
crc_16: .res 4
crc_17: .res 4
crc_18: .res 4
crc_19: .res 4
crc_20: .res 4

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
@@ -15,7 +11,7 @@ RESET:
stx $2000 ; Disable NMI (by writing zero)
stx $4010 ; Disable DMC IRQs
ldx #$08
stx $2001 ; Disable rendering
stx $2001 ; Enable rendering
bit $2002 ; Clear vblank flag by reading ppu status
VBLANKWAIT1:
@@ -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,130 @@
OPERANDS = 0
.include "shell.inc"
; .include "instr_test.inc"
; .include "macros.inc"
values:
.byte 0,1,2,$40,$7F,$80,$81,$FF
values_size = * - values
zp_res in_a,2
zp_res in_x,2
zp_res in_y,2
zp_res in_p,2
zp_res in_s,2
zp_res out_a
zp_res out_x
zp_res out_y
zp_res out_p
zp_res out_s
.macro store_ptr zp_ptr, value
lda #.lobyte(value)
sta zp_ptr
lda #.hibyte(value)
sta zp_ptr+1
.endmacro
testcase_init:
store_ptr in_a, values
store_ptr in_x, values
store_ptr in_y, values
store_ptr in_p, values
store_ptr in_s, values
rts
; Calling convention: Will trash stack - anything that needs to be saved must be elsewhere
; Will call `test_code` with the requisite
zp_res return_addr
run_tc:
lda (in_s,x)
tax
txs ; As long as the following code has the same number of pushes & pulls, stack ptr will be correct
ldx #0
lda (in_p,x)
pha
lda (in_a,x)
pha
lda (in_x,x)
pha
lda (in_y,x)
pha
; Stack <y, x, a, p>
pla
tay
; Stack <x, a, p>, y correct
pla
tax
; Stack <a, p>, C: x, y
pla
; Stack <p>, C: x, y
plp
; Stack <>, C: x, y, p, (also S)
jsr test_code
php
sta out_a
pla
sta out_p
stx out_x
sty out_y
tsx
stx out_s
jmp (return_addr)
test_code:
rts
main:
jsr testcase_init
; $A2,$67,$77,$F8
; entry $2A,"ROL A",$CB,$4B,$9A,$4C ; A = op A
; entry $0A,"ASL A",$00,$00,$00,$00
; entry $0A,"ROR A",$00,$00,$00,$00
; entry $4A,"LSR A",$00,$00,$00,$00
; entry $8A,"TXA",$00,$00,$00,$00 ; AXY = AXY
; entry $98,"TYA",$00,$00,$00,$00
; entry $AA,"TAX",$00,$00,$00,$00
; entry $A8,"TAY",$00,$00,$00,$00
; entry $E8,"INX",$00,$00,$00,$00 ; XY = op XY
; entry $C8,"INY",$00,$00,$00,$00
; entry $CA,"DEX",$00,$00,$00,$00
; entry $88,"DEY",$00,$00,$00,$00
; entry $38,"SEC",$00,$00,$00,$00 ; flags = op flags
; entry $18,"CLC",$00,$00,$00,$00
; entry $F8,"SED",$00,$00,$00,$00
; entry $D8,"CLD",$00,$00,$00,$00
; entry $78,"SEI",$00,$00,$00,$00
; entry $58,"CLI",$00,$00,$00,$00
; entry $B8,"CLV",$00,$00,$00,$00
; entry $EA,"NOP",$00,$00,$00,$00
lda #$00
jmp exit
; .ifndef OFFICIAL_ONLY
; entry $1A,"NOP"
; entry $3A,"NOP"
; entry $5A,"NOP"
; entry $7A,"NOP"
; entry $DA,"NOP"
; entry $FA,"NOP"
; .endif
; instrs_size = * - instrs
; instr_template:
; nop
; jmp instr_done
; instr_template_size = * - instr_template
; .include "instr_test_end.s"
; test_values:
; test_normal
; rts

335
src/test_roms/input_test.s Normal file
View File

@@ -0,0 +1,335 @@
; 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"
T_EMPTY = 0
.repeat 2 ; Empty 0
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.endrepeat
T_TOP_LEFT = 1
.repeat 2 ; Top Left 1
.byte %11111111
.byte %11111111
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.endrepeat
T_TOP_RIGHT = 2
.repeat 2 ; Top Right 2
.byte %11111111
.byte %11111111
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.endrepeat
T_BOTTOM_RIGHT = 3
.repeat 2 ; Bottom Right 3
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %11111111
.byte %11111111
.endrepeat
T_BOTTOM_LEFT = 4
.repeat 2 ; Bottom Left 4
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11111111
.byte %11111111
.endrepeat
T_FORWARD = 5
.repeat 2 ; FW 5
.byte %00000011
.byte %00000111
.byte %00001110
.byte %00011100
.byte %00111000
.byte %01110000
.byte %11100000
.byte %11000000
.endrepeat
T_BACKWARD = 6
.repeat 2 ; BW 6
.byte %11000000
.byte %11100000
.byte %01110000
.byte %00111000
.byte %00011100
.byte %00001110
.byte %00000111
.byte %00000011
.endrepeat
T_PILL_L = 7
.repeat 2 ; Pill L 7
.byte %00111111
.byte %01111111
.byte %11100000
.byte %11000000
.byte %11000000
.byte %11100000
.byte %01111111
.byte %00111111
.endrepeat
T_PILL_R = 8
.repeat 2 ; Pill R 8
.byte %11111100
.byte %11111110
.byte %00000111
.byte %00000011
.byte %00000011
.byte %00000111
.byte %11111110
.byte %11111100
.endrepeat
.popseg
zp_res TEMP
.macro load_ppu_addr addr
lda #.hibyte(addr)
sta PPUADDR
lda #.lobyte(addr)
sta PPUADDR ; Load $2000
.endmacro
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
; Fill NT0
load_ppu_addr $2000
ldx #$00
ldy #$04
lda #$00
fill_loop:
sta PPUDATA
dex
bne fill_loop
dey
bne fill_loop
; Set specific tiles
load_ppu_addr $2184
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21A4
lda #T_BACKWARD
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21E2
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C6
lda #T_FORWARD
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21E6
lda #T_BACKWARD
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $2204
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $2224
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $21EA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
lda #$00
sta PPUDATA
sta PPUDATA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21D2
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21F2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21D6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21F6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $3F00
lda #$0f
sta PPUDATA
lda #$20
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$09
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK
lda #$80
sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses
loop:
jmp loop
.macro shl_a count
.scope
ldx #count
a_l:asl A
dex
bne a_l
.endscope
.endmacro
nmi:
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
lda #$01
sta JOY1
lda #$00
sta JOY1
load_ppu_addr $23DC ; A
lda JOY1
and #$01
shl_a 6
sta PPUDATA
load_ppu_addr $23DD ; B
lda JOY1
and #$01
shl_a 6
sta PPUDATA
load_ppu_addr $23DA ; Select
lda JOY1
and #$01
shl_a 6
sta PPUDATA
load_ppu_addr $23DB ; Start
lda JOY1
and #$01
shl_a 6
sta PPUDATA
lda JOY1 ; Up
and #$01
sta TEMP
load_ppu_addr $23E1 ; Down
lda JOY1
and #$01
sta PPUDATA
load_ppu_addr $23D8 ; Left
lda JOY1
and #$01
shl_a 6
sta PPUDATA
load_ppu_addr $23D9 ; Right
lda JOY1
and #$01
shl_a 6
ora TEMP
sta PPUDATA
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK ; Enable rendering
rti
exit:
stp
irq:
stp

View File

@@ -0,0 +1,68 @@
use std::fmt;
use crate::NES;
use super::rom_test;
enum HexDump<'a> {
NES(&'a NES, u16, usize),
Lit(&'a [u8]),
}
impl HexDump<'_> {
fn get(&self, off: usize) -> Option<u8> {
match self {
Self::NES(_, _, len) if off >= *len => None,
Self::NES(nes, addr, _) => nes.mem().peek_cpu(addr + off as u16),
Self::Lit(items) => items.get(off).copied(),
}
}
}
impl fmt::Display for HexDump<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut i = 0;
while let Some(val) = self.get(i) {
if i % 16 == 0 {
write!(f, "\n{i:04X}:")?;
}
write!(f, " {val:02X}")?;
i += 1;
}
Ok(())
}
}
fn mem_cmp(nes: &NES, addr: u16, vals: &[u8]) {
for (i, v) in vals.iter().enumerate() {
if nes.mem().peek_cpu(addr + i as u16) != Some(*v) {
panic!(
"memcmp assertion failed:\nNES:{}\nTest:{}",
HexDump::NES(nes, addr, vals.len()),
HexDump::Lit(vals)
);
}
}
}
rom_test!(crc_check, "crc_check.nes", |nes| {
assert_eq!(nes.last_instruction(), "0x8026 HLT :2 []");
assert_eq!(nes.cpu.a, 0x00);
mem_cmp(&nes, 0x20, &[0xFF, 0xFF, 0xFF, 0xFF]);
mem_cmp(&nes, 0x24, &[0x69, 0xCF, 0xF8, 0x88]);
mem_cmp(&nes, 0x28, &[0xD3, 0x9E, 0xF1, 0x11]);
mem_cmp(&nes, 0x2C, &[0x52, 0x93, 0x45, 0x3F]);
mem_cmp(&nes, 0x30, &[0xDF, 0x7C, 0x47, 0x12]);
mem_cmp(&nes, 0x34, &[0x49, 0x4C, 0x40, 0x65]);
mem_cmp(&nes, 0x38, &[0x72, 0x10, 0xFD, 0xD2]);
});
rom_test!(implied_instructions, "implied_instrs.nes", |nes| {
assert_eq!(nes.last_instruction(), "0x8026 HLT :2 []");
assert_eq!(nes.cpu.a, 0x00);
});
rom_test!(bit, "alu_bit.nes", |nes| {
assert_eq!(nes.last_instruction(), "0x8021 HLT :2 []");
mem_cmp(&nes, 0x300, &[0xF6, 0xF6]);
});

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,25 @@
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.cpu_cycle(), 86967);
assert_eq!(nes.ppu().pixel, 40);
assert_eq!(nes.ppu().scanline, 241);
assert_eq!(nes.ppu().cycle, 260905);
assert_eq!(nes.mem().peek_cpu(0x1FF), Some(0x80));
assert_eq!(nes.mem().peek_cpu(0x1FE), Some(0x17));
assert_eq!(nes.mem().peek_cpu(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.cpu_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.mem().peek_cpu(0x1FF), Some(0x80));
assert_eq!(nes.mem().peek_cpu(0x1FE), Some(0x1B));
assert_eq!(nes.mem().peek_cpu(0x1FD), Some(0x26));
});

View File

@@ -1,15 +1,18 @@
use crate::{NES, hex_view::Memory};
mod cpu_reset_ram;
mod instructions;
mod ppu;
mod interrupts;
macro_rules! rom_test {
($name:ident, $rom:literal, |$nes:ident| $eval:expr) => {
rom_test!($name, $rom, timeout = 10000000, |$nes| $eval);
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 = concat!(env!("ROM_DIR"), "/", $rom);
println!("{}: {}", stringify!($name), rom_file);
let mut $nes = NES::load_nes_file(rom_file).expect("Failed to create nes object");
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
@@ -23,7 +26,7 @@ macro_rules! rom_test {
fn $name() {
let rom_file = $rom;
println!("{}: {}", stringify!($name), rom_file);
let mut $nes = NES::load_nes_file(rom_file).expect("Failed to create nes object");
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
@@ -31,39 +34,55 @@ macro_rules! rom_test {
};
}
pub(crate) use rom_test;
use crate::{Break, NES};
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.last_instruction(), "0x8001 HLT :2 []");
assert_eq!(nes.cpu_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.cpu_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.cpu_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.cpu_cycle(), 25);
assert_eq!(nes.cpu.pc, 0x800C);
assert_eq!(nes.cpu.sp, 0xFD);
assert_eq!(nes.cpu.a, 0xAA);
assert_eq!(nes.cpu.x, 0xAA);
assert_eq!(nes.cpu.y, 0xAA);
assert_eq!(nes.cpu_mem().peek(0x0000).unwrap(), 0xAA);
assert_eq!(nes.cpu_mem().peek(0x0001).unwrap(), 0xAA);
assert_eq!(nes.cpu_mem().peek(0x0002).unwrap(), 0xAA);
assert_eq!(nes.mem().peek_cpu(0x0000).unwrap(), 0xAA);
assert_eq!(nes.mem().peek_cpu(0x0001).unwrap(), 0xAA);
assert_eq!(nes.mem().peek_cpu(0x0002).unwrap(), 0xAA);
});
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.last_instruction(), "0x8017 HLT :2 []");
assert_eq!(nes.cpu_cycle(), 40);
assert_eq!(nes.cpu.pc, 0x8017);
assert_eq!(nes.cpu.sp, 0xFF);
assert_eq!(nes.cpu.a, 0x00);
@@ -72,33 +91,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.cpu.sp, 0xFF);
assert_eq!(nes.cpu.a, 0x00);
assert_eq!(nes.cpu.x, 0x00);
assert_eq!(nes.cpu.y, 0x00);
});
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.cpu.sp, 0xFF);
assert_eq!(nes.cpu.a, 0x00);
assert_eq!(nes.cpu.x, 0x00);
assert_eq!(nes.cpu.y, 0x00);
});
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.last_instruction(), "0x801C HLT :2 []");
assert_eq!(nes.cpu_cycle(), 27402);
assert_eq!(nes.cpu.pc, 0x801C);
assert_eq!(nes.ppu.pixel, 29);
assert_eq!(nes.cpu.sp, 0xFF);
@@ -107,11 +102,35 @@ rom_test!(basic_init_3, "basic_init_3.nes", |nes| {
assert_eq!(nes.cpu.y, 0x00);
});
rom_test!(basic_init_2, "basic_init_2.nes", |nes| {
assert_eq!(nes.last_instruction(), "0x8021 HLT :2 []");
assert_eq!(nes.cpu_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);
assert_eq!(nes.cpu.x, 0x00);
assert_eq!(nes.cpu.y, 0x00);
});
rom_test!(basic_init_3, "basic_init_3.nes", |nes| {
assert_eq!(nes.last_instruction(), "0x8026 HLT :2 []");
assert_eq!(nes.cpu_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);
assert_eq!(nes.cpu.x, 0x00);
assert_eq!(nes.cpu.y, 0x00);
});
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.last_instruction(), "0x8023 HLT :2 []");
assert_eq!(nes.cpu_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);
@@ -119,9 +138,70 @@ rom_test!(even_odd, "even_odd.nes", |nes| {
assert_eq!(nes.cpu.y, 0x00);
});
fn run_frame(nes: &mut NES) {
nes.run_one_clock_cycle(&Break::default());
while nes.ppu().scanline != 241 || nes.ppu().pixel != 0 {
nes.run_one_clock_cycle(&Break::default());
}
}
rom_test!(input_test, "input_test.nes", timeout = 86964*3, |nes| {
const A: u16 = 0x23DC; // 0 || 0b01000000
const B: u16 = 0x23DD; // 0 || 0b01000000
const SELECT: u16 = 0x23DA; // 0 || 0b01000000
const START: u16 = 0x23DB; // 0 || 0b01000000
const DOWN: u16 = 0x23E1; // 0 || 1
const LEFT: u16 = 0x23D8; // 0 || 0b01000000
const UP_RIGHT: u16 = 0x23D9; // 0 || 0b01000000 | 1
run_frame(&mut nes);
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
nes.controller_1().set_a(true);
run_frame(&mut nes);
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
nes.controller_1().set_b(true);
run_frame(&mut nes);
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
nes.controller_1().0 = 0xFF;
run_frame(&mut nes);
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0x01);
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0x40);
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0x41);
nes.controller_1().0 = 0;
run_frame(&mut nes);
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
});
// rom_test!(even_odd, "even_odd.nes", |nes| {
// assert_eq!(nes.last_instruction, "0x8023 HLT :2 []");
// assert_eq!(nes.cycle, 57182);
// assert_eq!(nes.last_instruction(), "0x8023 HLT :2 []");
// assert_eq!(nes.cpu_cycle(), 57182);
// assert_eq!(nes.cpu.pc, 0x8024);
// assert_eq!(nes.ppu.pixel, 25);

36
src/test_roms/nes.cfg Normal file
View File

@@ -0,0 +1,36 @@
# 32K iNES ROM with optional 8K CHR
MEMORY
{
ZP: start = $10, size = $E0; # leave $10 free at each end
RAM: start = $200, size = $500;
HEADER: start = 0, size = $10, fill=yes;
ROM: start = $8000, size = $7E00, fill=yes, fillval=$FF;
LOADER: start = $FE00, size = $100, fill=yes, fillval=$FF;
FF00: start = $FF00, size = $F4, fill=yes, fillval=$FF;
VECTORS:start = $FFF4, size = $C, fill=yes;
CHARS: start = 0, size = $3000, fill=yes, fillval=$FF;
}
SEGMENTS
{
ZEROPAGE: load = ZP, type = zp;
BSS: load = RAM, type = bss,align=$100;
NVRAM: load = RAM, type = bss,define=yes, optional=yes;
HEADER: load = HEADER, type = ro;
CODE: load = ROM, type = ro, align=$100;
CODE2: load = ROM, type = ro, align=$100, optional=yes;
RODATA: load = ROM, type = ro, align=$100;
CHARS_PRG: load = ROM, type = ro, align=$200, optional=yes;
CHARS_PRG_ASCII:load = ROM, type = ro, align=$200, optional=yes;
LOADER: load = LOADER, type = ro, optional=yes;
FF00: load = FF00, type = ro, align=$100, optional=yes;
VECTORS: load = VECTORS, type = ro;
CHARS: load = CHARS, type = ro, align=$100, optional=yes;
CHARS_ASCII:load = CHARS, type = ro, align=$200, optional=yes;
}

BIN
src/test_roms/pat.bin Normal file

Binary file not shown.

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

@@ -0,0 +1,101 @@
use crate::{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.mem().peek_ppu(0x2000), Ok(1));
assert_eq!(nes.mem().peek_ppu(0x2020), Ok(1));
assert_eq!(nes.mem().peek_ppu(0x2400), Ok(2));
assert_eq!(nes.mem().peek_ppu(0x2401), Ok(2));
});
rom_test!(ppu_palette_shared, "ppu_palette_shared.nes", |nes| {
assert_eq!(nes.last_instruction(), "0x8078 HLT :2 []");
});
// Sets up an image, and scrolls a specific number of pixels over
rom_test!(ppu_scrolling, "ppu_fine_x_scrolling.nes", timeout = 86964*4, |nes| {
assert_eq!(nes.image().read(0, 0), Color { r: 0xFF, g: 0xFE, b: 0xFF });
});

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,306 @@
.include "testing.s"
; patterns_bin "pat.bin"
.macro chr b,s
.repeat s
.byte b
.endrepeat
.endmacro
.pushseg
.segment "CHARS"
T_EMPTY = 0
.repeat 2 ; Empty 0
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.endrepeat
T_TOP_LEFT = 1
.repeat 2 ; Top Left 1
.byte %11111111
.byte %11111111
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.endrepeat
T_TOP_RIGHT = 2
.repeat 2 ; Top Right 2
.byte %11111111
.byte %11111111
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.endrepeat
T_BOTTOM_RIGHT = 3
.repeat 2 ; Bottom Right 3
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %11111111
.byte %11111111
.endrepeat
T_BOTTOM_LEFT = 4
.repeat 2 ; Bottom Left 4
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11111111
.byte %11111111
.endrepeat
T_FORWARD = 5
.repeat 2 ; FW 5
.byte %00000011
.byte %00000111
.byte %00001110
.byte %00011100
.byte %00111000
.byte %01110000
.byte %11100000
.byte %11000000
.endrepeat
T_BACKWARD = 6
.repeat 2 ; BW 6
.byte %11000000
.byte %11100000
.byte %01110000
.byte %00111000
.byte %00011100
.byte %00001110
.byte %00000111
.byte %00000011
.endrepeat
T_PILL_L = 7
.repeat 2 ; Pill L 7
.byte %00111111
.byte %01111111
.byte %11100000
.byte %11000000
.byte %11000000
.byte %11100000
.byte %01111111
.byte %00111111
.endrepeat
T_PILL_R = 8
.repeat 2 ; Pill R 8
.byte %11111100
.byte %11111110
.byte %00000111
.byte %00000011
.byte %00000011
.byte %00000111
.byte %11111110
.byte %11111100
.endrepeat
T_LINE = 9
.repeat 2 ; Empty 0
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.endrepeat
.popseg
zp_res Y_SCROLL
zp_res X_SCROLL
zp_res CTRL_BYTE
.macro load_ppu_addr addr
lda #.hibyte(addr)
sta PPUADDR
lda #.lobyte(addr)
sta PPUADDR ; Load $2000
.endmacro
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
; Fill NT0
load_ppu_addr $2000
ldx #$00
ldy #$04
lda #$00
fill_loop:
sta PPUDATA
dex
bne fill_loop
dey
bne fill_loop
; Set specific tiles
load_ppu_addr $2184
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21A4
lda #T_BACKWARD
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21E2
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C6
lda #T_FORWARD
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21E6
lda #T_BACKWARD
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $2204
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $2224
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $21EA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
lda #$00
sta PPUDATA
sta PPUDATA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21D2
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21F2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21D6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21F6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $3F00
lda #$0f
sta PPUDATA
lda #$20
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$09
sta PPUDATA
sta PPUDATA
sta PPUDATA
load_ppu_addr $2001
lda #T_LINE
sta PPUDATA
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK
lda #$80
sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses
sta CTRL_BYTE
loop:
jmp loop
nmi:
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
lda #$0A
; adc X_SCROLL
; sta X_SCROLL
sta PPUSCROLL
; bcc y_scroll
; lda #$01
; eor CTRL_BYTE
; sta CTRL_BYTE
; sta PPUCTRL
; y_scroll:
; clc
lda #$00
; adc Y_SCROLL
; sta Y_SCROLL
sta PPUSCROLL
lda CTRL_BYTE
sta PPUCTRL
lda #%00001110
sta PPUMASK ; Enable rendering
rti
exit:
stp
irq:
stp

View File

@@ -0,0 +1,70 @@
.include "testing.s"
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 $4010 ; Disable DMC IRQs
ldx #$08
stx $2001 ; Disable rendering
bit $2002 ; Clear vblank flag by reading ppu status
VBLANKWAIT1:
bit $2002
bpl VBLANKWAIT1
VBLANKWAIT2:
bit $2002
bpl VBLANKWAIT2
lda #$3F
sta PPUADDR
lda #$00
sta PPUADDR
lda #$01
ldx #$10
fill_loop:
sta PPUDATA
dex
bne fill_loop
lda #$3F
sta PPUADDR
lda #$10
sta PPUADDR
lda PPUDATA
cmp #$01
bne fail
lda PPUDATA
lda PPUDATA
lda PPUDATA
lda PPUDATA
cmp #$01
bne fail
lda PPUDATA
lda PPUDATA
lda PPUDATA
lda PPUDATA
cmp #$01
bne fail
lda PPUDATA
lda PPUDATA
lda PPUDATA
lda PPUDATA
cmp #$01
bne fail
stp
jmp $8000
fail:
stp
nmi:
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

View File

@@ -0,0 +1,96 @@
.include "testing.s"
.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 CUR
zp_res POS
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 $4010 ; Disable DMC IRQs
ldx #$0A
stx $2001 ; Disable rendering
bit $2002 ; Clear vblank flag by reading ppu status
VBLANKWAIT1:
bit $2002
bpl VBLANKWAIT1
VBLANKWAIT2:
bit $2002
bpl VBLANKWAIT2
lda #$1
sta CUR
lda #$0
sta POS
lda #$80
sta PPUCTRL
loop:
jmp loop
nmi:
lda #$20
sta PPUADDR ; MSB
lda POS
adc #$1
sta POS
sta PPUADDR
bne update
clc
lda CUR
adc #$1
sta CUR
cmp #$7
bne update
lda #$0
sta CUR
update:
lda CUR
sta PPUDATA
lda #$0
sta PPUSCROLL
sta PPUSCROLL
lda #$80
sta PPUCTRL
rti
irq:
stp

306
src/test_roms/scrolling.s Normal file
View File

@@ -0,0 +1,306 @@
.include "testing.s"
; patterns_bin "pat.bin"
.macro chr b,s
.repeat s
.byte b
.endrepeat
.endmacro
.pushseg
.segment "CHARS"
T_EMPTY = 0
.repeat 2 ; Empty 0
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.endrepeat
T_TOP_LEFT = 1
.repeat 2 ; Top Left 1
.byte %11111111
.byte %11111111
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.endrepeat
T_TOP_RIGHT = 2
.repeat 2 ; Top Right 2
.byte %11111111
.byte %11111111
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.endrepeat
T_BOTTOM_RIGHT = 3
.repeat 2 ; Bottom Right 3
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %11111111
.byte %11111111
.endrepeat
T_BOTTOM_LEFT = 4
.repeat 2 ; Bottom Left 4
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11111111
.byte %11111111
.endrepeat
T_FORWARD = 5
.repeat 2 ; FW 5
.byte %00000011
.byte %00000111
.byte %00001110
.byte %00011100
.byte %00111000
.byte %01110000
.byte %11100000
.byte %11000000
.endrepeat
T_BACKWARD = 6
.repeat 2 ; BW 6
.byte %11000000
.byte %11100000
.byte %01110000
.byte %00111000
.byte %00011100
.byte %00001110
.byte %00000111
.byte %00000011
.endrepeat
T_PILL_L = 7
.repeat 2 ; Pill L 7
.byte %00111111
.byte %01111111
.byte %11100000
.byte %11000000
.byte %11000000
.byte %11100000
.byte %01111111
.byte %00111111
.endrepeat
T_PILL_R = 8
.repeat 2 ; Pill R 8
.byte %11111100
.byte %11111110
.byte %00000111
.byte %00000011
.byte %00000011
.byte %00000111
.byte %11111110
.byte %11111100
.endrepeat
T_LINE = 9
.repeat 2 ; Empty 0
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.endrepeat
.popseg
zp_res Y_SCROLL
zp_res X_SCROLL
zp_res CTRL_BYTE
.macro load_ppu_addr addr
lda #.hibyte(addr)
sta PPUADDR
lda #.lobyte(addr)
sta PPUADDR ; Load $2000
.endmacro
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
; Fill NT0
load_ppu_addr $2000
ldx #$00
ldy #$04
lda #$00
fill_loop:
sta PPUDATA
dex
bne fill_loop
dey
bne fill_loop
; Set specific tiles
load_ppu_addr $2184
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21A4
lda #T_BACKWARD
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21E2
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C6
lda #T_FORWARD
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21E6
lda #T_BACKWARD
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $2204
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $2224
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $21EA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
lda #$00
sta PPUDATA
sta PPUDATA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21D2
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21F2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21D6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21F6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $3F00
lda #$0f
sta PPUDATA
lda #$20
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$09
sta PPUDATA
sta PPUDATA
sta PPUDATA
load_ppu_addr $2001
lda #T_LINE
sta PPUDATA
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK
lda #$80
sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses
sta CTRL_BYTE
loop:
jmp loop
nmi:
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
lda #$0A
adc X_SCROLL
sta X_SCROLL
sta PPUSCROLL
bcc y_scroll
lda #$01
eor CTRL_BYTE
sta CTRL_BYTE
sta PPUCTRL
y_scroll:
clc
lda #$00
adc Y_SCROLL
sta Y_SCROLL
sta PPUSCROLL
lda CTRL_BYTE
sta PPUCTRL
lda #%00001110
sta PPUMASK ; Enable rendering
rti
exit:
stp
irq:
stp

View File

@@ -0,0 +1,334 @@
.include "testing.s"
; patterns_bin "pat.bin"
.macro chr b,s
.repeat s
.byte b
.endrepeat
.endmacro
.pushseg
.segment "CHARS"
T_EMPTY = 0
.repeat 2 ; Empty 0
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.endrepeat
T_TOP_LEFT = 1
.repeat 2 ; Top Left 1
.byte %11111111
.byte %11111111
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.endrepeat
T_TOP_RIGHT = 2
.repeat 2 ; Top Right 2
.byte %11111111
.byte %11111111
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.endrepeat
T_BOTTOM_RIGHT = 3
.repeat 2 ; Bottom Right 3
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %11111111
.byte %11111111
.endrepeat
T_BOTTOM_LEFT = 4
.repeat 2 ; Bottom Left 4
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11111111
.byte %11111111
.endrepeat
T_FORWARD = 5
.repeat 2 ; FW 5
.byte %00000011
.byte %00000111
.byte %00001110
.byte %00011100
.byte %00111000
.byte %01110000
.byte %11100000
.byte %11000000
.endrepeat
T_BACKWARD = 6
.repeat 2 ; BW 6
.byte %11000000
.byte %11100000
.byte %01110000
.byte %00111000
.byte %00011100
.byte %00001110
.byte %00000111
.byte %00000011
.endrepeat
T_PILL_L = 7
.repeat 2 ; Pill L 7
.byte %00111111
.byte %01111111
.byte %11100000
.byte %11000000
.byte %11000000
.byte %11100000
.byte %01111111
.byte %00111111
.endrepeat
T_PILL_R = 8
.repeat 2 ; Pill R 8
.byte %11111100
.byte %11111110
.byte %00000111
.byte %00000011
.byte %00000011
.byte %00000111
.byte %11111110
.byte %11111100
.endrepeat
T_LINE = 9
.repeat 2 ; Line
.byte %00000000
.byte %11111111
.byte %11111111
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.endrepeat
.popseg
zp_res Y_SCROLL
zp_res X_SCROLL
zp_res CTRL_BYTE
.macro load_ppu_addr addr
lda #.hibyte(addr)
sta PPUADDR
lda #.lobyte(addr)
sta PPUADDR ; Load $2000
.endmacro
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
; Fill NT0
load_ppu_addr $2000
ldx #$00
ldy #$04
lda #$00
fill_loop:
sta PPUDATA
dex
bne fill_loop
dey
bne fill_loop
; Set specific tiles
load_ppu_addr $2184
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21A4
lda #T_BACKWARD
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21E2
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C6
lda #T_FORWARD
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21E6
lda #T_BACKWARD
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $2204
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $2224
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $21EA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
lda #$00
sta PPUDATA
sta PPUDATA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21D2
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21F2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21D6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21F6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $3F00
lda #$0f
sta PPUDATA
lda #$20
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$09
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$1A
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$21
sta PPUDATA
sta PPUDATA
sta PPUDATA
load_ppu_addr $2000
lda #T_TOP_LEFT
ldx #$1F
line_loop:
sta PPUDATA
dex
bne line_loop
load_ppu_addr $23C0
ldx #$7
line_color_loop:
lda #%00000000
sta PPUDATA
lda #%00001111
sta PPUDATA
dex
bne line_color_loop
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK
lda #$80
sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses
sta CTRL_BYTE
loop:
jmp loop
nmi:
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
lda #$01
adc X_SCROLL
sta X_SCROLL
sta PPUSCROLL
bcc y_scroll
lda #$01
eor CTRL_BYTE
sta CTRL_BYTE
sta PPUCTRL
y_scroll:
clc
lda #$00
adc Y_SCROLL
sta Y_SCROLL
sta PPUSCROLL
lda CTRL_BYTE
sta PPUCTRL
lda #%00001110
sta PPUMASK ; Enable rendering
rti
exit:
stp
irq:
stp

321
src/test_roms/sprites.s Normal file
View File

@@ -0,0 +1,321 @@
.include "testing.s"
.pushseg
.segment "CHARS"
T_EMPTY = 0
.repeat 2 ; Empty 0
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.endrepeat
T_TOP_LEFT = 1
.repeat 2 ; Top Left 1
.byte %11111111
.byte %11111111
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.endrepeat
T_TOP_RIGHT = 2
.repeat 2 ; Top Right 2
.byte %11111111
.byte %11111111
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.endrepeat
T_BOTTOM_RIGHT = 3
.repeat 2 ; Bottom Right 3
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %00000011
.byte %11111111
.byte %11111111
.endrepeat
T_BOTTOM_LEFT = 4
.repeat 2 ; Bottom Left 4
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11000000
.byte %11111111
.byte %11111111
.endrepeat
T_FORWARD = 5
.repeat 2 ; FW 5
.byte %00000011
.byte %00000111
.byte %00001110
.byte %00011100
.byte %00111000
.byte %01110000
.byte %11100000
.byte %11000000
.endrepeat
T_BACKWARD = 6
.repeat 2 ; BW 6
.byte %11000000
.byte %11100000
.byte %01110000
.byte %00111000
.byte %00011100
.byte %00001110
.byte %00000111
.byte %00000011
.endrepeat
T_PILL_L = 7
.repeat 2 ; Pill L 7
.byte %00111111
.byte %01111111
.byte %11100000
.byte %11000000
.byte %11000000
.byte %11100000
.byte %01111111
.byte %00111111
.endrepeat
T_PILL_R = 8
.repeat 2 ; Pill R 8
.byte %11111100
.byte %11111110
.byte %00000111
.byte %00000011
.byte %00000011
.byte %00000111
.byte %11111110
.byte %11111100
.endrepeat
T_LINE = 9
.repeat 2 ; Empty 0
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.byte %00100000
.endrepeat
.popseg
zp_res Y_SCROLL
zp_res X_SCROLL
zp_res CTRL_BYTE
.macro load_ppu_addr addr
lda #.hibyte(addr)
sta PPUADDR
lda #.lobyte(addr)
sta PPUADDR ; Load $2000
.endmacro
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
; Fill NT0
load_ppu_addr $2000
ldx #$00
ldy #$04
lda #$00
fill_loop:
sta PPUDATA
dex
bne fill_loop
dey
bne fill_loop
; Set specific tiles
load_ppu_addr $2184
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21A4
lda #T_BACKWARD
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21E2
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_FORWARD
sta PPUDATA
load_ppu_addr $21C6
lda #T_FORWARD
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21E6
lda #T_BACKWARD
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $2204
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $2224
lda #T_BOTTOM_LEFT
sta PPUDATA
lda #T_BOTTOM_RIGHT
sta PPUDATA
load_ppu_addr $21EA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
lda #$00
sta PPUDATA
sta PPUDATA
lda #T_PILL_L
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21D2
lda #T_FORWARD
sta PPUDATA
lda #T_BACKWARD
sta PPUDATA
load_ppu_addr $21F2
lda #T_TOP_LEFT
sta PPUDATA
lda #T_TOP_RIGHT
sta PPUDATA
load_ppu_addr $21D6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $21F6
lda #T_TOP_LEFT
sta PPUDATA
lda #T_PILL_R
sta PPUDATA
load_ppu_addr $3F00
lda #$0f
sta PPUDATA
lda #$20
sta PPUDATA
sta PPUDATA
sta PPUDATA
lda #$0f
sta PPUDATA
lda #$09
sta PPUDATA
sta PPUDATA
sta PPUDATA
load_ppu_addr $2001
lda #T_LINE
sta PPUDATA
ldx #$00
sprite_loop:
lda sprite_data,x
sta $300,x
inx
cpx #(sprite_data_end - sprite_data)
bne sprite_loop
lda #$00
sta PPUSCROLL
sta PPUSCROLL
lda #%00001110
sta PPUMASK
lda #$80
sta PPUCTRL ; NMI on, PPU slave, Small sprites, 0 addresses
sta CTRL_BYTE
loop:
jmp loop
nmi:
lda PPUSTATUS
lda #%00000110
sta PPUMASK ; Force blank
lda #$01
adc X_SCROLL
sta X_SCROLL
sta PPUSCROLL
bcc y_scroll
lda #$01
eor CTRL_BYTE
sta CTRL_BYTE
sta PPUCTRL
y_scroll:
clc
lda #$00
adc Y_SCROLL
sta Y_SCROLL
sta PPUSCROLL
lda #$00
sta SPRADDR
lda #$03
sta SPRDMA
lda CTRL_BYTE
sta PPUCTRL
lda #%00011110
sta PPUMASK ; Enable rendering
rti
exit:
stp
irq:
stp
sprite_data:
.byte $00
.byte T_PILL_R
.byte $00
.byte $00
sprite_data_end:

View File

@@ -1,9 +1,9 @@
; FINAL = ""
.inesprg 2 ; 2 banks
.ineschr 1 ;
.inesmap 0 ; mapper 0 = NROM
.inesmir 0 ; background mirroring, horizontal
; .inesprg 2 ; 2 banks
; .ineschr 1 ;
; .inesmap 0 ; mapper 0 = NROM
; .inesmir 0 ; background mirroring, horizontal
.org $8000
RESET: