Compare commits

..

7 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
34 changed files with 4891 additions and 331 deletions

1
.gitignore vendored
View File

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

146
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"
@@ -2006,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"
@@ -2196,8 +2277,12 @@ name = "nes-emu"
version = "0.1.0"
dependencies = [
"bitfield",
"bytes",
"cpal",
"iced",
"iced_core",
"rfd",
"ringbuf",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -2401,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"
@@ -2436,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"
@@ -2466,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",
]
@@ -3214,6 +3349,17 @@ 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,6 +7,7 @@ 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"] }
@@ -14,3 +15,6 @@ 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,5 +1,70 @@
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);
@@ -20,24 +85,23 @@ bitfield::bitfield! {
shift, set_shift: 2, 0;
}
bitfield::bitfield! {
#[derive(Clone, Copy, PartialEq, Eq)]
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,
timer_low: u8,
length_timer_high: LengthTimerHigh,
sweep_reload: bool,
cur_time: u16,
cur_length: u8,
counter: LengthCounter,
period: u16,
period_timer: u16,
cur: u8,
sample: u8,
envelope_start: bool,
envelope_counter: u8,
envelope_divider: u8,
}
impl PulseChannel {
@@ -46,51 +110,152 @@ impl PulseChannel {
enabled: false,
duty_vol: DutyVol(0),
sweep: Sweep(0),
timer_low: 0,
length_timer_high: LengthTimerHigh(0),
cur_time: 0,
cur_length: 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,
}
}
fn use_envelope(&self) -> bool {
!self.duty_vol.const_vol()
}
fn volume(&self) -> u8 {
self.duty_vol.volume()
}
fn timer(&self) -> u16 {
self.timer_low as u16 | ((self.length_timer_high.timer_high() as u16) << 8)
}
fn length(&self) -> u8 {
self.length_timer_high.length()
}
pub fn reset(&mut self) {
// TODO
self.cur_time = self.timer();
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!(),
}
}
pub fn clock(&mut self) {
if self.cur_time == 0 {
self.cur_time = self.timer();
fn volume(&self) -> u8 {
if self.duty_vol.const_vol() {
self.duty_vol.volume()
} else {
self.cur_time -= 1;
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.duty_vol.length_counter_halt() && self.cur_length > 0 {
self.cur_length -= 1;
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) {
if !self.duty_vol.length_counter_halt() && self.cur_length > 0 {
self.cur_length -= 1;
}
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;
@@ -99,20 +264,250 @@ bitfield::bitfield! {
#[derive(Debug, Clone)]
struct TriangleChannel {
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)]
@@ -131,6 +526,8 @@ pub struct APU {
noise: NoiseChannel,
dmc: DeltaChannel,
frame_counter: FrameCounter,
samples: Vec<u8>,
}
impl std::fmt::Debug for APU {
@@ -149,17 +546,24 @@ impl APU {
Self {
pulse_1: PulseChannel::new(),
pulse_2: PulseChannel::new(),
triangle: TriangleChannel { enabled: false },
noise: NoiseChannel { enabled: false },
dmc: DeltaChannel { enabled: false },
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 {
@@ -177,44 +581,16 @@ impl APU {
_ => 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 => self.pulse_1.duty_vol.0 = val,
0x01 => self.pulse_1.sweep.0 = val,
0x02 => self.pulse_1.timer_low = val,
0x03 => {
self.pulse_1.length_timer_high.0 = val;
self.pulse_1.reset();
}
0x04 => self.pulse_2.duty_vol.0 = val,
0x05 => self.pulse_2.sweep.0 = val,
0x06 => self.pulse_2.timer_low = val,
0x07 => {
self.pulse_2.length_timer_high.0 = val;
self.pulse_2.reset();
}
0x09 => (), // Unused, technically noise channel?
0x08 | 0x0A | 0x0B => {
// TODO: Triangle channel
}
0x0D => (), // Unused, technically noise channel?
0x0C | 0x0E | 0x0F => {
// TODO: Noise channel
}
0x10 => {
assert_eq!(val, 0x00);
// TODO: implement this value
}
0x11 => {
// TODO: load dmc counter with (val & 7F)
}
0x12 => {
// TODO: DMC
}
0x13 => {
// TODO: DMC
}
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;
@@ -236,13 +612,28 @@ impl APU {
fn q_frame_clock(&mut self) {
self.pulse_1.q_frame_clock();
self.pulse_2.q_frame_clock(); // TODO: clock all
}
fn h_frame_clock(&mut self) {
self.pulse_1.q_frame_clock();
self.pulse_1.h_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 {
@@ -268,19 +659,51 @@ impl APU {
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 {
self.frame_counter.irq
}
pub fn irq_waiting(&mut self) -> bool {
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,15 +1,98 @@
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 {}
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) { }
}

View File

@@ -1955,14 +1955,8 @@ pub enum DmaState {
}
impl DmaState {
pub fn merge(&mut self, other: DmaState) {
if matches!(other, DmaState::Running { .. }) {
if matches!(self, DmaState::Running { .. }) {
panic!("Merging incompatible dma states");
} else {
*self = other;
}
}
pub fn reset(&mut self) {
*self = Self::Passive;
}
pub fn cycle(self, mem: &mut CpuMem<'_>, cpu: &mut Cpu) -> Option<Self> {
@@ -1996,7 +1990,8 @@ impl DmaState {
rem,
read: None,
} => {
if cpu.cycle % 2 != 1 {
if cpu.cycle % 2 == 0 {
writeln!(&mut cpu.debug_log, "DMA: Waiting for cycle").unwrap();
return Some(self);
}
let read = mem.read(cpu_addr);

View File

@@ -39,8 +39,8 @@ pub struct DebuggerState {
#[derive(Debug, Clone)]
pub enum DebuggerMessage {
Run,
Pause,
// Run,
// Pause,
SetPPUCycles(usize),
RunPPUCycles,
SetCPUCycles(usize),
@@ -102,12 +102,6 @@ impl DebuggerState {
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"),
@@ -142,7 +136,7 @@ impl DebuggerState {
row![
column![
labelled("Cycle", text(nes.ppu().pixel)),
labelled("Scanline", text(nes.ppu().scanline)),
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)),
@@ -160,13 +154,13 @@ impl DebuggerState {
| ((nes.ppu().background.v >> 2) & 0x07)
)
),
labelled("AT:", hex8(nes.ppu().background.cur_attr)),
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("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(""),
@@ -338,10 +332,10 @@ impl DebuggerState {
},
|_, nes| nes.cpu.pc as usize == self.breakpoint,
),
DebuggerMessage::Run => {
Self::run_until(nes, &Break { ..Break::default() }, |_, nes| nes.halted())
}
DebuggerMessage::Pause => todo!(),
// DebuggerMessage::Run => {
// Self::run_until(nes, &Break { ..Break::default() }, |_, nes| nes.halted())
// }
// DebuggerMessage::Pause => todo!(),
}
}
}

View File

@@ -1,13 +1,36 @@
use std::fmt;
use iced::{
Element, Font,
Length::Fill,
Task,
widget::{column, lazy, row, text},
use std::{
fmt::{self, Display},
sync::Arc,
};
use crate::{mem::Mapped, PPU};
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: usize) -> Option<u8>;
@@ -16,7 +39,7 @@ pub trait Memory {
}
#[derive(Debug, Clone, Copy)]
pub struct Cpu<'a>(pub &'a Mapped);
struct Cpu<'a>(&'a Mapped);
impl Memory for Cpu<'_> {
fn peek(&self, val: usize) -> Option<u8> {
@@ -33,15 +56,19 @@ impl Memory for Cpu<'_> {
}
#[derive(Debug, Clone, Copy)]
pub struct Ppu<'a>(pub &'a Mapped);
struct Ppu<'a>(&'a Mapped, &'a PPU);
impl Memory for Ppu<'_> {
fn peek(&self, val: usize) -> Option<u8> {
self.0.peek_ppu(val as u16)
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 {
0x10000
0x4000
}
fn edit_ver(&self) -> usize {
@@ -101,38 +128,477 @@ impl HexView {
}
column![
text!("Hex view"),
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()
}))
)
))
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))
// 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) -> Element<'a, HexEvent> {
self.render_any(Ppu(mem))
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> {
self.render_any(Oam(ppu))
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
}
}

View File

@@ -10,6 +10,7 @@ mod ppu;
pub mod resize_watcher;
#[cfg(test)]
mod test_roms;
pub mod audio;
pub use ppu::{Color, PPU, RenderBuffer};
use tokio::io::AsyncReadExt as _;
@@ -20,7 +21,7 @@ use thiserror::Error;
use crate::{
apu::APU,
controllers::Controllers,
controllers::{ControllerState, Controllers},
cpu::{CPUMMRegisters, ClockState, Cpu, CycleResult, DmaState},
debug::DebugLog,
mem::{CpuMem, Mapped, PpuMem},
@@ -172,14 +173,14 @@ impl NES {
br: &Break,
) -> bool {
if let Some(dma) = self.dma.cycle(
&mut CpuMem::new(
&mut self.mapped,
&mut self.ppu,
&mut self.apu,
&mut self.dma,
&mut self.controller,
),
&mut self.cpu
&mut CpuMem::new(
&mut self.mapped,
&mut self.ppu,
&mut self.apu,
&mut self.dma,
&mut self.controller,
),
&mut self.cpu,
) {
self.dma = dma;
false
@@ -211,22 +212,7 @@ impl NES {
};
}
let cpu_exec = if self.clock_count % 3 == 0 && self.clock_count > 0 {
let nmi = self.ppu.nmi_waiting() || self.apu.nmi_waiting();
let irq = self.ppu.irq_waiting() || self.apu.irq_waiting();
let mut dma = DmaState::Passive;
if self.run_cpu_cycle(
// &mut CpuMem::new(
// &mut self.mapped,
// &mut self.ppu,
// &mut self.apu,
// &mut dma,
// &mut self.controller,
// ),
// &mut self.dma,
// nmi,
// irq,
br,
) {
if self.run_cpu_cycle(br) {
println!("Returning early from clock_cycle");
return CycleResult {
cpu_exec: true,
@@ -235,7 +221,6 @@ impl NES {
dbg_int: true,
};
}
self.dma.merge(dma);
self.cpu.cpu_cycle_update();
self.cpu.executed()
} else if self.clock_count == 0 {
@@ -268,6 +253,17 @@ impl NES {
}
pub fn reset(&mut self) {
self.clock_count = 0;
// dbg_int: false,
// clock_count: 0,
// mapped,
// cpu: Cpu::init(),
// dma: DmaState::Passive,
// // ppu: PPU::with_chr_rom(chr_rom, mapper),
// ppu: PPU::init(),
// apu: APU::init(),
// controller: Controllers::init(),
self.cpu.reset(&mut CpuMem::new(
&mut self.mapped,
&mut self.ppu,
@@ -275,7 +271,10 @@ impl NES {
&mut self.dma,
&mut self.controller,
));
self.dma.reset();
self.ppu.reset();
self.apu.reset();
self.controller.reset();
}
pub fn power_cycle(&mut self) {
@@ -351,6 +350,10 @@ impl NES {
&self.apu
}
pub fn apu_mut(&mut self) -> &mut APU {
&mut self.apu
}
pub fn debug_log(&self) -> &DebugLog {
&self.cpu.debug_log
}
@@ -373,6 +376,13 @@ impl NES {
pub fn cpu_cycle(&self) -> usize {
self.cpu.cycle
}
pub fn controller_1(&mut self) -> &mut ControllerState {
self.controller.controller_1()
}
pub fn controller_2(&mut self) -> &mut ControllerState {
self.controller.controller_2()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]

View File

@@ -1,20 +1,25 @@
use std::{collections::HashMap, fmt, time::Duration};
use std::{
collections::HashMap,
fmt,
time::{Duration, Instant},
};
use iced::{
Color, Element,
Element,
Length::{Fill, Shrink},
Point, Renderer, Size, Subscription, Task, Theme,
advanced::graphics::compositor::Display,
mouse,
Point, Rectangle, Renderer, Size, Subscription, Task, Theme,
keyboard::{self, Key, Modifiers, key::Named},
mouse, time,
widget::{
Canvas,
self, Canvas, button,
canvas::{Frame, Program},
column, container, row,
column, container, image, row,
},
window::{self, Id, Settings},
};
use nes_emu::{
NES,
Break, NES,
audio::Audio,
debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image},
header_menu::header_menu,
hex_view::{HexEvent, HexView},
@@ -28,7 +33,17 @@ use tracing_subscriber::EnvFilter;
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_fill_red.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_fill_name_table.nes");
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "int_nmi_exit_timing.nes");
const ROM_FILE: &str = "./Super Mario Bros. (World).nes";
// const ROM_FILE: &str = 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";
@@ -68,6 +83,7 @@ enum WindowType {
TileViewer,
Palette,
Debugger,
Apu,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -93,6 +109,7 @@ impl fmt::Display for HeaderButton {
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"),
@@ -101,10 +118,13 @@ impl fmt::Display for HeaderButton {
}
struct Emulator {
running: bool,
nes: NES,
windows: HashMap<Id, WindowType>,
debugger: DebuggerState,
main_win_size: Size,
prev: [Instant; 2],
audio: Audio,
}
#[derive(Debug, Clone)]
@@ -117,6 +137,9 @@ enum Message {
// DebugInt,
WindowClosed(Id),
WindowOpened(Id),
Key(keyboard::Event),
Periodic(Instant),
SetRunning(bool),
Header(HeaderButton),
Hex(Id, HexEvent),
Debugger(DebuggerMessage),
@@ -128,38 +151,43 @@ impl Emulator {
fn new() -> (Self, Task<Message>) {
let mut nes = nes_emu::NES::load_nes_file(ROM_FILE).expect("Failed to load nes file");
nes.reset();
// TODO: remove
// let mut count = 10;
// while !nes.halted() {
// if nes.run_one_clock_cycle().ppu_frame {
// count -= 1;
// if count <= 0 {
// break;
// }
// }
// }
let (win, task) = iced::window::open(Settings {
min_size: None,
..Settings::default()
});
// 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!(),
}
}
@@ -266,6 +294,101 @@ impl Emulator {
})
.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()
@@ -275,10 +398,13 @@ impl Emulator {
Subscription::batch([
window::close_events().map(Message::WindowClosed),
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![
@@ -300,7 +426,7 @@ impl Emulator {
.render_cpu(self.nes.mem())
.map(move |e| Message::Hex(win, e)),
MemoryTy::PPU => view
.render_ppu(self.nes.mem())
.render_ppu(self.nes.mem(), self.nes.ppu())
.map(move |e| Message::Hex(win, e)),
MemoryTy::OAM => view
.render_oam(self.nes.ppu())
@@ -320,11 +446,20 @@ impl Emulator {
Some(WindowType::Palette) => {
dbg_image(DbgImage::Palette(self.nes.mem(), self.nes.ppu())).into()
}
Some(WindowType::Debugger) => {
container(self.debugger.view(&self.nes).map(Message::Debugger))
.width(Fill)
.height(Fill)
.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!(),
@@ -352,6 +487,7 @@ impl Emulator {
HeaderButton::Open(WindowType::TileMap),
HeaderButton::Open(WindowType::TileViewer),
HeaderButton::Open(WindowType::Palette),
HeaderButton::Open(WindowType::Apu),
],
Message::Header
)
@@ -372,25 +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()]
}
}

View File

@@ -6,13 +6,13 @@ use crate::{
};
#[derive(Debug, Clone)]
pub enum Value<'a, R> {
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),
@@ -117,7 +117,7 @@ pub struct MemoryMap<R> {
}
impl<R: Copy> MemoryMap<R> {
pub fn read(&self, addr: u16) -> Value<'_, 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 {
@@ -125,7 +125,7 @@ impl<R: Copy> 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(pos) => {
@@ -201,28 +201,28 @@ impl<R: Copy> MemoryMap<R> {
None
}
fn peek_val(&self, addr: u16) -> Option<u8> {
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) => Some(items[(addr - segment.position) as usize]),
Data::ROM(items) => Some(items[(addr - segment.position) as usize]),
Data::Reg(_) => None,
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 => None,
Data::Disabled => Err(None),
};
}
}
None
Err(None)
}
}
impl<R: Copy> Memory for MemoryMap<R> {
fn peek(&self, addr: usize) -> Option<u8> {
self.peek_val(addr as u16)
self.peek_val(addr as u16).ok()
}
fn len(&self) -> usize {
@@ -455,14 +455,14 @@ impl Mapped {
}
}
pub fn peek_ppu(&self, addr: u16) -> Option<u8> {
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)
self.cpu.peek_val(addr).ok()
}
pub fn cpu_edit_ver(&self) -> usize {
self.cpu.edit_ver
@@ -638,7 +638,7 @@ impl<'a> PpuMem<'a> {
pub fn new(mem: &'a mut Mapped) -> Self {
Self(mem)
}
pub fn read(&mut self, addr: u16) -> Value<'_, PPUMMRegisters> {
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)) {

View File

@@ -1,15 +1,13 @@
use std::fmt;
use bytes::{Bytes, BytesMut};
use iced::{
Point, Size,
advanced::graphics::geometry::Renderer,
widget::canvas::{Fill, Frame},
};
use crate::{
hex_view::Memory,
mem::{Mapped, PpuMem},
};
use crate::mem::{Mapped, PpuMem, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Color {
@@ -33,18 +31,25 @@ impl fmt::Display for Color {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RenderBuffer<const W: usize, const H: usize> {
buffer: Box<[Color]>,
raw_rgba: BytesMut,
}
impl<const W: usize, const H: usize> RenderBuffer<W, H> {
pub fn empty() -> Self {
Self {
buffer: vec![Color { r: 0, g: 0, b: 0 }; W * H].into_boxed_slice(),
raw_rgba: BytesMut::from_iter(vec![0; W * H * 4]),
}
}
pub fn write(&mut self, line: usize, pixel: usize, color: Color) {
assert!(line < H && pixel < W);
self.buffer[line * W + pixel] = color;
let pos = (line * W + pixel) * 4;
self.raw_rgba[pos] = color.r;
self.raw_rgba[pos + 1] = color.g;
self.raw_rgba[pos + 2] = color.b;
self.raw_rgba[pos + 3] = 0xFF;
}
pub fn read(&self, line: usize, pixel: usize) -> Color {
assert!(line < H && pixel < W);
@@ -53,6 +58,12 @@ impl<const W: usize, const H: usize> RenderBuffer<W, H> {
pub fn clone_from(&mut self, other: &Self) {
self.buffer.copy_from_slice(&other.buffer);
// self.raw_rgba.fr
self.raw_rgba.copy_from_slice(&other.raw_rgba);
}
pub fn image(&self) -> Bytes {
Bytes::copy_from_slice(&self.raw_rgba)
}
pub fn assert_eq(&self, other: &Self) {
@@ -72,35 +83,221 @@ impl<const W: usize, const H: usize> RenderBuffer<W, H> {
}
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PPUMMRegisters {
Palette,
}
bitfield::bitfield! {
#[derive(Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpriteAttrs(u8);
impl Debug;
vflip, set_vflip: 7;
hflip, set_hflip: 6;
priority, set_priority: 5;
palette, set_palette: 1, 0;
}
#[derive(Debug, Clone, Default)]
struct SpriteOutputUnit {
high: u8,
low: u8,
x_pos: u8,
y_pos: u8,
tile: u8,
attrs: SpriteAttrs,
}
#[derive(Debug, Clone)]
enum OamState {
ReadY,
ReadTile,
ReadAttrs,
ReadX,
OverflowScan,
Wait,
}
#[derive(Debug, Clone)]
pub struct OAM {
mem: Vec<u8>,
secondary: Vec<u8>,
addr: u8,
count: u8,
state: OamState,
oam_read_buffer: u8,
edit_ver: usize,
sprite_output_units: Vec<SpriteOutputUnit>,
large_sprites: bool,
pub overflow: bool,
sprite_offset_0x1000: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BackgroundState {
NameTableBytePre,
NameTableByte,
AttrTableBytePre,
AttrTableByte,
PatternTableTileLowPre,
PatternTableTileLow,
PatternTableTileHighPre,
PatternTableTileHigh,
impl OAM {
fn new() -> Self {
Self {
mem: vec![0u8; 256],
addr: 0,
count: 0,
edit_ver: 0,
secondary: vec![0xFF; 32],
sprite_output_units: vec![SpriteOutputUnit::default(); 8],
state: OamState::ReadY,
oam_read_buffer: 0,
overflow: false,
sprite_offset_0x1000: false,
large_sprites: false,
}
}
fn ppu_cycle(&mut self, pixel: usize, line: usize, mem: &mut PpuMem<'_>) {
if pixel < 64 {
} else if pixel == 64 {
self.secondary.fill(0xFF);
self.state = OamState::ReadY;
self.addr = 0;
self.count = 0;
} else if pixel <= 256 {
// Fetch/evalute sprites for next line
if pixel % 2 == 1 && !matches!(self.state, OamState::Wait) {
self.oam_read_buffer = self.mem[self.addr as usize];
if self.addr == 255 {
self.addr = 0;
} else {
self.addr += 1;
}
} else {
match self.state {
OamState::ReadY => {
let l = self.oam_read_buffer as usize;
if self.count < 8 {
if l <= line && line < l + 8 {
self.secondary[self.count as usize * 4] = self.oam_read_buffer;
self.state = OamState::ReadTile;
} else {
if self.addr < 0xFD {
self.addr += 3;
} else {
self.state = OamState::Wait; // Should be Overflow scan...
}
}
} else {
self.state = OamState::Wait; // Should be Overflow scan...
}
}
OamState::ReadTile => {
self.secondary[self.count as usize * 4 + 1] = self.oam_read_buffer;
self.state = OamState::ReadAttrs;
}
OamState::ReadAttrs => {
self.secondary[self.count as usize * 4 + 2] = self.oam_read_buffer;
self.state = OamState::ReadX;
}
OamState::ReadX => {
self.secondary[self.count as usize * 4 + 3] = self.oam_read_buffer;
self.state = OamState::ReadY;
self.count += 1;
if self.count == 8 {
self.state = OamState::Wait; // Should be Overflow scan...
}
}
OamState::OverflowScan => todo!(),
OamState::Wait => (),
}
}
} else if pixel <= 320 {
if pixel == 257 {
// Reset output units
self.sprite_output_units.fill(SpriteOutputUnit::default());
}
let run = pixel - 257;
if run / 8 < self.count as usize {
if run % 8 == 0 {
self.sprite_output_units[run / 8].y_pos = self.secondary[(run / 8) * 4];
} else if run % 8 == 1 {
self.sprite_output_units[run / 8].tile = self.secondary[(run / 8) * 4 + 1];
} else if run % 8 == 2 {
self.sprite_output_units[run / 8].attrs.0 = self.secondary[(run / 8) * 4 + 2];
} else if run % 8 == 3 {
self.sprite_output_units[run / 8].x_pos = self.secondary[(run / 8) * 4 + 3];
} else if run % 8 == 4 {
} else if run % 8 == 5 {
let off = line - self.sprite_output_units[run / 8].y_pos as usize;
let off = if self.sprite_output_units[run / 8].attrs.vflip() {
7 - off
} else {
off
};
let addr = if self.sprite_offset_0x1000 {
0x1000
} else {
0x0000
} + 16 * self.sprite_output_units[run / 8].tile as u16
+ off as u16;
self.sprite_output_units[run / 8].low = mem.read(addr).reg_map(|_, _| todo!());
} else if run % 8 == 6 {
} else if run % 8 == 7 {
let off = line - self.sprite_output_units[run / 8].y_pos as usize;
let off = if self.sprite_output_units[run / 8].attrs.vflip() {
7 - off
} else {
off
};
let addr = if self.sprite_offset_0x1000 {
0x1000
} else {
0x0000
} + 16 * self.sprite_output_units[run / 8].tile as u16
+ off as u16
+ 8;
self.sprite_output_units[run / 8].high = mem.read(addr).reg_map(|_, _| todo!());
}
}
}
}
/// Returns Some((palette index, above background))
fn pixel(&mut self, pixel: usize, _line: usize) -> Option<(u8, bool, bool)> {
self.sprite_output_units
.iter()
.enumerate()
.filter_map(|(i, s)| {
if pixel >= s.x_pos as usize && pixel < s.x_pos as usize + 8 && s.x_pos < 0xFF {
let bit_pos = pixel - s.x_pos as usize;
let bit_pos = if s.attrs.hflip() {
bit_pos
} else {
7 - bit_pos
};
// println!("Shifting: 0b{:08b} by {}", s.high, 8 - bit_pos);
let hi = (s.high >> bit_pos) & 1;
// println!("Shifting: 0b{:08b} by {}", s.low, bit_pos);
let lo = (s.low >> bit_pos) & 1;
let idx = (hi << 1) | lo;
if idx != 0 {
Some((
idx + s.attrs.palette() * 4 + 0x10,
!s.attrs.priority(),
i == 0,
))
} else {
None
}
} else {
None
}
})
.next()
}
}
#[derive(Debug, Clone)]
pub struct Background {
/// Current vram address, 15 bits
/// yyy NN YYYYY XXXXX (0yyy NNYY YYYX XXXX)
pub v: u16,
/// Temp vram address, 15 bits
/// yyy NN YYYYY XXXXX
pub t: u16,
/// Fine X control, 3 bits
pub x: u8,
@@ -108,20 +305,20 @@ pub struct Background {
/// When false, writes to x
pub w: bool,
copy_v: u8,
/// When true, v is incremented by 32 after each read
pub vram_column: bool,
pub second_pattern: bool,
state: BackgroundState,
pub cur_nametable: u8,
pub cur_attr: u8,
pub next_attr: u8,
pub next_attr_2: u8,
pub cur_high: u8,
pub cur_low: u8,
pub cur_shift_high: u32,
pub cur_shift_low: u32,
pub cur_attr_shift_high: u32,
pub cur_attr_shift_low: u32,
}
#[derive(Debug, Clone, Copy)]
@@ -466,9 +663,13 @@ pub struct Palette {
}
impl Palette {
pub fn color(&self, idx: u8, palette: u8) -> Color {
pub fn color(&self, idx: u8) -> Color {
debug_assert!(idx < 0x20, "Palette index out of range");
self.colors[(self.ram[idx as usize + palette as usize * 4] & 0x3F) as usize]
self.colors[(self.ram[idx as usize] & 0x3F) as usize]
}
pub fn ram(&self, offset: u8) -> u8 {
self.ram[offset as usize]
}
}
@@ -482,12 +683,15 @@ pub struct PPU {
pub scanline: usize,
pub pixel: usize,
vram_buffer: u8,
pub mask: Mask,
pub vblank: bool,
pub sprite_zero_hit: bool,
palette: Palette,
pub palette: Palette,
pub background: Background,
oam: OAM,
pub oam: OAM,
pub render_buffer: RenderBuffer<256, 240>,
pub dbg_int: bool,
pub cycle: usize,
@@ -539,9 +743,9 @@ impl PPU {
],
},
vblank: false,
sprite_zero_hit: false,
frame_count: 0,
nmi_enabled: false,
// nmi_waiting: false,
even: false,
scanline: 0,
pixel: 25,
@@ -551,23 +755,20 @@ impl PPU {
t: 0,
x: 0,
w: false,
copy_v: 0,
vram_column: false,
second_pattern: false,
state: BackgroundState::NameTableBytePre,
cur_high: 0,
cur_low: 0,
cur_shift_high: 0,
cur_shift_low: 0,
cur_attr_shift_high: 0,
cur_attr_shift_low: 0,
cur_nametable: 0,
cur_attr: 0,
next_attr: 0,
next_attr_2: 0,
},
oam: OAM {
mem: vec![0u8; 256],
addr: 0,
edit_ver: 0,
},
oam: OAM::new(),
vram_buffer: 0,
}
}
pub fn reset(&mut self) {
@@ -583,7 +784,9 @@ impl PPU {
0 => panic!("ppuctrl is write-only"),
1 => panic!("ppumask is write-only"),
2 => {
let tmp = if self.vblank { 0b1000_0000 } else { 0 };
let tmp = if self.vblank { 0b1000_0000 } else { 0 }
| if self.sprite_zero_hit { 0b0100_0000 } else { 0 }
| if self.oam.overflow { 0b0010_0000 } else { 0 };
self.vblank = false;
self.background.w = false;
tmp
@@ -593,11 +796,17 @@ impl PPU {
5 => panic!("ppuscroll is write-only"),
6 => panic!("ppuaddr is write-only"),
7 => {
// println!("Updating v for ppudata read");
let val = mem.read(self.background.v).reg_map(|a, off| match a {
PPUMMRegisters::Palette => self.palette.ram[off as usize],
});
// if self.background
let val = match mem.read(self.background.v) {
Value::Value(v) => {
let val = self.vram_buffer;
self.vram_buffer = v;
val
}
Value::Register {
reg: PPUMMRegisters::Palette,
offset,
} => self.palette.ram[offset as usize],
};
self.increment_v();
val
}
@@ -608,12 +817,18 @@ impl PPU {
pub fn write_reg(&mut self, mem: &mut PpuMem, offset: u16, mut val: u8) {
match offset {
0x00 => {
self.nmi_enabled = val & 0b1000_0000 != 0;
self.background.t =
(self.background.t & 0b0001_1000_0000_0000) | (((val & 0b11) as u16) << 10);
(self.background.t & !0b0000_1100_0000_0000) | (((val & 0b11) as u16) << 10);
self.background.vram_column = val & 0b0000_0100 != 0;
self.oam.sprite_offset_0x1000 = val & 0b0000_1000 != 0;
self.background.second_pattern = val & 0b0001_0000 != 0;
// TODO: other control fields
self.oam.large_sprites = val & 0b0010_0000 != 0;
if val & 0b0100_0000 != 0 {
println!(
"WARNING: Bit 6 set in PPUCTRL - may cause damage on physical hardware"
);
}
self.nmi_enabled = val & 0b1000_0000 != 0;
}
0x01 => {
// self.dbg_int = true;
@@ -629,7 +844,6 @@ impl PPU {
}
0x02 => {
todo!("Unable to write to PPU status")
// TODO: ppu status
}
0x03 => self.oam.addr = val,
0x04 => {
@@ -656,30 +870,32 @@ impl PPU {
}
}
0x06 => {
// TODO: ppu addr
if self.background.w {
self.background.v =
u16::from_le_bytes([val, self.background.v.to_le_bytes()[1]]);
self.background.t =
u16::from_le_bytes([val, self.background.t.to_le_bytes()[1]]);
self.background.w = false;
self.background.copy_v = 2; // Set t to be copied to v in a pixel or so
} else {
self.background.v =
u16::from_le_bytes([self.background.v.to_le_bytes()[0], val & 0b0011_1111]);
self.background.t =
u16::from_le_bytes([self.background.t.to_le_bytes()[0], val & 0b0011_1111]);
self.background.w = true;
}
// println!("Updating v for ppuaddr write: to {:04X}", self.background.v);
// self.dbg_int = true;
// todo!("PPUADDR write")
}
0x07 => {
// println!("Writing: {:02X}, @{:04X}", val, self.background.v);
mem.write(self.background.v, val, |r, o, v| match r {
mem.write(self.background.v, val, |r, mut o, v| match r {
PPUMMRegisters::Palette => {
// println!("Writing {:02X} to {:02X}", v & 0x3F, o);
self.palette.ram[o as usize] = v & 0x3F;
if o % 4 == 0 {
o = o & !0b1_0000;
self.palette.ram[o as usize] = v & 0x3F;
self.palette.ram[(o | 0b1_0000) as usize] = v & 0x3F;
} else {
self.palette.ram[o as usize] = v & 0x3F;
}
}
});
self.increment_v();
// self.background.v += 1; // TODO: implement inc behavior
}
_ => panic!("No register at {:02X}", offset),
}
@@ -692,24 +908,24 @@ impl PPU {
} else {
self.background.v += 1;
}
// self.background.v = self
// .background
// .v
// .wrapping_add(if self.background.vram_column { 32 } else { 1 });
}
// pub fn write_oamdma(&mut self, val: u8) {
// // TODO: OAM high addr
// }
pub fn run_one_clock_cycle(&mut self, mem: &mut PpuMem<'_>) -> bool {
self.cycle += 1;
self.pixel += 1;
if self.background.copy_v > 0 {
self.background.copy_v -= 1;
if self.background.copy_v == 0 {
self.background.v = self.background.t;
}
}
if self.scanline == 261
&& (self.pixel == 341 || (self.pixel == 340 && self.even && self.rendering_enabled()))
{
self.scanline = 0;
self.pixel = 0;
self.even = !self.even;
self.frame_count += 1;
}
if self.pixel == 341 {
self.pixel = 0;
@@ -725,40 +941,68 @@ impl PPU {
self.background.v = (self.background.v & 0b0000_0100_0001_1111)
| (self.background.t & 0b0111_1011_1110_0000);
}
if self.pixel > 280 && self.pixel < 320 {
self.background.cur_shift_high <<= 1;
self.background.cur_shift_low <<= 1;
if self.scanline != 261 {
self.oam.ppu_cycle(self.pixel, self.scanline, mem);
}
// TODO
if self.pixel == 0 {
if self.scanline == 1 {
// let h_scroll_offset = self.background.x as usize + ((self.background.t as usize & 0b11) << 3);
// dbg!(h_scroll_offset);
// dbg!((self.scanline, &self.oam.sprite_output_units[0]));
// dbg!(&self.oam.secondary);
}
// self.dbg_int = true;
// idle cycle
} else if self.pixel < 257 || self.pixel > 320 {
// self.dbg_int = true;
const POS: u32 = 1 << 15;
// Determine background color
let a = self.background.cur_shift_high & POS;
let b = self.background.cur_shift_low & POS;
let val = (a >> 14) | (b >> 15);
debug_assert!(val < 4);
// const POS: u32 = 1 << 15;
let bg_color = if self.mask.enable_background {
let bit_pos = 15 - self.background.x;
// Determine background color
let a = (self.background.cur_shift_high >> bit_pos) & 1;
let b = (self.background.cur_shift_low >> bit_pos) & 1;
let val = (a << 1) | b;
debug_assert!(val < 4);
let h_off = ((self.pixel - 1) / 16) % 2;
let v_off = (self.scanline / 16) % 2;
let off = v_off * 4 + h_off * 2;
let palette = (self.background.cur_attr >> off) & 0x3;
let a = (self.background.cur_attr_shift_high >> bit_pos) & 1;
let b = (self.background.cur_attr_shift_low >> bit_pos) & 1;
let palette = ((a << 1) | b) as u8;
debug_assert!(palette < 4);
val as u8 + if val != 0 { palette * 4 } else { 0 }
} else {
0
};
if self.scanline < 240 && self.pixel < 257 {
let color = if let Some((sp_color, above, is_sprite_zero)) = self
.mask
.enable_sprites
.then_some(())
.and(self.oam.pixel(self.pixel, self.scanline))
{
if is_sprite_zero && sp_color != 0 && bg_color != 0 {
self.sprite_zero_hit = true;
}
if sp_color == 0 || (bg_color != 0 && !above) {
bg_color
} else {
sp_color
}
} else {
bg_color
};
// Write to screen
self.render_buffer.write(
self.scanline,
self.pixel - 1,
self.palette
.color(val as u8, if val != 0 { palette } else { 0 }), // self.palette.colors[val as usize],
); // TODO: this should come from shift registers
self.palette.color(color),
);
}
if self.pixel < 337 {
self.background.cur_shift_high <<= 1;
self.background.cur_shift_low <<= 1;
self.background.cur_attr_shift_high <<= 1;
self.background.cur_attr_shift_low <<= 1;
}
if self.scanline < 240 || self.scanline == 261 {
@@ -779,8 +1023,10 @@ impl PPU {
| ((self.background.v >> 2) & 0x07);
// println!("Cur: {:04X}, comp: {:04X}", addr, addr_2);
// assert_eq!(addr, addr_2);
let val = mem.read(addr).reg_map(|_, _| 0); // TODO: handle reg reads
self.background.next_attr_2 = val;
let val = mem
.read(addr)
.reg_map(|_, offset| self.palette.ram(offset as u8));
self.background.next_attr = val;
} else if self.pixel % 8 == 6 {
// BG pattern low
let addr = self.background.cur_nametable as u16 * 16
@@ -807,8 +1053,14 @@ impl PPU {
self.background.cur_high = val;
self.background.cur_shift_low |= self.background.cur_low as u32;
self.background.cur_shift_high |= self.background.cur_high as u32;
self.background.cur_attr = self.background.next_attr;
self.background.next_attr = self.background.next_attr_2;
let h_off = (self.background.v >> 1) % 2;
let v_off = (self.background.v >> 6) % 2;
let off = h_off * 2 + v_off * 4;
let palette = (self.background.next_attr >> off) & 0b11;
self.background.cur_attr_shift_low |=
if palette & 0b01 == 0 { 0 } else { 0xFF };
self.background.cur_attr_shift_high |=
if palette & 0b10 == 0 { 0 } else { 0xFF };
// Inc horizontal
if self.background.v & 0x1F == 31 {
self.background.v = (self.background.v & !0x1F) ^ 0x400;
@@ -838,19 +1090,16 @@ impl PPU {
}
}
}
} else {
// TODO: Sprite fetches
}
}
}
if self.scanline == 261 && self.pixel == 1 {
self.vblank = false;
// TODO: clear sprite 0 & sprite overflow
self.sprite_zero_hit = false;
self.oam.overflow = false;
}
if self.scanline == 241 && self.pixel == 1 {
self.vblank = true;
self.frame_count += 1;
self.background.state = BackgroundState::NameTableBytePre;
return true;
}
return false;
@@ -909,10 +1158,10 @@ impl PPU {
),
Size::new(1., 1.),
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
(false, false) => self.palette.color(0, 0),
(true, false) => self.palette.color(1, palette),
(false, true) => self.palette.color(2, palette),
(true, true) => self.palette.color(3, palette),
(false, false) => self.palette.color(0),
(true, false) => self.palette.color(1 + 4 * palette),
(false, true) => self.palette.color(2 + 4 * palette),
(true, true) => self.palette.color(3 + 4 * palette),
// (false, false) => Color { r: 0, g: 0, b: 0 },
// (true, false) => Color {
// r: 64,

View File

@@ -19,6 +19,14 @@ reset:
pla
sta RAM+0
lda #$F0
sta $400
lda #$0F
bit $400
php
pla
sta RAM+1
stp
nmi:

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

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

@@ -34,6 +34,18 @@ 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

View File

@@ -11,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:

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

@@ -63,6 +63,6 @@ rom_test!(implied_instructions, "implied_instrs.nes", |nes| {
});
rom_test!(bit, "alu_bit.nes", |nes| {
assert_eq!(nes.last_instruction(), "0x8012 HLT :2 []");
mem_cmp(&nes, 0x300, &[0xF6]);
assert_eq!(nes.last_instruction(), "0x8021 HLT :2 []");
mem_cmp(&nes, 0x300, &[0xF6, 0xF6]);
});

View File

@@ -36,6 +36,8 @@ 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 []");
assert_eq!(nes.cpu_cycle(), 10);
@@ -136,6 +138,67 @@ 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.cpu_cycle(), 57182);

View File

@@ -83,8 +83,19 @@ rom_test!(ppu_fill_name_table, "ppu_fill_name_table.nes", |nes| {
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), Some(1));
assert_eq!(nes.mem().peek_ppu(0x2020), Some(1));
assert_eq!(nes.mem().peek_ppu(0x2400), Some(2));
assert_eq!(nes.mem().peek_ppu(0x2401), Some(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 });
});

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,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: