Compare commits
8 Commits
b5e1d1a4c3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
3010469c8a
|
|||
|
fa88c825a6
|
|||
|
b9a30c286a
|
|||
|
3372559c19
|
|||
|
22c586f15a
|
|||
|
825e245df1
|
|||
|
7b76026ade
|
|||
|
f861f75b21
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/target
|
||||
*.nes
|
||||
*.zip
|
||||
*.dmp
|
||||
|
||||
182
Cargo.lock
generated
182
Cargo.lock
generated
@@ -70,6 +70,28 @@ dependencies = [
|
||||
"equator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-activity"
|
||||
version = "0.6.0"
|
||||
@@ -771,6 +793,20 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-text"
|
||||
version = "0.15.0"
|
||||
@@ -794,6 +830,36 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"objc2 0.6.3",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-avf-audio",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -858,6 +924,12 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "dispatch"
|
||||
version = "0.2.0"
|
||||
@@ -871,6 +943,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"libc",
|
||||
"objc2 0.6.3",
|
||||
]
|
||||
|
||||
@@ -2004,6 +2078,15 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
@@ -2194,7 +2277,12 @@ name = "nes-emu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitfield",
|
||||
"bytes",
|
||||
"cpal",
|
||||
"iced",
|
||||
"iced_core",
|
||||
"rfd",
|
||||
"ringbuf",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -2398,6 +2486,31 @@ dependencies = [
|
||||
"objc2-quartz-core 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-audio-toolbox"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"libc",
|
||||
"objc2 0.6.3",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-avf-audio"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
|
||||
dependencies = [
|
||||
"objc2 0.6.3",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-cloud-kit"
|
||||
version = "0.2.2"
|
||||
@@ -2433,6 +2546,29 @@ dependencies = [
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-audio"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"objc2 0.6.3",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-audio-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"objc2 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-data"
|
||||
version = "0.2.2"
|
||||
@@ -2463,7 +2599,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
"dispatch2",
|
||||
"libc",
|
||||
"objc2 0.6.3",
|
||||
]
|
||||
|
||||
@@ -2875,6 +3013,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
@@ -3172,12 +3316,50 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"dispatch2",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"objc2 0.6.3",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"percent-encoding",
|
||||
"pollster",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"web-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.52"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
|
||||
|
||||
[[package]]
|
||||
name = "ringbuf"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.20.0"
|
||||
|
||||
@@ -7,9 +7,14 @@ edition = "2024"
|
||||
bitfield = "0.19.3"
|
||||
# iced = { version = "0.14.0", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] }
|
||||
iced = { path = "../iced", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] }
|
||||
iced_core = { path = "../iced/core", features = ["advanced"] }
|
||||
rfd = "0.17.2"
|
||||
# iced_graphics = { version = "0.14.0", features = ["geometry", "image"] }
|
||||
# iced_widget = { version = "0.13.4", features = ["canvas", "image"] }
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["ansi", "chrono", "env-filter", "json", "serde"] }
|
||||
bytes = "*"
|
||||
cpal = "0.17.1"
|
||||
ringbuf = "0.4.8"
|
||||
|
||||
637
src/apu.rs
637
src/apu.rs
@@ -1,6 +1,72 @@
|
||||
use std::iter::repeat_n;
|
||||
|
||||
use iced::{
|
||||
Element, Font,
|
||||
widget::{column, text},
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
macro_rules! lut {
|
||||
($name:ident: [$ty:ty; $len:expr] = |$n:ident| $expr:expr) => {
|
||||
const $name: [$ty; $len] = {
|
||||
let mut table = [0; $len];
|
||||
let mut $n = 0;
|
||||
while $n < $len {
|
||||
table[$n] = $expr;
|
||||
$n += 1;
|
||||
}
|
||||
table
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct LengthTimerHigh(u8);
|
||||
impl Debug;
|
||||
length, set_length: 7, 3;
|
||||
u16;
|
||||
timer_high, set_timer_high: 2, 0;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LengthCounter {
|
||||
current: u16,
|
||||
silenced: bool,
|
||||
}
|
||||
|
||||
impl LengthCounter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current: 0,
|
||||
silenced: true,
|
||||
}
|
||||
}
|
||||
pub fn clock(&mut self) {
|
||||
if self.current == 0 {
|
||||
self.silenced = true;
|
||||
} else {
|
||||
self.current -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&mut self, length: u8) {
|
||||
// self.reg.0 = val;
|
||||
self.silenced = false;
|
||||
const LENGTH_LUT: [u16; 0x20] = [
|
||||
10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20,
|
||||
96, 22, 192, 24, 72, 26, 16, 28, 32, 3,
|
||||
];
|
||||
self.current = LENGTH_LUT[length as usize] + 1; // I think?
|
||||
}
|
||||
|
||||
pub fn silenced(&self) -> bool {
|
||||
self.silenced
|
||||
}
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DutyVol(u8);
|
||||
impl Debug;
|
||||
duty, set_duty: 7, 6;
|
||||
@@ -10,6 +76,7 @@ bitfield::bitfield! {
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Sweep(u8);
|
||||
impl Debug;
|
||||
enable, set_enable: 7;
|
||||
@@ -18,22 +85,23 @@ bitfield::bitfield! {
|
||||
shift, set_shift: 2, 0;
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
pub struct LengthTimerHigh(u8);
|
||||
impl Debug;
|
||||
length, set_length: 7, 3;
|
||||
timer_high, set_timer_high: 2, 0;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PulseChannel {
|
||||
enabled: bool,
|
||||
duty_vol: DutyVol,
|
||||
sweep: Sweep,
|
||||
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 {
|
||||
@@ -42,72 +110,407 @@ 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;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TriangleChannel {
|
||||
enabled: bool,
|
||||
}
|
||||
struct NoiseChannel {
|
||||
enabled: bool,
|
||||
}
|
||||
struct DeltaChannel {
|
||||
enabled: bool,
|
||||
length: LengthCounterReg,
|
||||
reload: bool,
|
||||
counter: LengthCounter,
|
||||
|
||||
length_counter: u16,
|
||||
cur: u8,
|
||||
period: u16,
|
||||
period_timer: u16,
|
||||
sample: u8,
|
||||
}
|
||||
|
||||
impl DeltaChannel {
|
||||
pub fn int(&self) -> bool {
|
||||
false
|
||||
impl TriangleChannel {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
length: LengthCounterReg(0),
|
||||
counter: LengthCounter::new(),
|
||||
reload: false,
|
||||
enabled: false,
|
||||
sample: 0,
|
||||
period: 0,
|
||||
period_timer: 0,
|
||||
cur: 0,
|
||||
length_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&mut self, offset: u16, val: u8) {
|
||||
match offset {
|
||||
0x00 => {
|
||||
self.length.0 = val;
|
||||
}
|
||||
0x01 => (),
|
||||
0x02 => self.period = self.period & 0x700 | (val as u16),
|
||||
0x03 => {
|
||||
// self.length_load.0 = val;
|
||||
// self.reload = true;
|
||||
let reg = LengthTimerHigh(val);
|
||||
self.counter.write(reg.length());
|
||||
self.period = (self.period & 0xFF) | (reg.timer_high() << 8);
|
||||
self.period_timer = self.period;
|
||||
self.cur = 0;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cur_sample(&self) -> u8 {
|
||||
self.sample
|
||||
}
|
||||
|
||||
pub fn clock(&mut self) {
|
||||
if self.length_counter > 0 && self.period > 0 {
|
||||
if self.period_timer == 0 {
|
||||
self.period_timer = self.period;
|
||||
const SAMPLES: [u8; 32] = [
|
||||
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7,
|
||||
8, 9, 10, 11, 12, 13, 14, 15,
|
||||
];
|
||||
self.cur = (self.cur + 1) % SAMPLES.len() as u8;
|
||||
self.sample = SAMPLES[self.cur as usize];
|
||||
} else {
|
||||
self.period_timer -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn q_frame_clock(&mut self) {
|
||||
if self.reload {
|
||||
self.length_counter = self.length.value() as u16;
|
||||
self.reload = self.length.halt();
|
||||
} else if self.length_counter == 0 {
|
||||
} else {
|
||||
self.length_counter -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn h_frame_clock(&mut self) {
|
||||
self.q_frame_clock();
|
||||
}
|
||||
|
||||
pub fn view<T>(&self) -> Element<'_, T> {
|
||||
text!(
|
||||
"Triangle Channel
|
||||
Linear Counter - Reload: {0:>3} ${0:02X}
|
||||
Linear Counter - Halted: {1}
|
||||
Period: {2:>3} ${2:04X}
|
||||
Length Counter - Reload Value: {3:>3} ${3:04X}
|
||||
Enabled: {4}
|
||||
Timer: {5:>3} ${5:02X}
|
||||
Frequency: ???
|
||||
Sequence Position: {6:>3} ${6:02X}
|
||||
Length Counter - Counter: {7:>3} ${7:02X}
|
||||
Linear Counter - Counter: {8:>3} ${8:02X}
|
||||
Linear Counter - Reload Flag: {9}
|
||||
Output: {10:>3} ${10:02X}
|
||||
",
|
||||
self.length.value(),
|
||||
self.length.halt(),
|
||||
self.period,
|
||||
0,
|
||||
self.enabled,
|
||||
self.period_timer,
|
||||
self.cur,
|
||||
0,
|
||||
self.length_counter,
|
||||
self.reload,
|
||||
self.cur_sample(),
|
||||
)
|
||||
.font(Font::MONOSPACE)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct NoiseEnvelope(u8);
|
||||
impl Debug;
|
||||
length_counter_halt, set_length_counter_halt: 5;
|
||||
constant_volume, set_constant_volume: 4;
|
||||
volume, set_volume: 3, 0;
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ModePeriod(u8);
|
||||
impl Debug;
|
||||
mode, set_mode: 7;
|
||||
period, set_period: 3, 0;
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct LengthCounterLoad(u8);
|
||||
impl Debug;
|
||||
length, set_length: 7, 3;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct NoiseChannel {
|
||||
enabled: bool,
|
||||
evelope: NoiseEnvelope,
|
||||
mode_period: ModePeriod,
|
||||
length_counter_load: LengthCounterLoad,
|
||||
|
||||
shr: u16,
|
||||
count: u16,
|
||||
length_counter: u8,
|
||||
}
|
||||
|
||||
impl NoiseChannel {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
evelope: NoiseEnvelope(0),
|
||||
mode_period: ModePeriod(0),
|
||||
length_counter_load: LengthCounterLoad(0),
|
||||
shr: 1,
|
||||
count: 0,
|
||||
length_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&mut self, offset: u16, val: u8) {
|
||||
match offset {
|
||||
0x00 => (),
|
||||
0x01 => (),
|
||||
0x02 => (),
|
||||
0x03 => (),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cur_sample(&self) -> u8 {
|
||||
if self.length_counter == 0 || self.shr & 1 == 1 {
|
||||
0
|
||||
} else {
|
||||
self.evelope.volume()
|
||||
}
|
||||
}
|
||||
pub fn clock(&mut self) {
|
||||
const PERIODS: &[u16] = &[
|
||||
4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4096,
|
||||
];
|
||||
if self.count == PERIODS[self.mode_period.period() as usize] / 2 {
|
||||
self.count = 0;
|
||||
let bit_0 = (self.shr & 0b1) << 14;
|
||||
let other = if self.mode_period.mode() {
|
||||
(self.shr & 0b100_0000) << 8
|
||||
} else {
|
||||
(self.shr & 0b10) << 13
|
||||
};
|
||||
self.shr = (self.shr >> 1) | (bit_0 ^ other);
|
||||
} else {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
pub fn q_frame_clock(&mut self) {}
|
||||
pub fn h_frame_clock(&mut self) {
|
||||
self.q_frame_clock();
|
||||
}
|
||||
|
||||
pub fn view<T>(&self) -> Element<'_, T> {
|
||||
text!("").font(Font::MONOSPACE).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DeltaChannel {
|
||||
enabled: bool,
|
||||
sample: u8,
|
||||
}
|
||||
|
||||
impl DeltaChannel {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
sample: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&mut self, offset: u16, val: u8) {
|
||||
match offset {
|
||||
0x00 => (),
|
||||
0x01 => (),
|
||||
0x02 => (),
|
||||
0x03 => (),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cur_sample(&self) -> u8 {
|
||||
self.sample
|
||||
}
|
||||
pub fn clock(&mut self) {}
|
||||
pub fn q_frame_clock(&mut self) {}
|
||||
pub fn h_frame_clock(&mut self) {}
|
||||
|
||||
pub fn int(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn view<T>(&self) -> Element<'_, T> {
|
||||
text!("").font(Font::MONOSPACE).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct FrameCounter {
|
||||
count: usize,
|
||||
mode_5_step: bool,
|
||||
@@ -115,6 +518,7 @@ struct FrameCounter {
|
||||
irq: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct APU {
|
||||
pulse_1: PulseChannel,
|
||||
pulse_2: PulseChannel,
|
||||
@@ -122,6 +526,8 @@ pub struct APU {
|
||||
noise: NoiseChannel,
|
||||
dmc: DeltaChannel,
|
||||
frame_counter: FrameCounter,
|
||||
|
||||
samples: Vec<u8>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for APU {
|
||||
@@ -140,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 {
|
||||
@@ -168,30 +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();
|
||||
}
|
||||
0x10 => {
|
||||
assert_eq!(val, 0x00);
|
||||
// TODO: implement this value
|
||||
}
|
||||
0x11 => {
|
||||
// TODO: load dmc counter with (val & 7F)
|
||||
}
|
||||
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;
|
||||
@@ -200,7 +599,7 @@ impl APU {
|
||||
self.pulse_1.enabled = val & 0b0000_0001 != 0;
|
||||
}
|
||||
0x17 => {
|
||||
self.frame_counter.mode_5_step = val & 0b1000_0000 == 0;
|
||||
self.frame_counter.mode_5_step = val & 0b1000_0000 != 0;
|
||||
self.frame_counter.interrupt_enabled = val & 0b0100_0000 == 0;
|
||||
if !self.frame_counter.interrupt_enabled {
|
||||
self.frame_counter.irq = false;
|
||||
@@ -213,52 +612,98 @@ 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 {
|
||||
if ppu_cycle % 6 == 1 {
|
||||
// APU Frame Counter clock cycle
|
||||
if self.frame_counter.mode_5_step {
|
||||
todo!()
|
||||
} else {
|
||||
if self.frame_counter.count == 3728 {
|
||||
self.q_frame_clock();
|
||||
} else if self.frame_counter.count == 7456 {
|
||||
self.h_frame_clock();
|
||||
} else if self.frame_counter.count == 11185 {
|
||||
self.q_frame_clock();
|
||||
} else if self.frame_counter.count == 14914 {
|
||||
self.frame_counter.irq = self.frame_counter.interrupt_enabled;
|
||||
self.h_frame_clock();
|
||||
} else if self.frame_counter.count == 14915 {
|
||||
self.frame_counter.count = 0;
|
||||
self.frame_counter.irq = self.frame_counter.interrupt_enabled;
|
||||
}
|
||||
}
|
||||
self.frame_counter.count += 1;
|
||||
if self.frame_counter.count == 3728 {
|
||||
self.q_frame_clock();
|
||||
} else if self.frame_counter.count == 7456 {
|
||||
self.h_frame_clock();
|
||||
} else if self.frame_counter.count == 11185 {
|
||||
self.q_frame_clock();
|
||||
} else if self.frame_counter.count == 14914 && !self.frame_counter.mode_5_step {
|
||||
self.frame_counter.irq = self.frame_counter.interrupt_enabled;
|
||||
self.h_frame_clock();
|
||||
} else if self.frame_counter.count == 14915 && !self.frame_counter.mode_5_step {
|
||||
self.frame_counter.count = 0;
|
||||
self.frame_counter.irq = self.frame_counter.interrupt_enabled;
|
||||
} else if self.frame_counter.count == 18640 {
|
||||
self.h_frame_clock();
|
||||
self.frame_counter.count = 0;
|
||||
}
|
||||
|
||||
self.pulse_1.clock();
|
||||
self.pulse_2.clock();
|
||||
self.noise.clock();
|
||||
self.dmc.clock();
|
||||
}
|
||||
if ppu_cycle % 3 == 1 {
|
||||
self.triangle.clock();
|
||||
}
|
||||
if ppu_cycle % (6 * 4) == 1 {
|
||||
self.gen_sample();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_frame_samples(&self) -> &[u8] {
|
||||
// println!("'Frame' of samples: {}", self.samples.len());
|
||||
&self.samples
|
||||
}
|
||||
|
||||
pub fn reset_frame_samples(&mut self) {
|
||||
self.samples.clear();
|
||||
}
|
||||
|
||||
pub fn peek_nmi(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn nmi_waiting(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn peek_irq(&self) -> bool {
|
||||
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
96
src/audio.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,98 @@
|
||||
pub struct Controllers {}
|
||||
use bitfield::bitfield;
|
||||
bitfield! {
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub struct ControllerState(u8);
|
||||
impl Debug;
|
||||
pub a, set_a: 0;
|
||||
pub b, set_b: 1;
|
||||
pub select, set_select: 2;
|
||||
pub start, set_start: 3;
|
||||
pub up, set_up: 4;
|
||||
pub down, set_down: 5;
|
||||
pub left, set_left: 6;
|
||||
pub right, set_right: 7;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct ControllerShiftReg {
|
||||
state: ControllerState,
|
||||
shift: u8,
|
||||
}
|
||||
|
||||
impl ControllerShiftReg {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
state: ControllerState(0),
|
||||
shift: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&mut self) -> u8 {
|
||||
let val = self.shift & 1;
|
||||
// Reads after the first 8 are always 1
|
||||
self.shift = (self.shift >> 1) | 0x80;
|
||||
val
|
||||
}
|
||||
|
||||
fn load(&mut self) {
|
||||
self.shift = self.state.0;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Controllers {
|
||||
strobe: bool,
|
||||
primary: ControllerShiftReg,
|
||||
secondary: ControllerShiftReg,
|
||||
}
|
||||
|
||||
impl Controllers {
|
||||
pub fn init() -> Self {
|
||||
Self {}
|
||||
Self {
|
||||
strobe: false,
|
||||
primary: ControllerShiftReg::new(),
|
||||
secondary: ControllerShiftReg::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::init();
|
||||
}
|
||||
|
||||
pub fn read_joy1(&mut self) -> u8 {
|
||||
0
|
||||
if self.strobe {
|
||||
self.primary.load();
|
||||
}
|
||||
// TODO: Technically some kind of open-bus for all but the lowest 3 bits
|
||||
self.primary.read() | 0x40
|
||||
}
|
||||
|
||||
pub fn read_joy2(&mut self) -> u8 {
|
||||
0
|
||||
if self.strobe {
|
||||
self.secondary.load();
|
||||
}
|
||||
self.secondary.read() | 0x40
|
||||
}
|
||||
|
||||
pub fn write_joy_strobe(&mut self, val: u8) {
|
||||
if val & 1 == 0 {
|
||||
if self.strobe {
|
||||
self.primary.load();
|
||||
self.secondary.load();
|
||||
}
|
||||
self.strobe = false;
|
||||
} else {
|
||||
self.strobe = true;
|
||||
self.primary.load();
|
||||
self.secondary.load();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn controller_1(&mut self) -> &mut ControllerState {
|
||||
&mut self.primary.state
|
||||
}
|
||||
|
||||
pub fn controller_2(&mut self) -> &mut ControllerState {
|
||||
&mut self.secondary.state
|
||||
}
|
||||
pub fn write_joy_strobe(&mut self, val: u8) { }
|
||||
}
|
||||
|
||||
2019
src/cpu.rs
Normal file
2019
src/cpu.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DebugLog {
|
||||
current: String,
|
||||
history: Vec<String>,
|
||||
|
||||
151
src/debugger.rs
151
src/debugger.rs
@@ -3,7 +3,7 @@ use std::rc::Rc;
|
||||
use iced::{
|
||||
Element,
|
||||
Length::{self, Fill},
|
||||
Point, Renderer, Size, Theme,
|
||||
Point, Renderer, Size,
|
||||
advanced::{
|
||||
Widget,
|
||||
layout::Node,
|
||||
@@ -18,14 +18,13 @@ use iced::{
|
||||
canvas::{Frame, Program},
|
||||
checkbox, column,
|
||||
container::bordered_box,
|
||||
image, number_input, hex_input, row,
|
||||
hex_input, image, number_input, row,
|
||||
rule::horizontal,
|
||||
scrollable, text,
|
||||
},
|
||||
window::Event,
|
||||
};
|
||||
|
||||
use crate::{CycleResult, NES, PPU};
|
||||
use crate::{Break, CycleResult, NES, PPU, mem::Mapped};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DebuggerState {
|
||||
@@ -40,8 +39,8 @@ pub struct DebuggerState {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DebuggerMessage {
|
||||
Run,
|
||||
Pause,
|
||||
// Run,
|
||||
// Pause,
|
||||
SetPPUCycles(usize),
|
||||
RunPPUCycles,
|
||||
SetCPUCycles(usize),
|
||||
@@ -103,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"),
|
||||
@@ -117,7 +110,7 @@ impl DebuggerState {
|
||||
labelled("X:", text(format!("{:02X}", nes.cpu.x))),
|
||||
labelled("Y:", text(format!("{:02X}", nes.cpu.y))),
|
||||
labelled("PC:", text(format!("{:04X}", nes.cpu.pc))),
|
||||
labelled("Cycle:", text(format!("{}", nes.cycle))),
|
||||
labelled("Cycle:", text(format!("{}", nes.cpu_cycle()))),
|
||||
labelled("SP:", text(format!("{:02X}", nes.cpu.sp))),
|
||||
]
|
||||
.spacing(5.),
|
||||
@@ -143,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)),
|
||||
@@ -161,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(""),
|
||||
@@ -255,23 +248,41 @@ impl DebuggerState {
|
||||
|
||||
fn run_n_clock_cycles(nes: &mut NES, n: usize) {
|
||||
for _ in 0..n {
|
||||
if nes.run_one_clock_cycle().dbg_int || nes.halted {
|
||||
if nes.run_one_clock_cycle(&Break::default()).dbg_int || nes.halted() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn run_until(nes: &mut NES, mut f: impl FnMut(CycleResult, &NES) -> bool, mut count: usize) {
|
||||
loop {
|
||||
let res = nes.run_one_clock_cycle();
|
||||
if res.dbg_int || f(res, nes) {
|
||||
count -= 1;
|
||||
if count <= 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if nes.halted {
|
||||
break;
|
||||
}
|
||||
fn run_until(
|
||||
nes: &mut NES,
|
||||
br: &Break,
|
||||
mut f: impl FnMut(CycleResult, &NES) -> bool,
|
||||
// mut count: usize,
|
||||
) {
|
||||
// Always run at least 1 cycle
|
||||
let mut res = nes.run_one_clock_cycle(&Break::default());
|
||||
while !nes.halted() && !res.dbg_int && !f(res, nes) {
|
||||
// if res.dbg_int || f(res, nes) {
|
||||
// count -= 1;
|
||||
// if count <= 0 {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// if nes.halted() {
|
||||
// break;
|
||||
// }
|
||||
res = nes.run_one_clock_cycle(br);
|
||||
}
|
||||
}
|
||||
fn run_until_n(
|
||||
nes: &mut NES,
|
||||
br: &Break,
|
||||
mut f: impl FnMut(CycleResult, &NES) -> bool,
|
||||
mut count: usize,
|
||||
) {
|
||||
while count > 0 {
|
||||
Self::run_until(nes, br, &mut f);
|
||||
count -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,22 +292,50 @@ impl DebuggerState {
|
||||
DebuggerMessage::SetCPUCycles(n) => self.cpu_cycles = n,
|
||||
DebuggerMessage::SetInstructions(n) => self.instructions = n,
|
||||
DebuggerMessage::SetScanLines(n) => self.scan_lines = n,
|
||||
DebuggerMessage::SetToScanLine(n) => self.to_scan_line = n,
|
||||
DebuggerMessage::SetToScanLine(n) => self.to_scan_line = n.min(261), // Max scanline is 261
|
||||
DebuggerMessage::SetFrames(n) => self.frames = n,
|
||||
DebuggerMessage::SetBreakpoint(n) => self.breakpoint = n,
|
||||
DebuggerMessage::RunPPUCycles => Self::run_n_clock_cycles(nes, self.ppu_cycles),
|
||||
DebuggerMessage::RunCPUCycles => Self::run_n_clock_cycles(nes, self.cpu_cycles * 3),
|
||||
DebuggerMessage::RunInstructions => {
|
||||
Self::run_until(nes, |c, _| c.cpu_exec, self.instructions)
|
||||
}
|
||||
// DebuggerMessage::RunInstructions => Self::run_until_n(
|
||||
// nes,
|
||||
// &Break {
|
||||
// cpu_exec: true,
|
||||
// ..Break::default()
|
||||
// },
|
||||
// |_, _| false,
|
||||
// self.instructions,
|
||||
// ),
|
||||
DebuggerMessage::RunInstructions => Self::run_until_n(
|
||||
nes,
|
||||
&Break {
|
||||
..Break::default()
|
||||
},
|
||||
|res, _| res.cpu_exec,
|
||||
self.instructions,
|
||||
),
|
||||
DebuggerMessage::RunScanLines => Self::run_n_clock_cycles(nes, self.scan_lines * 341),
|
||||
DebuggerMessage::RunToScanLine => {
|
||||
Self::run_until(nes, |_, n| n.ppu.scanline == self.to_scan_line, 1)
|
||||
}
|
||||
DebuggerMessage::RunToScanLine => Self::run_until(
|
||||
nes,
|
||||
&Break {
|
||||
ppu_scanline: true,
|
||||
..Break::default()
|
||||
},
|
||||
|_, n| n.ppu.scanline == self.to_scan_line,
|
||||
),
|
||||
DebuggerMessage::RunFrames => Self::run_n_clock_cycles(nes, self.frames * 341 * 262),
|
||||
DebuggerMessage::RunBreakpoint => Self::run_until(nes, |_, nes| nes.cpu.pc as usize == self.breakpoint, 1),
|
||||
DebuggerMessage::Run => Self::run_until(nes, |_, nes| nes.halted, 1),
|
||||
DebuggerMessage::Pause => todo!(),
|
||||
DebuggerMessage::RunBreakpoint => Self::run_until(
|
||||
nes,
|
||||
&Break {
|
||||
break_points: vec![self.breakpoint as u16],
|
||||
..Break::default()
|
||||
},
|
||||
|_, nes| nes.cpu.pc as usize == self.breakpoint,
|
||||
),
|
||||
// DebuggerMessage::Run => {
|
||||
// Self::run_until(nes, &Break { ..Break::default() }, |_, nes| nes.halted())
|
||||
// }
|
||||
// DebuggerMessage::Pause => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,9 +405,9 @@ pub fn labelled_box<'a, Message: 'a>(label: &'a str, value: bool) -> Element<'a,
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum DbgImage<'a> {
|
||||
NameTable(&'a PPU),
|
||||
PatternTable(&'a PPU),
|
||||
Palette(&'a PPU),
|
||||
NameTable(&'a Mapped, &'a PPU),
|
||||
PatternTable(&'a Mapped, &'a PPU),
|
||||
Palette(&'a Mapped, &'a PPU),
|
||||
}
|
||||
|
||||
impl<Message, T> Program<Message, T> for DbgImage<'_> {
|
||||
@@ -388,9 +427,11 @@ impl<Message, T> Program<Message, T> for DbgImage<'_> {
|
||||
name_table_frame.scale(2.);
|
||||
// println!("Position: {:?}", cursor.position());
|
||||
match self {
|
||||
DbgImage::NameTable(ppu) => ppu.render_name_table(&mut name_table_frame),
|
||||
DbgImage::PatternTable(ppu) => ppu.render_pattern_tables(&mut name_table_frame),
|
||||
DbgImage::Palette(ppu) => ppu.render_palette(&mut name_table_frame),
|
||||
DbgImage::NameTable(mem, ppu) => ppu.render_name_table(mem, &mut name_table_frame),
|
||||
DbgImage::PatternTable(mem, ppu) => {
|
||||
ppu.render_pattern_tables(mem, &mut name_table_frame)
|
||||
}
|
||||
DbgImage::Palette(_, ppu) => ppu.render_palette(&mut name_table_frame),
|
||||
};
|
||||
vec![name_table_frame.into_geometry()]
|
||||
}
|
||||
@@ -399,23 +440,23 @@ impl<Message, T> Program<Message, T> for DbgImage<'_> {
|
||||
impl DbgImage<'_> {
|
||||
fn width(&self) -> Length {
|
||||
match self {
|
||||
DbgImage::NameTable(_) => Length::Fixed(512. * 2.),
|
||||
DbgImage::PatternTable(_) => Length::Fixed(16. * 8. * 2.),
|
||||
DbgImage::Palette(_) => Length::Fixed(40. * 2.),
|
||||
DbgImage::NameTable(_, _) => Length::Fixed(512. * 2.),
|
||||
DbgImage::PatternTable(_, _) => Length::Fixed(16. * 8. * 2.),
|
||||
DbgImage::Palette(_, _) => Length::Fixed(40. * 2.),
|
||||
}
|
||||
}
|
||||
fn height(&self) -> Length {
|
||||
match self {
|
||||
DbgImage::NameTable(_) => Length::Fixed(512. * 2.),
|
||||
DbgImage::PatternTable(_) => Length::Fixed(16. * 8. * 2. * 2.),
|
||||
DbgImage::Palette(_) => Length::Fixed(80. * 2.),
|
||||
DbgImage::NameTable(_, _) => Length::Fixed(512. * 2.),
|
||||
DbgImage::PatternTable(_, _) => Length::Fixed(16. * 8. * 2. * 2.),
|
||||
DbgImage::Palette(_, _) => Length::Fixed(80. * 2.),
|
||||
}
|
||||
}
|
||||
fn help(&self, cursor: Point) -> Option<String> {
|
||||
match self {
|
||||
DbgImage::NameTable(ppu) => ppu.name_cursor_info(cursor),
|
||||
DbgImage::PatternTable(ppu) => ppu.pattern_cursor_info(cursor),
|
||||
DbgImage::Palette(ppu) => ppu.palette_cursor_info(cursor),
|
||||
DbgImage::NameTable(mem, ppu) => ppu.name_cursor_info(mem, cursor),
|
||||
DbgImage::PatternTable(_, ppu) => ppu.pattern_cursor_info(cursor),
|
||||
DbgImage::Palette(_, ppu) => ppu.palette_cursor_info(cursor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
576
src/hex_view.rs
576
src/hex_view.rs
@@ -1,19 +1,103 @@
|
||||
use std::fmt;
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use iced::{widget::{column, lazy, row, text}, Element, Font, Length::Fill, Task};
|
||||
use iced::{
|
||||
Element, Fill, Font, Padding, Task,
|
||||
advanced::graphics::text::{
|
||||
FontSystem,
|
||||
cosmic_text::{
|
||||
Attrs, Family, Metrics,
|
||||
fontdb::{ID, Query},
|
||||
},
|
||||
font_system,
|
||||
},
|
||||
keyboard::{Key, key::Named},
|
||||
mouse::{Button, ScrollDelta},
|
||||
widget::{column, lazy, row, text},
|
||||
};
|
||||
use iced_core::{
|
||||
Clipboard, Color, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Text, Vector,
|
||||
Widget,
|
||||
layout::{Limits, Node},
|
||||
mouse::{Cursor, Interaction},
|
||||
overlay,
|
||||
renderer::{Quad, Style},
|
||||
widget::{
|
||||
Operation, Tree,
|
||||
tree::{State, Tag},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{PPU, mem::Mapped, ppu::PPUMMRegisters};
|
||||
|
||||
pub trait Memory {
|
||||
fn peek(&self, val: u16) -> Option<u8>;
|
||||
fn peek(&self, val: usize) -> Option<u8>;
|
||||
fn len(&self) -> usize;
|
||||
fn edit_ver(&self) -> usize;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Cpu<'a>(&'a Mapped);
|
||||
|
||||
impl Memory for Cpu<'_> {
|
||||
fn peek(&self, val: usize) -> Option<u8> {
|
||||
self.0.peek_cpu(val as u16)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
0x10000
|
||||
}
|
||||
|
||||
fn edit_ver(&self) -> usize {
|
||||
self.0.cpu_edit_ver()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Ppu<'a>(&'a Mapped, &'a PPU);
|
||||
|
||||
impl Memory for Ppu<'_> {
|
||||
fn peek(&self, val: usize) -> Option<u8> {
|
||||
match self.0.peek_ppu(val as u16) {
|
||||
Ok(v) => Some(v),
|
||||
Err(Some((PPUMMRegisters::Palette, off))) => Some(self.1.palette.ram(off as u8)),
|
||||
Err(None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
0x4000
|
||||
}
|
||||
|
||||
fn edit_ver(&self) -> usize {
|
||||
self.0.ppu_edit_ver()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Oam<'a>(pub &'a PPU);
|
||||
|
||||
impl Memory for Oam<'_> {
|
||||
fn peek(&self, val: usize) -> Option<u8> {
|
||||
Some(self.0.peek_oam(val as u8))
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
0x100
|
||||
}
|
||||
|
||||
fn edit_ver(&self) -> usize {
|
||||
self.0.oam_edit_ver()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HexEvent {}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HexView {
|
||||
|
||||
}
|
||||
pub struct HexView {}
|
||||
|
||||
struct Val(Option<u8>);
|
||||
impl fmt::Display for Val {
|
||||
@@ -31,9 +115,9 @@ impl HexView {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub fn render<'a>(&self, mem: &'a impl Memory) -> Element<'a, HexEvent> {
|
||||
struct Row<'a, M>(u16, &'a M);
|
||||
impl<M: Memory> fmt::Display for Row<'_, M> {
|
||||
pub fn render_any<'a, M: Memory + Copy + 'a>(&self, mem: M) -> Element<'a, HexEvent> {
|
||||
struct Row<M: Memory>(usize, M);
|
||||
impl<'a, M: Memory + 'a> fmt::Display for Row<M> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Val(self.1.peek(self.0)))?;
|
||||
for i in 1..16 {
|
||||
@@ -44,15 +128,477 @@ impl HexView {
|
||||
}
|
||||
column![
|
||||
text!("Hex view"),
|
||||
iced::widget::scrollable(lazy(mem.edit_ver(), |_| column([
|
||||
text!(" | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F").font(Font::MONOSPACE).into()
|
||||
].into_iter().chain((0..u16::MAX).step_by(16).map(|off| {
|
||||
text!(" {off:04X} | {}", Row(off, mem)).font(Font::MONOSPACE).into()
|
||||
}))))).width(Fill),
|
||||
].width(Fill).into()
|
||||
iced::widget::scrollable(lazy(mem.edit_ver(), move |_| column(
|
||||
[
|
||||
text!(" | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F")
|
||||
.font(Font::MONOSPACE)
|
||||
.into()
|
||||
]
|
||||
.into_iter()
|
||||
.chain((0..mem.len()).step_by(16).map(|off| {
|
||||
text!(" {off:04X} | {}", Row(off, mem))
|
||||
.font(Font::MONOSPACE)
|
||||
.into()
|
||||
}))
|
||||
)))
|
||||
.width(Fill),
|
||||
]
|
||||
.width(Fill)
|
||||
.into()
|
||||
}
|
||||
pub fn render_cpu<'a>(&self, mem: &'a Mapped) -> Element<'a, HexEvent> {
|
||||
// self.render_any(Cpu(mem))
|
||||
Element::new(
|
||||
hex_editor::<Cpu<'a>, HexEvent, iced::Renderer>(Cpu(mem)).font(Font::MONOSPACE),
|
||||
)
|
||||
}
|
||||
pub fn render_ppu<'a>(&self, mem: &'a Mapped, ppu: &'a PPU) -> Element<'a, HexEvent> {
|
||||
// self.render_any(Ppu(mem, ppu))
|
||||
Element::new(
|
||||
hex_editor::<Ppu<'a>, HexEvent, iced::Renderer>(Ppu(mem, ppu)).font(Font::MONOSPACE),
|
||||
)
|
||||
}
|
||||
pub fn render_oam<'a>(&self, ppu: &'a PPU) -> Element<'a, HexEvent> {
|
||||
Element::new(
|
||||
hex_editor::<Oam<'a>, HexEvent, iced::Renderer>(Oam(ppu)).font(Font::MONOSPACE),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, ev: HexEvent) -> Task<HexEvent> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Buffer {
|
||||
fn peek(&self, val: usize) -> Option<u8>;
|
||||
fn len(&self) -> usize;
|
||||
}
|
||||
|
||||
impl Buffer for &[u8] {
|
||||
fn peek(&self, val: usize) -> Option<u8> {
|
||||
self.get(val).copied()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.iter().len()
|
||||
}
|
||||
}
|
||||
impl<M: Memory> Buffer for M {
|
||||
fn peek(&self, val: usize) -> Option<u8> {
|
||||
self.peek(val)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hex_editor<B: Buffer, M, R: iced_core::text::Renderer>(raw: B) -> HexEditor<B, M, R> {
|
||||
HexEditor {
|
||||
val: raw,
|
||||
on_edit: None,
|
||||
font: None,
|
||||
font_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct HexEdit {
|
||||
position: usize,
|
||||
old_value: u8,
|
||||
new_value: u8,
|
||||
}
|
||||
|
||||
pub struct HexEditor<B, M, R: iced_core::text::Renderer> {
|
||||
val: B,
|
||||
font_size: Option<Pixels>,
|
||||
font: Option<R::Font>,
|
||||
on_edit: Option<Box<dyn Fn(HexEdit) -> M>>,
|
||||
}
|
||||
|
||||
impl<B, M, R> HexEditor<B, M, R>
|
||||
where
|
||||
R: iced_core::text::Renderer,
|
||||
{
|
||||
pub fn font(mut self, font: R::Font) -> Self {
|
||||
self.font = Some(font);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Buffer, M, R> HexEditor<B, M, R>
|
||||
where
|
||||
R: iced_core::text::Renderer,
|
||||
{
|
||||
fn value_bounds(&self, renderer: &R, layout: Layout<'_>) -> Rectangle {
|
||||
let size = self.font_size.unwrap_or(renderer.default_size());
|
||||
layout
|
||||
.bounds()
|
||||
.shrink(Padding::new(0.).top(size.0 * 1.5).right(10.))
|
||||
}
|
||||
|
||||
fn scrollbar_bounds(&self, renderer: &R, layout: Layout<'_>) -> Rectangle {
|
||||
let size = self.font_size.unwrap_or(renderer.default_size());
|
||||
layout.bounds().shrink(
|
||||
Padding::new(0.)
|
||||
.top(size.0 * 1.5)
|
||||
.left(layout.bounds().width - 10.),
|
||||
)
|
||||
}
|
||||
|
||||
fn row_height(&self, renderer: &R) -> f32 {
|
||||
self.font_size.unwrap_or(renderer.default_size()).0 * LINE_HEIGHT
|
||||
}
|
||||
|
||||
fn scroll_max(&self, renderer: &R, layout: Layout<'_>) -> f32 {
|
||||
let size = self.font_size.unwrap_or(renderer.default_size());
|
||||
let bounds = layout.bounds().shrink(Padding::new(0.).top(size.0 * 1.5));
|
||||
let rows = self.val.len().div_ceil(0x10);
|
||||
(self.row_height(renderer) * rows as f32 - bounds.height).max(0.)
|
||||
}
|
||||
|
||||
fn scroll(
|
||||
&self,
|
||||
state: &mut HexEditorState,
|
||||
renderer: &R,
|
||||
layout: Layout<'_>,
|
||||
delta: &ScrollDelta,
|
||||
) {
|
||||
// let size = self.font_size.unwrap_or(renderer.default_size());
|
||||
// let bounds = layout.bounds().shrink(Padding::new(0.).top(size.0 * 1.5));
|
||||
// let rows = self.val.len().div_ceil(0x10);
|
||||
let max = self.scroll_max(renderer, layout);
|
||||
// println!("max: {max}, rows: {rows}");
|
||||
match delta {
|
||||
ScrollDelta::Lines { y, .. } => {
|
||||
state.offset_y += y * self.row_height(renderer);
|
||||
}
|
||||
ScrollDelta::Pixels { y, .. } => {
|
||||
state.offset_y += y;
|
||||
}
|
||||
}
|
||||
if state.offset_y > 0. {
|
||||
state.offset_y = 0.;
|
||||
} else if state.offset_y < -max {
|
||||
state.offset_y = -max;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct HexEditorState {
|
||||
offset_y: f32,
|
||||
selected: usize,
|
||||
dragging: bool,
|
||||
}
|
||||
|
||||
const LINE_HEIGHT: f32 = 1.3;
|
||||
|
||||
impl<B: Buffer, M, T, R> Widget<M, T, R> for HexEditor<B, M, R>
|
||||
where
|
||||
R: iced_core::text::Renderer,
|
||||
{
|
||||
fn tag(&self) -> Tag {
|
||||
Tag::of::<HexEditorState>()
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::new(HexEditorState::default())
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size::new(Length::Fill, Length::Fill)
|
||||
}
|
||||
|
||||
fn layout(&mut self, _tree: &mut Tree, _renderer: &R, limits: &Limits) -> Node {
|
||||
Node::new(limits.max())
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut R,
|
||||
theme: &T,
|
||||
style: &Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let state: &HexEditorState = tree.state.downcast_ref();
|
||||
let size = self.font_size.unwrap_or(renderer.default_size());
|
||||
let font = self.font.unwrap_or(renderer.default_font());
|
||||
|
||||
// let fonts = font_system();
|
||||
// let mut font_sys = fonts.write().unwrap();
|
||||
// let id = font_sys.raw().db().query(&Query {
|
||||
// families: &[Family::Monospace],
|
||||
// ..Query::default()
|
||||
// });
|
||||
// let f = font_sys.raw().get_font(id.unwrap(), iced::advanced::graphics::text::cosmic_text::Weight(1)).unwrap();
|
||||
// let width = f.metrics();
|
||||
// println!("Width: {width:?}");
|
||||
// iced::advanced::graphics::text::cosmic_text::Buffer::new(
|
||||
// font_sys.raw(),
|
||||
// Metrics::new(size.0, size.0),
|
||||
// ).layout_runs();
|
||||
// TODO: this needs to be computed from the font
|
||||
let col_width = 0.6 * size.0;
|
||||
|
||||
let rows = self.val.len().div_ceil(0x10);
|
||||
let row_label_length = rows.ilog2().div_ceil(4) as usize;
|
||||
let mut draw = |v: &str, pos: Point| {
|
||||
renderer.fill_text(
|
||||
Text {
|
||||
content: format!("{}", v),
|
||||
bounds: viewport.size(),
|
||||
size,
|
||||
line_height: size.into(),
|
||||
font,
|
||||
align_x: iced_core::text::Alignment::Left,
|
||||
align_y: iced_core::alignment::Vertical::Top,
|
||||
shaping: iced_core::text::Shaping::Basic,
|
||||
wrapping: iced_core::text::Wrapping::None,
|
||||
hint_factor: None,
|
||||
},
|
||||
pos,
|
||||
Color::WHITE,
|
||||
layout.bounds(),
|
||||
);
|
||||
};
|
||||
draw("0", Point::new(0., 0.));
|
||||
for i in 0..0x10 {
|
||||
draw(
|
||||
"0",
|
||||
layout.position()
|
||||
+ Vector::new((3 + row_label_length + 3 * i) as f32 * col_width, 0.),
|
||||
);
|
||||
draw(
|
||||
"0",
|
||||
layout.position()
|
||||
+ Vector::new((4 + row_label_length + 3 * i) as f32 * col_width, 0.),
|
||||
);
|
||||
}
|
||||
// renderer.fill_text(
|
||||
// Text {
|
||||
// content: format!(
|
||||
// "{:width$} 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F {}",
|
||||
// "",
|
||||
// state.offset_y,
|
||||
// width = row_label_length
|
||||
// ),
|
||||
// bounds: viewport.size(),
|
||||
// size,
|
||||
// line_height: size.into(),
|
||||
// font,
|
||||
// align_x: iced_core::text::Alignment::Left,
|
||||
// align_y: iced_core::alignment::Vertical::Top,
|
||||
// shaping: iced_core::text::Shaping::Basic,
|
||||
// wrapping: iced_core::text::Wrapping::None,
|
||||
// hint_factor: None,
|
||||
// },
|
||||
// layout.position(),
|
||||
// Color::WHITE,
|
||||
// layout.bounds(),
|
||||
// );
|
||||
struct HexV(Option<u8>);
|
||||
impl Display for HexV {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.0 {
|
||||
Some(v) => write!(f, "{:02X}", v),
|
||||
None => write!(f, "XX"),
|
||||
}
|
||||
}
|
||||
}
|
||||
// if rows > 0 {
|
||||
// rows.ilog2()
|
||||
// }
|
||||
let bounds = self.value_bounds(renderer, layout);
|
||||
let mut pos = bounds.position() + Vector::new(0., state.offset_y);
|
||||
for row in 0..rows {
|
||||
if bounds.contains(pos) || bounds.contains(pos + Vector::new(0., size.0 * LINE_HEIGHT))
|
||||
{
|
||||
renderer.fill_text(
|
||||
Text {
|
||||
content: format!(
|
||||
"{:0width$X}0: {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}",
|
||||
row,
|
||||
HexV(self.val.peek(row * 0x10 + 0x0)),
|
||||
HexV(self.val.peek(row * 0x10 + 0x1)),
|
||||
HexV(self.val.peek(row * 0x10 + 0x2)),
|
||||
HexV(self.val.peek(row * 0x10 + 0x3)),
|
||||
HexV(self.val.peek(row * 0x10 + 0x4)),
|
||||
HexV(self.val.peek(row * 0x10 + 0x5)),
|
||||
HexV(self.val.peek(row * 0x10 + 0x6)),
|
||||
HexV(self.val.peek(row * 0x10 + 0x7)),
|
||||
HexV(self.val.peek(row * 0x10 + 0x8)),
|
||||
HexV(self.val.peek(row * 0x10 + 0x9)),
|
||||
HexV(self.val.peek(row * 0x10 + 0xA)),
|
||||
HexV(self.val.peek(row * 0x10 + 0xB)),
|
||||
HexV(self.val.peek(row * 0x10 + 0xC)),
|
||||
HexV(self.val.peek(row * 0x10 + 0xD)),
|
||||
HexV(self.val.peek(row * 0x10 + 0xE)),
|
||||
HexV(self.val.peek(row * 0x10 + 0xF)),
|
||||
width = row_label_length,
|
||||
),
|
||||
bounds: viewport.size(),
|
||||
size,
|
||||
line_height: size.into(),
|
||||
font,
|
||||
align_x: iced_core::text::Alignment::Left,
|
||||
align_y: iced_core::alignment::Vertical::Top,
|
||||
shaping: iced_core::text::Shaping::Basic,
|
||||
wrapping: iced_core::text::Wrapping::None,
|
||||
hint_factor: None,
|
||||
},
|
||||
pos,
|
||||
Color::WHITE,
|
||||
bounds,
|
||||
);
|
||||
}
|
||||
pos += Vector::new(0., size.0 * LINE_HEIGHT);
|
||||
}
|
||||
|
||||
let scrollbar = self.scrollbar_bounds(renderer, layout);
|
||||
renderer.fill_quad(
|
||||
Quad {
|
||||
bounds: scrollbar,
|
||||
..Quad::default()
|
||||
},
|
||||
Color::BLACK,
|
||||
);
|
||||
let pos = state.offset_y / self.scroll_max(renderer, layout);
|
||||
renderer.fill_quad(
|
||||
Quad {
|
||||
bounds: Rectangle::new(
|
||||
Point::new(scrollbar.x, scrollbar.y - pos * (scrollbar.height - 20.)),
|
||||
Size::new(10., 20.),
|
||||
),
|
||||
..Quad::default()
|
||||
},
|
||||
Color::WHITE,
|
||||
);
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
_tree: &mut Tree,
|
||||
_layout: Layout<'_>,
|
||||
_renderer: &R,
|
||||
_operation: &mut dyn Operation,
|
||||
) {
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: Cursor,
|
||||
renderer: &R,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, M>,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
// if !matches!(event, Event::Window(_)) {
|
||||
// println!("Event: {:#?}", event);
|
||||
// }
|
||||
match event {
|
||||
Event::Keyboard(iced::keyboard::Event::KeyPressed {
|
||||
key: Key::Named(Named::PageUp),
|
||||
..
|
||||
}) => {
|
||||
let state: &mut HexEditorState = tree.state.downcast_mut();
|
||||
self.scroll(
|
||||
state,
|
||||
renderer,
|
||||
layout,
|
||||
&ScrollDelta::Pixels {
|
||||
x: 0.,
|
||||
y: self.value_bounds(renderer, layout).height,
|
||||
},
|
||||
);
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
}
|
||||
Event::Keyboard(iced::keyboard::Event::KeyPressed {
|
||||
key: Key::Named(Named::PageDown),
|
||||
..
|
||||
}) => {
|
||||
let state: &mut HexEditorState = tree.state.downcast_mut();
|
||||
self.scroll(
|
||||
state,
|
||||
renderer,
|
||||
layout,
|
||||
&ScrollDelta::Pixels {
|
||||
x: 0.,
|
||||
y: -self.value_bounds(renderer, layout).height,
|
||||
},
|
||||
);
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
}
|
||||
Event::Mouse(iced::mouse::Event::WheelScrolled { delta }) => {
|
||||
let state: &mut HexEditorState = tree.state.downcast_mut();
|
||||
let bounds = self.value_bounds(renderer, layout);
|
||||
if cursor.is_over(bounds) {
|
||||
self.scroll(state, renderer, layout, delta);
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(iced::mouse::Event::ButtonPressed(Button::Left)) => {
|
||||
let state: &mut HexEditorState = tree.state.downcast_mut();
|
||||
let bounds = self.scrollbar_bounds(renderer, layout);
|
||||
if let Some(pos) = cursor.position_in(bounds) {
|
||||
state.offset_y = -(pos.y / bounds.height * self.scroll_max(renderer, layout));
|
||||
state.dragging = true;
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(iced::mouse::Event::ButtonReleased(Button::Left)) => {
|
||||
let state: &mut HexEditorState = tree.state.downcast_mut();
|
||||
state.dragging = false;
|
||||
}
|
||||
// Event::Mouse(iced::mouse::Event::CursorLeft) => {
|
||||
// let state: &mut HexEditorState = tree.state.downcast_mut();
|
||||
// state.dragging = false;
|
||||
// }
|
||||
Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => {
|
||||
let state: &mut HexEditorState = tree.state.downcast_mut();
|
||||
if state.dragging {
|
||||
let bounds = self.scrollbar_bounds(renderer, layout);
|
||||
if let Some(pos) = cursor.position_in(bounds) {
|
||||
state.offset_y =
|
||||
-(pos.y / bounds.height * self.scroll_max(renderer, layout));
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
_layout: Layout<'_>,
|
||||
_cursor: Cursor,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &R,
|
||||
) -> Interaction {
|
||||
Interaction::None
|
||||
}
|
||||
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
_tree: &'a mut Tree,
|
||||
_layout: Layout<'a>,
|
||||
_renderer: &R,
|
||||
_viewport: &Rectangle,
|
||||
_translation: Vector,
|
||||
) -> Option<overlay::Element<'a, M, T, R>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
2356
src/lib.rs
2356
src/lib.rs
File diff suppressed because it is too large
Load Diff
340
src/main.rs
340
src/main.rs
@@ -1,24 +1,31 @@
|
||||
use std::{collections::HashMap, fmt, time::Duration};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use iced::{
|
||||
Color, Element, Font,
|
||||
Element,
|
||||
Length::{Fill, Shrink},
|
||||
Point, Rectangle, Renderer, Size, Subscription, Task, Theme, mouse,
|
||||
Point, Rectangle, Renderer, Size, Subscription, Task, Theme,
|
||||
keyboard::{self, Key, Modifiers, key::Named},
|
||||
mouse, time,
|
||||
widget::{
|
||||
Action, Canvas, button,
|
||||
self, Canvas, button,
|
||||
canvas::{Frame, Program},
|
||||
column, container, mouse_area, row, text,
|
||||
column, container, image, row,
|
||||
},
|
||||
window::{self, Id, Settings},
|
||||
};
|
||||
use nes_emu::{
|
||||
NES, PPU,
|
||||
Break, NES,
|
||||
audio::Audio,
|
||||
debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image},
|
||||
header_menu::header_menu,
|
||||
hex_view::{HexEvent, HexView},
|
||||
resize_watcher::resize_watcher,
|
||||
};
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::{io::AsyncWriteExt, runtime::Runtime};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "even_odd.nes");
|
||||
@@ -26,8 +33,19 @@ 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 = 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 = "./cpu_timing_test.nes";
|
||||
// const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.nes";
|
||||
|
||||
extern crate nes_emu;
|
||||
|
||||
@@ -44,10 +62,17 @@ fn main() -> Result<(), iced::Error> {
|
||||
.run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum MemoryTy {
|
||||
Cpu,
|
||||
PPU,
|
||||
OAM,
|
||||
}
|
||||
|
||||
impl fmt::Display for MemoryTy {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Export {self:?} Memory")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -58,6 +83,7 @@ enum WindowType {
|
||||
TileViewer,
|
||||
Palette,
|
||||
Debugger,
|
||||
Apu,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -67,6 +93,7 @@ enum HeaderButton {
|
||||
// OpenTileViewer,
|
||||
// OpenDebugger,
|
||||
Open(WindowType),
|
||||
OpenRom,
|
||||
Reset,
|
||||
PowerCycle,
|
||||
}
|
||||
@@ -76,75 +103,91 @@ impl fmt::Display for HeaderButton {
|
||||
match self {
|
||||
Self::Open(WindowType::Memory(MemoryTy::Cpu, _)) => write!(f, "Open Memory Viewer"),
|
||||
Self::Open(WindowType::Memory(MemoryTy::PPU, _)) => write!(f, "Open PPU Memory Viewer"),
|
||||
Self::Open(WindowType::Memory(MemoryTy::OAM, _)) => write!(f, "Open OAM Memory Viewer"),
|
||||
Self::Open(WindowType::TileMap) => write!(f, "Open TileMap Viewer"),
|
||||
Self::Open(WindowType::TileViewer) => write!(f, "Open Tile Viewer"),
|
||||
Self::Open(WindowType::Debugger) => write!(f, "Open Debugger"),
|
||||
Self::Open(WindowType::Palette) => write!(f, "Open Palette"),
|
||||
Self::Open(WindowType::Main) => write!(f, "Create new Main window"),
|
||||
Self::Open(WindowType::Apu) => write!(f, "Open APU info"),
|
||||
Self::Reset => write!(f, "Reset"),
|
||||
Self::PowerCycle => write!(f, "Power Cycle"),
|
||||
Self::OpenRom => write!(f, "Open ROM file"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Emulator {
|
||||
running: bool,
|
||||
nes: NES,
|
||||
windows: HashMap<Id, WindowType>,
|
||||
debugger: DebuggerState,
|
||||
main_win_size: Size,
|
||||
prev: [Instant; 2],
|
||||
audio: Audio,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
Tick(usize),
|
||||
Frame,
|
||||
DMA,
|
||||
CPU,
|
||||
DebugInt,
|
||||
OpenRom(NES),
|
||||
// Tick(usize),
|
||||
// Frame,
|
||||
// DMA,
|
||||
// CPU,
|
||||
// DebugInt,
|
||||
WindowClosed(Id),
|
||||
WindowOpened(Id),
|
||||
Key(keyboard::Event),
|
||||
Periodic(Instant),
|
||||
SetRunning(bool),
|
||||
Header(HeaderButton),
|
||||
Hex(Id, HexEvent),
|
||||
Debugger(DebuggerMessage),
|
||||
SetSize(window::Id, Size),
|
||||
Export(MemoryTy),
|
||||
}
|
||||
|
||||
impl Emulator {
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
@@ -157,15 +200,15 @@ impl Emulator {
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Tick(count) => {
|
||||
for _ in 0..count {
|
||||
self.nes.run_one_clock_cycle();
|
||||
}
|
||||
}
|
||||
Message::Frame => while !self.nes.run_one_clock_cycle().ppu_frame {},
|
||||
Message::DMA => while !self.nes.run_one_clock_cycle().dma {},
|
||||
Message::CPU => while !self.nes.run_one_clock_cycle().cpu_exec {},
|
||||
Message::DebugInt => while !self.nes.run_one_clock_cycle().dbg_int {},
|
||||
// Message::Tick(count) => {
|
||||
// for _ in 0..count {
|
||||
// self.nes.run_one_clock_cycle();
|
||||
// }
|
||||
// }
|
||||
// Message::Frame => while !self.nes.run_one_clock_cycle().ppu_frame {},
|
||||
// Message::DMA => while !self.nes.run_one_clock_cycle().dma {},
|
||||
// Message::CPU => while !self.nes.run_one_clock_cycle().cpu_exec {},
|
||||
// Message::DebugInt => while !self.nes.run_one_clock_cycle().dbg_int {},
|
||||
Message::WindowClosed(id) => {
|
||||
if let Some(WindowType::Main) = self.windows.remove(&id) {
|
||||
return iced::exit();
|
||||
@@ -180,6 +223,22 @@ impl Emulator {
|
||||
Message::Header(HeaderButton::Open(w)) => {
|
||||
return self.open(w);
|
||||
}
|
||||
Message::Header(HeaderButton::OpenRom) => {
|
||||
return Task::future(
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_directory(".")
|
||||
.add_filter("NES", &["nes"])
|
||||
.set_title("Open NES Rom file")
|
||||
.pick_file(),
|
||||
)
|
||||
.and_then(|p| {
|
||||
Task::future(async move {
|
||||
// println!("Opening: {}", p.path().display());
|
||||
NES::async_load_nes_file(p.path()).await.ok()
|
||||
})
|
||||
})
|
||||
.and_then(|n| Task::done(Message::OpenRom(n)));
|
||||
}
|
||||
Message::Hex(id, val) => {
|
||||
if let Some(WindowType::Memory(_, view)) = self.windows.get_mut(&id) {
|
||||
return view.update(val).map(move |e| Message::Hex(id, e));
|
||||
@@ -203,6 +262,133 @@ impl Emulator {
|
||||
})
|
||||
.then(move |_| iced::window::resize(id, size));
|
||||
}
|
||||
Message::OpenRom(nes) => {
|
||||
self.nes = nes;
|
||||
self.nes.power_cycle();
|
||||
}
|
||||
Message::Export(ty) => {
|
||||
let raw: Vec<_> = match ty {
|
||||
MemoryTy::Cpu => (0..=0xFFFF)
|
||||
.map(|i| self.nes.mem().peek_cpu(i).unwrap_or(0))
|
||||
.collect(),
|
||||
MemoryTy::PPU => (0..=0xFFFF)
|
||||
.map(|i| self.nes.mem().peek_ppu(i).unwrap_or(0))
|
||||
.collect(),
|
||||
MemoryTy::OAM => (0..=0xFF).map(|i| self.nes.ppu().peek_oam(i)).collect(),
|
||||
};
|
||||
return Task::future(async move {
|
||||
if let Some(file) = rfd::AsyncFileDialog::new()
|
||||
.set_directory(".")
|
||||
.set_file_name("output.dmp")
|
||||
.set_title("Save memory dump")
|
||||
.save_file()
|
||||
.await
|
||||
{
|
||||
tokio::fs::File::create(file.path())
|
||||
.await
|
||||
.expect("Failed to save file")
|
||||
.write_all(&raw)
|
||||
.await
|
||||
.expect("Failed to write dump");
|
||||
}
|
||||
})
|
||||
.discard();
|
||||
}
|
||||
Message::Key(key) => match key {
|
||||
keyboard::Event::KeyPressed {
|
||||
key: Key::Character(val),
|
||||
modifiers: Modifiers::CTRL,
|
||||
repeat: false,
|
||||
..
|
||||
} => {
|
||||
if val == "t" {
|
||||
self.nes.reset();
|
||||
}
|
||||
}
|
||||
keyboard::Event::KeyPressed {
|
||||
key,
|
||||
modifiers: Modifiers::NONE,
|
||||
repeat: false,
|
||||
..
|
||||
} => {
|
||||
if key == Key::Character("z".into()) {
|
||||
self.nes.controller_1().set_a(true);
|
||||
} else if key == Key::Character("x".into()) {
|
||||
self.nes.controller_1().set_b(true);
|
||||
} else if key == Key::Character("a".into()) {
|
||||
self.nes.controller_1().set_select(true);
|
||||
} else if key == Key::Character("s".into()) {
|
||||
self.nes.controller_1().set_start(true);
|
||||
} else if key == Key::Named(Named::ArrowDown) {
|
||||
self.nes.controller_1().set_down(true);
|
||||
} else if key == Key::Named(Named::ArrowUp) {
|
||||
self.nes.controller_1().set_up(true);
|
||||
} else if key == Key::Named(Named::ArrowLeft) {
|
||||
self.nes.controller_1().set_left(true);
|
||||
} else if key == Key::Named(Named::ArrowRight) {
|
||||
self.nes.controller_1().set_right(true);
|
||||
} else if key == Key::Named(Named::Play) || key == Key::Named(Named::MediaPlay)
|
||||
{
|
||||
self.running = true;
|
||||
} else if key == Key::Named(Named::Pause)
|
||||
|| key == Key::Named(Named::MediaPause)
|
||||
{
|
||||
self.running = false;
|
||||
} else if key == Key::Named(Named::MediaPlayPause)
|
||||
|| key == Key::Named(Named::Space)
|
||||
{
|
||||
self.running = !self.running;
|
||||
}
|
||||
}
|
||||
keyboard::Event::KeyReleased {
|
||||
key,
|
||||
modifiers: Modifiers::NONE,
|
||||
..
|
||||
} => {
|
||||
if key == Key::Character("z".into()) {
|
||||
self.nes.controller_1().set_a(false);
|
||||
} else if key == Key::Character("x".into()) {
|
||||
self.nes.controller_1().set_b(false);
|
||||
} else if key == Key::Character("a".into()) {
|
||||
self.nes.controller_1().set_select(false);
|
||||
} else if key == Key::Character("s".into()) {
|
||||
self.nes.controller_1().set_start(false);
|
||||
} else if key == Key::Named(Named::ArrowDown) {
|
||||
self.nes.controller_1().set_down(false);
|
||||
} else if key == Key::Named(Named::ArrowUp) {
|
||||
self.nes.controller_1().set_up(false);
|
||||
} else if key == Key::Named(Named::ArrowLeft) {
|
||||
self.nes.controller_1().set_left(false);
|
||||
} else if key == Key::Named(Named::ArrowRight) {
|
||||
self.nes.controller_1().set_right(false);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Message::Periodic(_i) => {
|
||||
if self.running {
|
||||
// TODO: Smarter frame skip
|
||||
if self.prev[0].elapsed() >= Duration::from_millis(2) {
|
||||
self.nes.run_one_clock_cycle(&Break::default());
|
||||
let mut count = 0;
|
||||
while !self.nes.run_one_clock_cycle(&Break::default()).ppu_frame
|
||||
&& !self.nes.halted()
|
||||
{
|
||||
count += 1;
|
||||
if count > 100000 {
|
||||
println!("Loop overran...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.prev[0] = Instant::now();
|
||||
self.audio.submit(self.nes.apu().get_frame_samples());
|
||||
self.nes.apu_mut().reset_frame_samples();
|
||||
}
|
||||
} else {
|
||||
self.audio.pause();
|
||||
}
|
||||
}
|
||||
Message::SetRunning(running) => self.running = running,
|
||||
}
|
||||
// self.image.0.clone_from(self.nes.image());
|
||||
Task::none()
|
||||
@@ -212,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![
|
||||
@@ -234,25 +423,43 @@ impl Emulator {
|
||||
Some(WindowType::Memory(ty, view)) => {
|
||||
let hex = match ty {
|
||||
MemoryTy::Cpu => view
|
||||
.render(self.nes.cpu_mem())
|
||||
.render_cpu(self.nes.mem())
|
||||
.map(move |e| Message::Hex(win, e)),
|
||||
MemoryTy::PPU => view
|
||||
.render(self.nes.ppu().mem())
|
||||
.render_ppu(self.nes.mem(), self.nes.ppu())
|
||||
.map(move |e| Message::Hex(win, e)),
|
||||
MemoryTy::OAM => view
|
||||
.render_oam(self.nes.ppu())
|
||||
.map(move |e| Message::Hex(win, e)),
|
||||
};
|
||||
let content = column![hex].width(Fill).height(Fill);
|
||||
let content = column![row![header_menu("Export", [*ty], Message::Export)], hex]
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
container(content).width(Fill).height(Fill).into()
|
||||
}
|
||||
Some(WindowType::TileMap) => dbg_image(DbgImage::NameTable(self.nes.ppu())).into(),
|
||||
Some(WindowType::TileViewer) => {
|
||||
dbg_image(DbgImage::PatternTable(self.nes.ppu())).into()
|
||||
Some(WindowType::TileMap) => {
|
||||
dbg_image(DbgImage::NameTable(self.nes.mem(), self.nes.ppu())).into()
|
||||
}
|
||||
Some(WindowType::Palette) => dbg_image(DbgImage::Palette(self.nes.ppu())).into(),
|
||||
Some(WindowType::Debugger) => {
|
||||
container(self.debugger.view(&self.nes).map(Message::Debugger))
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into()
|
||||
Some(WindowType::TileViewer) => {
|
||||
dbg_image(DbgImage::PatternTable(self.nes.mem(), self.nes.ppu())).into()
|
||||
}
|
||||
Some(WindowType::Palette) => {
|
||||
dbg_image(DbgImage::Palette(self.nes.mem(), self.nes.ppu())).into()
|
||||
}
|
||||
Some(WindowType::Debugger) => column![
|
||||
row![
|
||||
button(image("./images/ic_fluent_play_24_filled.png"))
|
||||
.on_press(Message::SetRunning(true)),
|
||||
button(image("./images/ic_fluent_pause_24_filled.png"))
|
||||
.on_press(Message::SetRunning(false)),
|
||||
],
|
||||
self.debugger.view(&self.nes).map(Message::Debugger)
|
||||
]
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into(),
|
||||
Some(WindowType::Apu) => {
|
||||
self.nes.apu().view()
|
||||
}
|
||||
None => panic!("Window not found"),
|
||||
// _ => todo!(),
|
||||
@@ -263,7 +470,11 @@ impl Emulator {
|
||||
row![
|
||||
header_menu(
|
||||
"Console",
|
||||
[HeaderButton::Reset, HeaderButton::PowerCycle,],
|
||||
[
|
||||
HeaderButton::OpenRom,
|
||||
HeaderButton::Reset,
|
||||
HeaderButton::PowerCycle,
|
||||
],
|
||||
Message::Header
|
||||
),
|
||||
header_menu(
|
||||
@@ -272,9 +483,11 @@ impl Emulator {
|
||||
HeaderButton::Open(WindowType::Debugger),
|
||||
HeaderButton::Open(WindowType::Memory(MemoryTy::Cpu, HexView {})),
|
||||
HeaderButton::Open(WindowType::Memory(MemoryTy::PPU, HexView {})),
|
||||
HeaderButton::Open(WindowType::Memory(MemoryTy::OAM, HexView {})),
|
||||
HeaderButton::Open(WindowType::TileMap),
|
||||
HeaderButton::Open(WindowType::TileViewer),
|
||||
HeaderButton::Open(WindowType::Palette),
|
||||
HeaderButton::Open(WindowType::Apu),
|
||||
],
|
||||
Message::Header
|
||||
)
|
||||
@@ -295,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()]
|
||||
}
|
||||
}
|
||||
|
||||
712
src/mem.rs
712
src/mem.rs
@@ -1,12 +1,18 @@
|
||||
use crate::{hex_view::Memory, ppu::PPUMMRegisters};
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
pub enum Value<'a, R> {
|
||||
use crate::{
|
||||
CPUMMRegisters, PPU, apu::APU, controllers::Controllers, cpu::DmaState, hex_view::Memory,
|
||||
ppu::PPUMMRegisters,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Value<R> {
|
||||
Value(u8),
|
||||
Register { reg: &'a R, offset: u16 },
|
||||
Register { reg: R, offset: u16 },
|
||||
}
|
||||
|
||||
impl<R> Value<'_, R> {
|
||||
pub fn reg_map(self, f: impl FnOnce(&R, u16) -> u8) -> u8 {
|
||||
impl<R> Value<R> {
|
||||
pub fn reg_map(self, f: impl FnOnce(R, u16) -> u8) -> u8 {
|
||||
match self {
|
||||
Value::Value(v) => v,
|
||||
Value::Register { reg, offset } => f(reg, offset),
|
||||
@@ -14,23 +20,29 @@ impl<R> Value<'_, R> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Data<R> {
|
||||
RAM(Vec<u8>),
|
||||
ROM(Vec<u8>),
|
||||
Mirror(usize),
|
||||
ROM(Arc<[u8]>),
|
||||
Mirror(u16),
|
||||
Reg(R),
|
||||
// Disabled(),
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Segment<R> {
|
||||
name: &'static str,
|
||||
name: SegmentId,
|
||||
position: u16,
|
||||
size: u16,
|
||||
mem: Data<R>,
|
||||
}
|
||||
|
||||
impl<R> Segment<R> {
|
||||
pub fn ram(name: &'static str, position: u16, size: u16) -> Self {
|
||||
fn is(&self, id: SegmentId) -> bool {
|
||||
self.name == id
|
||||
}
|
||||
|
||||
fn ram(name: SegmentId, position: u16, size: u16) -> Self {
|
||||
Self {
|
||||
name,
|
||||
position,
|
||||
@@ -38,15 +50,19 @@ impl<R> Segment<R> {
|
||||
mem: Data::RAM(vec![0u8; size as usize]),
|
||||
}
|
||||
}
|
||||
pub fn rom(name: &'static str, position: u16, raw: &[u8]) -> Self {
|
||||
fn rom<Raw>(name: SegmentId, position: u16, raw: Raw) -> Self
|
||||
where
|
||||
Arc<[u8]>: From<Raw>,
|
||||
{
|
||||
let raw = Arc::from(raw);
|
||||
Self {
|
||||
name,
|
||||
position,
|
||||
size: raw.len() as u16,
|
||||
mem: Data::ROM(Vec::from(raw)),
|
||||
mem: Data::ROM(raw),
|
||||
}
|
||||
}
|
||||
pub fn reg(name: &'static str, position: u16, size: u16, reg: R) -> Self {
|
||||
fn reg(name: SegmentId, position: u16, size: u16, reg: R) -> Self {
|
||||
Self {
|
||||
name,
|
||||
position,
|
||||
@@ -54,7 +70,7 @@ impl<R> Segment<R> {
|
||||
mem: Data::Reg(reg),
|
||||
}
|
||||
}
|
||||
pub fn mirror(name: &'static str, position: u16, size: u16, of: usize) -> Self {
|
||||
fn mirror(name: SegmentId, position: u16, size: u16, of: u16) -> Self {
|
||||
Self {
|
||||
name,
|
||||
position,
|
||||
@@ -62,21 +78,46 @@ impl<R> Segment<R> {
|
||||
mem: Data::Mirror(of),
|
||||
}
|
||||
}
|
||||
|
||||
// fn take_rom(&mut self) -> Vec<u8> {
|
||||
// match std::mem::replace(&mut self.mem, Data::Disabled) {
|
||||
// Data::ROM(items) => items,
|
||||
// _ => panic!("Cannot take rom since memory is not rom"),
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn set_rom(&mut self, rom: Vec<u8>) {
|
||||
// assert!(
|
||||
// matches!(self.mem, Data::Disabled),
|
||||
// "Cannot set non-disabled memory to rom"
|
||||
// );
|
||||
// self.mem = Data::ROM(rom);
|
||||
// }
|
||||
|
||||
fn swap_rom(&mut self, rom: Arc<[u8]>) {
|
||||
match &mut self.mem {
|
||||
Data::ROM(items) => *items = rom,
|
||||
_ => panic!("Cannot swap rom since memory is not rom"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn power_cycle(&mut self) {
|
||||
match &mut self.mem {
|
||||
Data::RAM(v) => v.fill(0),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryMap<R> {
|
||||
edit_ver: usize,
|
||||
segments: Vec<Segment<R>>,
|
||||
// map: Remapper,
|
||||
}
|
||||
|
||||
impl<R> MemoryMap<R> {
|
||||
pub fn new(segments: Vec<Segment<R>>) -> Self {
|
||||
Self {
|
||||
edit_ver: 0,
|
||||
segments,
|
||||
}
|
||||
}
|
||||
pub fn read(&self, addr: u16) -> Value<'_, R> {
|
||||
impl<R: Copy> MemoryMap<R> {
|
||||
pub fn read(&self, addr: u16) -> Value<R> {
|
||||
// self.edit_ver += 1;
|
||||
for segment in &self.segments {
|
||||
if segment.position <= addr && addr - segment.position < segment.size {
|
||||
@@ -84,14 +125,16 @@ impl<R> MemoryMap<R> {
|
||||
Data::RAM(items) => Value::Value(items[(addr - segment.position) as usize]),
|
||||
Data::ROM(items) => Value::Value(items[(addr - segment.position) as usize]),
|
||||
Data::Reg(reg) => Value::Register {
|
||||
reg,
|
||||
reg: *reg,
|
||||
offset: addr - segment.position,
|
||||
},
|
||||
Data::Mirror(index) => {
|
||||
Data::Mirror(pos) => {
|
||||
let offset = addr - segment.position;
|
||||
let s = &self.segments[*index];
|
||||
self.read(s.position + offset % s.size)
|
||||
} // Data::Disabled() => todo!(),
|
||||
self.read(pos + offset)
|
||||
// let s = &self.segments[*index];
|
||||
// self.read(s.position + offset % s.size)
|
||||
}
|
||||
Data::Disabled => todo!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -100,26 +143,32 @@ impl<R> MemoryMap<R> {
|
||||
// todo!("Open bus")
|
||||
}
|
||||
|
||||
pub fn write(&mut self, addr: u16, val: u8, reg_fn: impl FnOnce(&R, u16, u8)) {
|
||||
pub fn write(&mut self, addr: u16, val: u8) -> Option<(R, u16, u8)> {
|
||||
self.edit_ver += 1;
|
||||
for segment in &mut self.segments {
|
||||
if segment.position <= addr && addr - segment.position < segment.size {
|
||||
return match &mut segment.mem {
|
||||
Data::RAM(items) => {
|
||||
items[(addr - segment.position) as usize] = val;
|
||||
None
|
||||
}
|
||||
Data::ROM(_items) => (),
|
||||
Data::Reg(reg) => reg_fn(reg, addr - segment.position, val),
|
||||
Data::Mirror(index) => {
|
||||
Data::ROM(_items) => None,
|
||||
Data::Reg(reg) => Some((*reg, addr - segment.position, val)),
|
||||
Data::Mirror(pos) => {
|
||||
let pos = *pos;
|
||||
let offset = addr - segment.position;
|
||||
let index = *index;
|
||||
let s = &self.segments[index];
|
||||
self.write(s.position + offset % s.size, val, reg_fn)
|
||||
} // Data::Disabled() => todo!(),
|
||||
self.write(pos + offset, val)
|
||||
// let index = *index;
|
||||
// let s = &self.segments[index];
|
||||
// self.write(s.position + offset % s.size, val)
|
||||
}
|
||||
Data::Disabled => todo!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
todo!("Open bus")
|
||||
// iirc, open bus just drops writes
|
||||
None
|
||||
// todo!("Open bus (${addr:04X})")
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
@@ -131,120 +180,525 @@ impl<R> MemoryMap<R> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rom(&self, idx: usize) -> Option<&[u8]> {
|
||||
if let Some(Segment {
|
||||
mem: Data::ROM(val),
|
||||
..
|
||||
}) = self.segments.get(idx)
|
||||
{
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
pub fn power_cycle(&mut self) -> Self {
|
||||
for seg in &mut self.segments {
|
||||
seg.power_cycle();
|
||||
}
|
||||
let segments = std::mem::take(&mut self.segments);
|
||||
Self {
|
||||
edit_ver: self.edit_ver + 1,
|
||||
segments,
|
||||
// map: self.map.power_cycle(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rom_or_ram(&self, idx: usize) -> Option<&[u8]> {
|
||||
if let Some(Segment {
|
||||
mem: Data::ROM(val),
|
||||
..
|
||||
}) = self.segments.get(idx)
|
||||
{
|
||||
Some(val)
|
||||
} else if let Some(Segment {
|
||||
mem: Data::RAM(_), ..
|
||||
}) = self.segments.get(idx)
|
||||
{
|
||||
Some(&[])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn with_peek_reg<'s>(&'s self, f: impl (Fn(&R, u16) -> Option<u8>) + 's) -> impl Memory + 's {
|
||||
// struct MemImpl<'a, R>(&'a MemoryMap<R>, Box<dyn Fn(&R, u16) -> Option<u8> + 'a>);
|
||||
// impl<R> Memory for MemImpl<'_, R> {
|
||||
// fn peek(&self, val: u16) -> Option<u8> {
|
||||
// match self.0.read(val) {
|
||||
// Value::Value(v) => Some(v),
|
||||
// Value::Register { reg, offset } => self.1(reg, offset),
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn edit_ver(&self) -> usize {
|
||||
// self.0.edit_ver()
|
||||
// }
|
||||
// }
|
||||
// MemImpl(self, Box::new(f))
|
||||
// }
|
||||
}
|
||||
|
||||
impl<R> Memory for MemoryMap<R> {
|
||||
fn peek(&self, addr: u16) -> Option<u8> {
|
||||
for segment in &self.segments {
|
||||
if segment.position <= addr && addr - segment.position < segment.size {
|
||||
return match &segment.mem {
|
||||
Data::RAM(items) => Some(items[(addr - segment.position) as usize]),
|
||||
Data::ROM(items) => Some(items[(addr - segment.position) as usize]),
|
||||
Data::Reg(_) => None,
|
||||
Data::Mirror(index) => {
|
||||
let offset = addr - segment.position;
|
||||
let s = &self.segments[*index];
|
||||
self.peek(s.position + offset % s.size)
|
||||
} // Data::Disabled() => todo!(),
|
||||
};
|
||||
fn find(&mut self, id: SegmentId) -> Option<&mut Segment<R>> {
|
||||
for s in &mut self.segments {
|
||||
if s.is(id) {
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn peek_val(&self, addr: u16) -> Result<u8, Option<(R, u16)>> {
|
||||
for segment in &self.segments {
|
||||
if segment.position <= addr && addr - segment.position < segment.size {
|
||||
return match &segment.mem {
|
||||
Data::RAM(items) => Ok(items[(addr - segment.position) as usize]),
|
||||
Data::ROM(items) => Ok(items[(addr - segment.position) as usize]),
|
||||
Data::Reg(r) => Err(Some((*r, addr - segment.position))),
|
||||
Data::Mirror(pos) => {
|
||||
let offset = addr - segment.position;
|
||||
self.peek_val(pos + offset)
|
||||
}
|
||||
Data::Disabled => Err(None),
|
||||
};
|
||||
}
|
||||
}
|
||||
Err(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Copy> Memory for MemoryMap<R> {
|
||||
fn peek(&self, addr: usize) -> Option<u8> {
|
||||
self.peek_val(addr as u16).ok()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.segments
|
||||
.iter()
|
||||
.map(|s| s.position as usize + s.size as usize)
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn edit_ver(&self) -> usize {
|
||||
self.edit_ver
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Mapper {
|
||||
struct Mapper {
|
||||
horizontal_name_table: bool,
|
||||
mapper: u16,
|
||||
sub_mapper: u8,
|
||||
prg_rom_size: u8,
|
||||
chr_rom_size: u8,
|
||||
}
|
||||
|
||||
impl Mapper {
|
||||
pub fn from_flags(flags: u8) -> Self {
|
||||
if flags & 0b11111110 != 0 {
|
||||
todo!("Support other mapper flags");
|
||||
}
|
||||
Self {
|
||||
horizontal_name_table: flags & (1 << 0) == 1,
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum SegmentId {
|
||||
#[allow(unused)]
|
||||
TestRam,
|
||||
|
||||
pub(crate) fn ppu_map(&self, rom: &[u8]) -> MemoryMap<PPUMMRegisters> {
|
||||
let chr = if rom.len() == 0 {
|
||||
Segment::ram("CHR RAM", 0x0000, 0x2000)
|
||||
InternalRam,
|
||||
InternalRamMirror,
|
||||
PpuRegisters,
|
||||
ApuIoRegisters,
|
||||
PrgRom,
|
||||
|
||||
Nametable0,
|
||||
Nametable1,
|
||||
Nametable2,
|
||||
Nametable3,
|
||||
VramMirror,
|
||||
PaletteControl,
|
||||
PaletteMirror,
|
||||
PpuRom,
|
||||
PpuRam,
|
||||
|
||||
// CpuBank(u32),
|
||||
PrgBank0,
|
||||
PrgBank1,
|
||||
ChrBank0,
|
||||
ChrBank1,
|
||||
}
|
||||
|
||||
impl fmt::Display for SegmentId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mapped {
|
||||
cpu: MemoryMap<CPUMMRegisters>,
|
||||
ppu: MemoryMap<PPUMMRegisters>,
|
||||
mapper: Remapper,
|
||||
}
|
||||
|
||||
impl Mapped {
|
||||
pub fn from_rom(rom: &[u8]) -> Self {
|
||||
let (header, rom) = rom.split_at(0x10);
|
||||
let nes_20 = header[7] & 0x0C == 0x08;
|
||||
// assert!(nes_20, "Only supports nes 2.0 format");
|
||||
// if header[6] & 0b11111110 != 0 {
|
||||
// todo!("Support other mapper flags, {:08b}", header[6]);
|
||||
// }
|
||||
let mapper = if nes_20 {
|
||||
assert_eq!(header[9], 0, "No support for larger PRG/CHR roms");
|
||||
assert_eq!(header[10], 0, "No support for PRG-RAM/EEPROM");
|
||||
assert_eq!(header[11], 0, "No support for CHR-RAM");
|
||||
assert!(header[12] == 0 || header[12] == 2, "Only support NTSC NES");
|
||||
Mapper {
|
||||
horizontal_name_table: header[6] & (1 << 0) == 1,
|
||||
mapper: ((header[6] as u16 & 0xF0) >> 4)
|
||||
| ((header[7] as u16 & 0xF0) >> 0)
|
||||
| ((header[8] as u16 & 0x0F) << 8),
|
||||
sub_mapper: (header[8] & 0xF0) >> 4,
|
||||
prg_rom_size: header[4],
|
||||
chr_rom_size: header[5],
|
||||
}
|
||||
} else {
|
||||
Segment::rom("CHR ROM", 0x0000, rom)
|
||||
Mapper {
|
||||
horizontal_name_table: header[6] & (1 << 0) == 1,
|
||||
mapper: ((header[6] as u16 & 0xF0) >> 4) | ((header[7] as u16 & 0xF0) >> 0),
|
||||
sub_mapper: 0,
|
||||
prg_rom_size: header[4],
|
||||
chr_rom_size: header[5],
|
||||
}
|
||||
};
|
||||
if self.horizontal_name_table {
|
||||
MemoryMap::new(vec![
|
||||
chr,
|
||||
Segment::ram("Internal VRAM", 0x2000, 0x400),
|
||||
Segment::ram("Internal VRAM", 0x2400, 0x400),
|
||||
Segment::mirror("Internal VRAM", 0x2800, 0x400, 1),
|
||||
Segment::mirror("Internal VRAM", 0x2C00, 0x400, 2),
|
||||
Segment::mirror("Mirror of VRAM", 0x3000, 0x0F00, 1),
|
||||
Segment::reg("Palette Control", 0x3F00, 0x0020, PPUMMRegisters::Palette),
|
||||
Segment::mirror("Mirror of Palette", 0x3F20, 0x00E0, 6),
|
||||
])
|
||||
let (prg_rom, rom) = rom.split_at(mapper.prg_rom_size as usize * 0x4000);
|
||||
let (chr_rom, rom) = rom.split_at(mapper.chr_rom_size as usize * 0x2000);
|
||||
// assert_eq!(rom.len(), 0);
|
||||
// let prg_rom = &file[self.cpu_offset()..self.ppu_offset()];
|
||||
let mut cpu_segments = vec![
|
||||
Segment::ram(SegmentId::InternalRam, 0x0000, 0x0800),
|
||||
Segment::mirror(SegmentId::InternalRamMirror, 0x0800, 0x1800, 0x0000),
|
||||
Segment::reg(SegmentId::PpuRegisters, 0x2000, 0x0008, CPUMMRegisters::PPU),
|
||||
Segment::reg(
|
||||
SegmentId::ApuIoRegisters,
|
||||
0x4000,
|
||||
0x0018,
|
||||
CPUMMRegisters::APU,
|
||||
),
|
||||
];
|
||||
let mut ppu_segments = vec![
|
||||
Segment::mirror(SegmentId::VramMirror, 0x3000, 0x0F00, 0x0000),
|
||||
Segment::reg(
|
||||
SegmentId::PaletteControl,
|
||||
0x3F00,
|
||||
0x0020,
|
||||
PPUMMRegisters::Palette,
|
||||
),
|
||||
Segment::mirror(SegmentId::PaletteMirror, 0x3F20, 0x00E0, 0x3F00),
|
||||
];
|
||||
if mapper.horizontal_name_table {
|
||||
ppu_segments.push(Segment::ram(SegmentId::Nametable0, 0x2000, 0x400));
|
||||
ppu_segments.push(Segment::ram(SegmentId::Nametable1, 0x2400, 0x400));
|
||||
ppu_segments.push(Segment::mirror(
|
||||
SegmentId::Nametable2,
|
||||
0x2800,
|
||||
0x400,
|
||||
0x2000,
|
||||
));
|
||||
ppu_segments.push(Segment::mirror(
|
||||
SegmentId::Nametable3,
|
||||
0x2C00,
|
||||
0x400,
|
||||
0x2400,
|
||||
));
|
||||
} else {
|
||||
MemoryMap::new(vec![
|
||||
chr,
|
||||
Segment::ram("Internal VRAM", 0x2000, 0x400),
|
||||
Segment::mirror("Internal VRAM", 0x2400, 0x400, 1),
|
||||
Segment::ram("Internal VRAM", 0x2800, 0x400),
|
||||
Segment::mirror("Internal VRAM", 0x2C00, 0x400, 3),
|
||||
Segment::mirror("Mirror of VRAM", 0x3000, 0x0F00, 1),
|
||||
Segment::reg("Palette Control", 0x3F00, 0x0020, PPUMMRegisters::Palette),
|
||||
Segment::mirror("Mirror of Palette", 0x3F20, 0x00E0, 6),
|
||||
])
|
||||
ppu_segments.push(Segment::ram(SegmentId::Nametable0, 0x2000, 0x400));
|
||||
ppu_segments.push(Segment::mirror(
|
||||
SegmentId::Nametable1,
|
||||
0x2400,
|
||||
0x400,
|
||||
0x2000,
|
||||
));
|
||||
ppu_segments.push(Segment::ram(SegmentId::Nametable2, 0x2800, 0x400));
|
||||
ppu_segments.push(Segment::mirror(
|
||||
SegmentId::Nametable3,
|
||||
0x2C00,
|
||||
0x400,
|
||||
0x2800,
|
||||
));
|
||||
}
|
||||
let remap = if mapper.mapper == 0 {
|
||||
cpu_segments.push(Segment::rom(
|
||||
SegmentId::PrgRom,
|
||||
0x8000 + (0x8000 - prg_rom.len() as u16),
|
||||
prg_rom,
|
||||
));
|
||||
if chr_rom.len() == 0 {
|
||||
ppu_segments.push(Segment::ram(SegmentId::PpuRam, 0, 0x2000));
|
||||
} else {
|
||||
ppu_segments.push(Segment::rom(SegmentId::PpuRom, 0, chr_rom));
|
||||
}
|
||||
Remapper::None
|
||||
} else if mapper.mapper == 1 && mapper.sub_mapper == 0 {
|
||||
let prg_banks: Vec<Arc<[u8]>> =
|
||||
prg_rom.chunks(0x4000).map(|ch| Arc::from(ch)).collect();
|
||||
for (i, b) in prg_banks.iter().enumerate() {
|
||||
println!("{i}: {:X?}", &b[0x3FF0..]);
|
||||
}
|
||||
cpu_segments.push(Segment::rom(
|
||||
SegmentId::PrgBank0,
|
||||
0x8000,
|
||||
prg_banks.first().unwrap().clone(),
|
||||
));
|
||||
cpu_segments.push(Segment::rom(
|
||||
SegmentId::PrgBank1,
|
||||
0xC000,
|
||||
prg_banks.last().unwrap().clone(),
|
||||
));
|
||||
println!("CHR_ROM: {}", chr_rom.len());
|
||||
let chr_banks: Vec<Arc<[u8]>> =
|
||||
chr_rom.chunks(0x1000).map(|ch| Arc::from(ch)).collect();
|
||||
if chr_rom.len() == 0 {
|
||||
ppu_segments.push(Segment::ram(SegmentId::PpuRam, 0, 0x2000));
|
||||
} else {
|
||||
ppu_segments.push(Segment::rom(
|
||||
SegmentId::ChrBank0,
|
||||
0x0000,
|
||||
chr_banks[0].clone(),
|
||||
));
|
||||
ppu_segments.push(Segment::rom(
|
||||
SegmentId::ChrBank1,
|
||||
0x1000,
|
||||
chr_banks[1].clone(),
|
||||
));
|
||||
}
|
||||
Remapper::MMC1 {
|
||||
shift_reg: 0,
|
||||
count: 0,
|
||||
prg_banks,
|
||||
chr_banks,
|
||||
mode: MMC1Mode::LastFixed,
|
||||
cur_prg_bank: 0,
|
||||
}
|
||||
} else {
|
||||
todo!()
|
||||
};
|
||||
Self {
|
||||
cpu: MemoryMap {
|
||||
edit_ver: 0,
|
||||
segments: cpu_segments,
|
||||
},
|
||||
ppu: MemoryMap {
|
||||
edit_ver: 0,
|
||||
segments: ppu_segments,
|
||||
},
|
||||
mapper: remap,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn power_cylce(&mut self) -> Self {
|
||||
// TODO: mapper needs to reset cpu and ppu mem maps
|
||||
let mut cpu = self.cpu.power_cycle();
|
||||
let mut ppu = self.ppu.power_cycle();
|
||||
Self {
|
||||
mapper: self.mapper.power_cycle(&mut cpu, &mut ppu),
|
||||
cpu,
|
||||
ppu,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek_ppu(&self, addr: u16) -> Result<u8, Option<(PPUMMRegisters, u16)>> {
|
||||
self.ppu.peek_val(addr)
|
||||
}
|
||||
pub fn ppu_edit_ver(&self) -> usize {
|
||||
self.ppu.edit_ver
|
||||
}
|
||||
pub fn peek_cpu(&self, addr: u16) -> Option<u8> {
|
||||
self.cpu.peek_val(addr).ok()
|
||||
}
|
||||
pub fn cpu_edit_ver(&self) -> usize {
|
||||
self.cpu.edit_ver
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn test_ram() -> Self {
|
||||
Self {
|
||||
cpu: MemoryMap {
|
||||
edit_ver: 0,
|
||||
segments: vec![Segment::ram(SegmentId::TestRam, 0, 0x8000)],
|
||||
},
|
||||
ppu: MemoryMap {
|
||||
edit_ver: 0,
|
||||
segments: vec![Segment::ram(SegmentId::TestRam, 0, 0x8000)],
|
||||
},
|
||||
mapper: Remapper::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CpuMem<'a> {
|
||||
mem: &'a mut Mapped,
|
||||
ppu: &'a mut PPU,
|
||||
apu: &'a mut APU,
|
||||
dma: &'a mut DmaState,
|
||||
controllers: &'a mut Controllers,
|
||||
}
|
||||
|
||||
impl<'a> CpuMem<'a> {
|
||||
pub fn new(
|
||||
mem: &'a mut Mapped,
|
||||
ppu: &'a mut PPU,
|
||||
apu: &'a mut APU,
|
||||
dma: &'a mut DmaState,
|
||||
controllers: &'a mut Controllers,
|
||||
) -> Self {
|
||||
Self {
|
||||
mem,
|
||||
ppu,
|
||||
apu,
|
||||
dma,
|
||||
controllers,
|
||||
}
|
||||
}
|
||||
pub fn read(&mut self, addr: u16) -> u8 {
|
||||
match self.mem.mapper {
|
||||
_ => (),
|
||||
}
|
||||
match self.mem.cpu.read(addr) {
|
||||
Value::Value(v) => v,
|
||||
Value::Register { reg, offset } => match reg {
|
||||
CPUMMRegisters::PPU => self.ppu.read_reg(&mut PpuMem::new(self.mem), offset),
|
||||
CPUMMRegisters::APU => {
|
||||
if offset == 0x014 {
|
||||
todo!("OAM DMA")
|
||||
} else if offset == 0x16 {
|
||||
self.controllers.read_joy1()
|
||||
} else if offset == 0x17 {
|
||||
self.controllers.read_joy2()
|
||||
} else {
|
||||
self.apu.read_reg(offset)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn write(&mut self, addr: u16, val: u8) {
|
||||
match &mut self.mem.mapper {
|
||||
Remapper::MMC1 {
|
||||
shift_reg,
|
||||
count,
|
||||
mode,
|
||||
prg_banks,
|
||||
chr_banks,
|
||||
cur_prg_bank,
|
||||
} if addr & 0x8000 != 0 => {
|
||||
if val & 0x80 != 0 {
|
||||
*shift_reg = 0;
|
||||
*count = 0;
|
||||
} else if *count == 4 {
|
||||
let val = (*shift_reg << 1) | (val & 0x01);
|
||||
if (addr & 0x6000) >> 13 == 0 {
|
||||
// TODO: fix mem layout if it's changed
|
||||
if val & 0b01100 == 0b01000 {
|
||||
*mode = MMC1Mode::FirstFixed;
|
||||
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
|
||||
bank0.swap_rom(prg_banks[0].clone());
|
||||
let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap();
|
||||
bank1.swap_rom(prg_banks[*cur_prg_bank].clone());
|
||||
} else if val & 0b01100 == 0b01100 {
|
||||
*mode = MMC1Mode::LastFixed;
|
||||
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
|
||||
bank0.swap_rom(prg_banks[*cur_prg_bank].clone());
|
||||
let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap();
|
||||
bank1.swap_rom(prg_banks.last().unwrap().clone());
|
||||
} else {
|
||||
*mode = MMC1Mode::Full;
|
||||
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
|
||||
bank0.swap_rom(prg_banks[*cur_prg_bank & !1].clone());
|
||||
let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap();
|
||||
bank1.swap_rom(prg_banks[(*cur_prg_bank & !1) + 1].clone());
|
||||
}
|
||||
if val & 0b10000 == 0b10000 {
|
||||
// TODO: CHR-ROM mode
|
||||
}
|
||||
// TODO: Set name table mirroring
|
||||
if val & 0b00011 == 0b00000 {
|
||||
} else if val & 0b00011 == 0b00001 {
|
||||
} else if val & 0b00011 == 0b00010 {
|
||||
} else {
|
||||
}
|
||||
} else if (addr & 0x6000) >> 13 == 1 {
|
||||
if chr_banks.len() != 0 {
|
||||
todo!("Swap CHR bank 0")
|
||||
}
|
||||
} else if (addr & 0x6000) >> 13 == 2 {
|
||||
if chr_banks.len() != 0 {
|
||||
todo!("Swap CHR bank 1")
|
||||
}
|
||||
} else if (addr & 0x6000) >> 13 == 3 {
|
||||
*cur_prg_bank = (val & 0x0F) as usize;
|
||||
match mode {
|
||||
MMC1Mode::Full => {
|
||||
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
|
||||
bank0.swap_rom(prg_banks[*cur_prg_bank & !1].clone());
|
||||
let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap();
|
||||
bank1.swap_rom(prg_banks[(*cur_prg_bank & !1) + 1].clone());
|
||||
}
|
||||
MMC1Mode::FirstFixed => {
|
||||
let bank1 = self.mem.cpu.find(SegmentId::PrgBank1).unwrap();
|
||||
bank1.swap_rom(prg_banks[(val & 0x0F) as usize].clone());
|
||||
}
|
||||
MMC1Mode::LastFixed => {
|
||||
let bank0 = self.mem.cpu.find(SegmentId::PrgBank0).unwrap();
|
||||
bank0.swap_rom(prg_banks[(val & 0x0F) as usize].clone());
|
||||
}
|
||||
}
|
||||
// TODO: handle MSB, changes some stuff...
|
||||
} else {
|
||||
todo!("Handle reg write {} => {}", val, (addr & 0x6000) >> 13,)
|
||||
}
|
||||
*shift_reg = 0;
|
||||
*count = 0;
|
||||
} else {
|
||||
*shift_reg = (*shift_reg << 1) | (val & 0x01);
|
||||
*count += 1;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
match self.mem.cpu.write(addr, val) {
|
||||
Some((CPUMMRegisters::PPU, offset, val)) => {
|
||||
self.ppu.write_reg(&mut PpuMem::new(self.mem), offset, val)
|
||||
}
|
||||
Some((CPUMMRegisters::APU, offset, val)) => {
|
||||
if offset == 0x014 {
|
||||
*self.dma = DmaState::Idle((val as u16) << 8);
|
||||
} else if offset == 0x16 {
|
||||
self.controllers.write_joy_strobe(val);
|
||||
} else {
|
||||
self.apu.write_reg(offset, val);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PpuMem<'a>(&'a mut Mapped);
|
||||
|
||||
impl<'a> PpuMem<'a> {
|
||||
pub fn new(mem: &'a mut Mapped) -> Self {
|
||||
Self(mem)
|
||||
}
|
||||
pub fn read(&mut self, addr: u16) -> Value<PPUMMRegisters> {
|
||||
self.0.ppu.read(addr)
|
||||
}
|
||||
pub fn write(&mut self, addr: u16, val: u8, reg_fn: impl FnOnce(&PPUMMRegisters, u16, u8)) {
|
||||
match self.0.ppu.write(addr, val) {
|
||||
Some((r, o, v)) => reg_fn(&r, o, v),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum MMC1Mode {
|
||||
Full,
|
||||
FirstFixed,
|
||||
LastFixed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Remapper {
|
||||
None,
|
||||
MMC1 {
|
||||
shift_reg: u8,
|
||||
count: u8,
|
||||
prg_banks: Vec<Arc<[u8]>>,
|
||||
chr_banks: Vec<Arc<[u8]>>,
|
||||
mode: MMC1Mode,
|
||||
cur_prg_bank: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Remapper {
|
||||
fn power_cycle(
|
||||
&mut self,
|
||||
cpu: &mut MemoryMap<CPUMMRegisters>,
|
||||
_ppu: &mut MemoryMap<PPUMMRegisters>,
|
||||
) -> Self {
|
||||
match self {
|
||||
Remapper::None => Remapper::None,
|
||||
Remapper::MMC1 {
|
||||
prg_banks,
|
||||
chr_banks,
|
||||
..
|
||||
} => {
|
||||
let prg_banks = std::mem::take(prg_banks);
|
||||
let chr_banks = std::mem::take(chr_banks);
|
||||
cpu.find(SegmentId::PrgBank0)
|
||||
.unwrap()
|
||||
.swap_rom(prg_banks[0].clone());
|
||||
cpu.find(SegmentId::PrgBank1)
|
||||
.unwrap()
|
||||
.swap_rom(prg_banks.last().unwrap().clone());
|
||||
Remapper::MMC1 {
|
||||
shift_reg: 0,
|
||||
count: 0,
|
||||
prg_banks,
|
||||
chr_banks,
|
||||
mode: MMC1Mode::LastFixed,
|
||||
cur_prg_bank: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
543
src/ppu.rs
543
src/ppu.rs
@@ -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::{Mapper, MemoryMap, Segment},
|
||||
};
|
||||
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,30 +83,221 @@ impl<const W: usize, const H: usize> RenderBuffer<W, H> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum PPUMMRegisters {
|
||||
#[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,
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -103,22 +305,23 @@ 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)]
|
||||
pub struct Mask {
|
||||
pub grayscale: bool,
|
||||
pub background_on_left_edge: bool,
|
||||
@@ -453,18 +656,24 @@ const COLORS: &'static [Color; 0b100_0000] = &[
|
||||
}, // 3F
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Palette {
|
||||
colors: &'static [Color; 0x40],
|
||||
ram: [u8; 0x20],
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PPU {
|
||||
// registers: PPURegisters,
|
||||
pub frame_count: usize,
|
||||
@@ -474,13 +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,
|
||||
|
||||
pub(crate) memory: MemoryMap<PPUMMRegisters>,
|
||||
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,
|
||||
@@ -505,7 +716,10 @@ impl std::fmt::Debug for PPU {
|
||||
}
|
||||
|
||||
impl PPU {
|
||||
pub fn with_chr_rom(rom: &[u8], mapper: Mapper) -> Self {
|
||||
// pub fn with_chr_rom(rom: &[u8], mapper: Mapper) -> Self {
|
||||
// todo!()
|
||||
// }
|
||||
pub fn init() -> Self {
|
||||
Self {
|
||||
cycle: 25,
|
||||
dbg_int: false,
|
||||
@@ -529,54 +743,50 @@ impl PPU {
|
||||
],
|
||||
},
|
||||
vblank: false,
|
||||
sprite_zero_hit: false,
|
||||
frame_count: 0,
|
||||
nmi_enabled: false,
|
||||
// nmi_waiting: false,
|
||||
even: false,
|
||||
scanline: 0,
|
||||
pixel: 25,
|
||||
render_buffer: RenderBuffer::empty(),
|
||||
memory: mapper.ppu_map(rom),
|
||||
background: Background {
|
||||
v: 0,
|
||||
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,
|
||||
},
|
||||
oam: OAM::new(),
|
||||
vram_buffer: 0,
|
||||
}
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self {
|
||||
memory: std::mem::replace(&mut self.memory, MemoryMap::new(vec![])),
|
||||
..Self::with_chr_rom(&[], Mapper::from_flags(0))
|
||||
};
|
||||
*self = Self::init();
|
||||
}
|
||||
|
||||
pub fn rendering_enabled(&self) -> bool {
|
||||
self.mask.enable_background || self.mask.enable_sprites
|
||||
}
|
||||
|
||||
pub fn read_reg(&mut self, offset: u16) -> u8 {
|
||||
pub fn read_reg(&mut self, mem: &mut PpuMem<'_>, offset: u16) -> u8 {
|
||||
match offset {
|
||||
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
|
||||
@@ -586,14 +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 = self
|
||||
.memory
|
||||
.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
|
||||
}
|
||||
@@ -601,15 +814,21 @@ impl PPU {
|
||||
_ => panic!("No register at {:02X}", offset),
|
||||
}
|
||||
}
|
||||
pub fn write_reg(&mut self, offset: u16, val: u8) {
|
||||
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;
|
||||
@@ -625,12 +844,15 @@ impl PPU {
|
||||
}
|
||||
0x02 => {
|
||||
todo!("Unable to write to PPU status")
|
||||
// TODO: ppu status
|
||||
}
|
||||
0x03 => self.oam.addr = val,
|
||||
0x04 => {
|
||||
if self.oam.addr % 4 == 2 {
|
||||
val &= 0b11100011;
|
||||
}
|
||||
self.oam.mem[self.oam.addr as usize] = val;
|
||||
self.oam.addr = self.oam.addr.wrapping_add(1);
|
||||
self.oam.edit_ver += 1;
|
||||
}
|
||||
0x05 => {
|
||||
if self.background.w {
|
||||
@@ -648,31 +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);
|
||||
self.memory
|
||||
.write(self.background.v, val, |r, o, v| match r {
|
||||
PPUMMRegisters::Palette => {
|
||||
// println!("Writing {:02X} to {:02X}", v & 0x3F, o);
|
||||
mem.write(self.background.v, val, |r, mut o, v| match r {
|
||||
PPUMMRegisters::Palette => {
|
||||
// println!("Writing {:02X} to {:02X}", v & 0x3F, o);
|
||||
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),
|
||||
}
|
||||
@@ -685,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) -> bool {
|
||||
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;
|
||||
@@ -718,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 {
|
||||
@@ -761,7 +1012,7 @@ impl PPU {
|
||||
// let addr = 0x2000 + self.pixel / 8 + self.scanline / 8;
|
||||
let addr = 0x2000 | (self.background.v & 0x0FFF);
|
||||
// println!("Cur: {:04X}, comp: {:04X}", addr, addr_2);
|
||||
let val = self.memory.read(addr).reg_map(|_, _| 0);
|
||||
let val = mem.read(addr).reg_map(|_, _| 0);
|
||||
self.background.cur_nametable = val;
|
||||
} else if self.pixel % 8 == 4 {
|
||||
// Attr table fetch
|
||||
@@ -772,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 = self.memory.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
|
||||
@@ -784,7 +1037,7 @@ impl PPU {
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let val = self.memory.read(addr).reg_map(|_, _| 0);
|
||||
let val = mem.read(addr).reg_map(|_, _| 0);
|
||||
self.background.cur_low = val;
|
||||
} else if self.pixel % 8 == 0 && self.pixel > 0 {
|
||||
let addr = self.background.cur_nametable as u16 * 16
|
||||
@@ -796,12 +1049,18 @@ impl PPU {
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let val = self.memory.read(addr).reg_map(|_, _| todo!());
|
||||
let val = mem.read(addr).reg_map(|_, _| todo!());
|
||||
self.background.cur_high = val;
|
||||
self.background.cur_shift_low |= self.background.cur_low as u32;
|
||||
self.background.cur_shift_high |= self.background.cur_high as u32;
|
||||
self.background.cur_attr = self.background.next_attr;
|
||||
self.background.next_attr = self.background.next_attr_2;
|
||||
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;
|
||||
@@ -831,20 +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.nmi_waiting = self.nmi_enabled;
|
||||
self.frame_count += 1;
|
||||
self.background.state = BackgroundState::NameTableBytePre;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -858,12 +1113,6 @@ impl PPU {
|
||||
}
|
||||
pub fn nmi_waiting(&mut self) -> bool {
|
||||
self.vblank && self.nmi_enabled
|
||||
// if self.nmi_waiting {
|
||||
// self.nmi_waiting = false;
|
||||
// return true;
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
pub fn peek_irq(&self) -> bool {
|
||||
false
|
||||
@@ -871,31 +1120,36 @@ impl PPU {
|
||||
pub fn irq_waiting(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
pub fn peek_oam(&self, addr: u8) -> u8 {
|
||||
self.oam.mem[addr as usize]
|
||||
}
|
||||
pub fn oam_edit_ver(&self) -> usize {
|
||||
self.oam.edit_ver
|
||||
}
|
||||
|
||||
pub fn render_name_table<R: Renderer>(&self, frame: &mut Frame<R>) {
|
||||
pub fn render_name_table<R: Renderer>(&self, mem: &Mapped, frame: &mut Frame<R>) {
|
||||
for y in 0..60 {
|
||||
for x in 0..64 {
|
||||
let row = y % 30;
|
||||
let col = x % 32;
|
||||
let off = 0x2000 + 0x400 * (y / 30 * 2 + x / 32);
|
||||
let name = self.memory.peek((off + col + row * 32) as u16).unwrap() as u16 * 16
|
||||
let name = mem.peek_ppu((off + col + row * 32) as u16).unwrap() as u16 * 16
|
||||
+ if self.background.second_pattern {
|
||||
0x1000
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let attr = self
|
||||
.memory
|
||||
.peek((col / 4 + row / 4 * 8 + 0x3C0 + off) as u16)
|
||||
let attr = mem
|
||||
.peek_ppu((col / 4 + row / 4 * 8 + 0x3C0 + off) as u16)
|
||||
.unwrap();
|
||||
// attr << (((col & 1) << 1) | ((row & 1) << 0)) * 2
|
||||
// let h_off = ((self.pixel - 1) / 16) % 2;
|
||||
// let v_off = (self.scanline / 16) % 2;
|
||||
// let off = v_off * 4 + h_off * 2;
|
||||
let palette = (attr >> (((col & 1) << 1) | ((row & 1) << 2))) & 0x3;
|
||||
let palette = (attr >> (((col & 2) << 0) | ((row & 2) << 1))) & 0x3;
|
||||
for y_off in 0..8 {
|
||||
let low = self.memory.peek(name + y_off).unwrap();
|
||||
let high = self.memory.peek(name + y_off + 8).unwrap();
|
||||
let low = mem.peek_ppu(name + y_off).unwrap();
|
||||
let high = mem.peek_ppu(name + y_off + 8).unwrap();
|
||||
for bit in 0..8 {
|
||||
frame.fill_rectangle(
|
||||
Point::new(
|
||||
@@ -904,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,
|
||||
@@ -929,21 +1183,20 @@ impl PPU {
|
||||
}
|
||||
}
|
||||
// for
|
||||
// let pat = self.memory.peek();
|
||||
// let pat = mem.peek();
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn name_cursor_info(&self, cursor: Point) -> Option<String> {
|
||||
pub fn name_cursor_info(&self, mem: &Mapped, cursor: Point) -> Option<String> {
|
||||
let x = (cursor.x / 8.) as usize;
|
||||
let y = (cursor.y / 8.) as usize;
|
||||
if x < 64 && y < 60 {
|
||||
let row = y % 30;
|
||||
let col = x % 32;
|
||||
let off = 0x2000 + 0x400 * (y / 30 * 2 + x / 32);
|
||||
let name = self.memory.peek((off + col + row * 32) as u16).unwrap() as usize;
|
||||
let attr = self
|
||||
.memory
|
||||
.peek((col / 4 + row / 4 * 8 + 0x3C0 + off) as u16)
|
||||
let name = mem.peek_ppu((off + col + row * 32) as u16).unwrap() as usize;
|
||||
let attr = mem
|
||||
.peek_ppu((col / 4 + row / 4 * 8 + 0x3C0 + off) as u16)
|
||||
.unwrap();
|
||||
Some(format!(
|
||||
"Row, Column: {}, {}
|
||||
@@ -988,13 +1241,13 @@ Attribute data: ${:02X}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_pattern_tables<R: Renderer>(&self, frame: &mut Frame<R>) {
|
||||
pub fn render_pattern_tables<R: Renderer>(&self, mem: &Mapped, frame: &mut Frame<R>) {
|
||||
for y in 0..16 {
|
||||
for x in 0..16 {
|
||||
let name = (y * 16 + x) * 16;
|
||||
for y_off in 0..8 {
|
||||
let low = self.memory.peek(name + y_off).unwrap();
|
||||
let high = self.memory.peek(name + y_off + 8).unwrap();
|
||||
let low = mem.peek_ppu(name + y_off).unwrap();
|
||||
let high = mem.peek_ppu(name + y_off + 8).unwrap();
|
||||
for bit in 0..8 {
|
||||
frame.fill_rectangle(
|
||||
Point::new(
|
||||
@@ -1030,8 +1283,8 @@ Attribute data: ${:02X}
|
||||
for x in 0..16 {
|
||||
let name = (y * 16 + x) * 16;
|
||||
for y_off in 0..8 {
|
||||
let low = self.memory.peek(name + y_off + 0x1000).unwrap();
|
||||
let high = self.memory.peek(name + y_off + 8 + 0x1000).unwrap();
|
||||
let low = mem.peek_ppu(name + y_off + 0x1000).unwrap();
|
||||
let high = mem.peek_ppu(name + y_off + 8 + 0x1000).unwrap();
|
||||
for bit in 0..8 {
|
||||
frame.fill_rectangle(
|
||||
Point::new(
|
||||
@@ -1061,7 +1314,7 @@ Attribute data: ${:02X}
|
||||
}
|
||||
}
|
||||
// for
|
||||
// let pat = self.memory.peek();
|
||||
// let pat = mem.peek();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1105,10 +1358,6 @@ Attribute data: ${:02X}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mem(&self) -> &impl Memory {
|
||||
&self.memory
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1117,14 +1366,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ppu_registers() {
|
||||
let mut ppu = PPU::with_chr_rom(&[0u8; 8192], Mapper::from_flags(0));
|
||||
// let mut ppu = PPU::with_chr_rom(&[0u8; 8192], Mapper::from_header(0));
|
||||
let mut ppu = PPU::init();
|
||||
// let mut mem = MemoryMap::new(vec![Segment::ram("")]);
|
||||
let mut mem = Mapped::test_ram();
|
||||
let mut mem = PpuMem::new(&mut mem);
|
||||
assert_eq!(ppu.background.v, 0);
|
||||
assert_eq!(ppu.background.t, 0);
|
||||
assert_eq!(ppu.background.x, 0);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
ppu.write_reg(0, 0);
|
||||
ppu.write_reg(&mut mem, 0, 0);
|
||||
assert_eq!(ppu.background.v, 0);
|
||||
ppu.write_reg(0, 0b11);
|
||||
ppu.write_reg(&mut mem, 0, 0b11);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b0001100_00000000,
|
||||
"Actual: {:016b}",
|
||||
@@ -1132,7 +1385,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
|
||||
ppu.write_reg(5, 0x7D);
|
||||
ppu.write_reg(&mut mem, 5, 0x7D);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b0001100_00001111,
|
||||
"Actual: {:016b}",
|
||||
@@ -1140,7 +1393,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(ppu.background.x, 0b101);
|
||||
assert_eq!(ppu.background.w, true);
|
||||
ppu.write_reg(5, 0x5E);
|
||||
ppu.write_reg(&mut mem, 5, 0x5E);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b1101101_01101111,
|
||||
"Actual: {:016b}",
|
||||
@@ -1149,7 +1402,7 @@ mod tests {
|
||||
assert_eq!(ppu.background.x, 0b101);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
|
||||
ppu.write_reg(5, 0x7D);
|
||||
ppu.write_reg(&mut mem, 5, 0x7D);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b1101101_01101111,
|
||||
"Actual: {:016b}",
|
||||
@@ -1157,7 +1410,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(ppu.background.x, 0b101);
|
||||
assert_eq!(ppu.background.w, true);
|
||||
ppu.read_reg(2);
|
||||
ppu.read_reg(&mut mem, 2);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
}
|
||||
}
|
||||
|
||||
36
src/test_roms/alu_bit.s
Normal file
36
src/test_roms/alu_bit.s
Normal file
@@ -0,0 +1,36 @@
|
||||
; Verifies that reset doesn't alter any RAM.
|
||||
|
||||
.include "testing.s"
|
||||
|
||||
zp_res VAL
|
||||
RAM = $300
|
||||
|
||||
reset:
|
||||
sei
|
||||
cld
|
||||
ldx #$FF
|
||||
txs
|
||||
|
||||
lda #$F0
|
||||
sta VAL
|
||||
lda #$0F
|
||||
bit VAL
|
||||
php
|
||||
pla
|
||||
sta RAM+0
|
||||
|
||||
lda #$F0
|
||||
sta $400
|
||||
lda #$0F
|
||||
bit $400
|
||||
php
|
||||
pla
|
||||
sta RAM+1
|
||||
|
||||
stp
|
||||
|
||||
nmi:
|
||||
stp
|
||||
|
||||
irq:
|
||||
stp
|
||||
97
src/test_roms/apu_pulse_1.s
Normal file
97
src/test_roms/apu_pulse_1.s
Normal 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
|
||||
104
src/test_roms/apu_pulse_channel_1.s
Normal file
104
src/test_roms/apu_pulse_channel_1.s
Normal 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
|
||||
60
src/test_roms/apu_pulse_channel_1_evelope.s
Normal file
60
src/test_roms/apu_pulse_channel_1_evelope.s
Normal 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
|
||||
135
src/test_roms/apu_triangle.s
Normal file
135
src/test_roms/apu_triangle.s
Normal 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
|
||||
109
src/test_roms/common/audio_inp.s
Normal file
109
src/test_roms/common/audio_inp.s
Normal 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
|
||||
530
src/test_roms/common/bitmap_font.s
Normal file
530
src/test_roms/common/bitmap_font.s
Normal 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
|
||||
30
src/test_roms/common/joysticks.s
Normal file
30
src/test_roms/common/joysticks.s
Normal 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
|
||||
|
||||
37
src/test_roms/common/minimal_ppu.s
Normal file
37
src/test_roms/common/minimal_ppu.s
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
335
src/test_roms/input_test.s
Normal 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
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::{NES, hex_view::Memory};
|
||||
use crate::NES;
|
||||
|
||||
use super::rom_test;
|
||||
|
||||
@@ -13,7 +13,7 @@ impl HexDump<'_> {
|
||||
fn get(&self, off: usize) -> Option<u8> {
|
||||
match self {
|
||||
Self::NES(_, _, len) if off >= *len => None,
|
||||
Self::NES(nes, addr, _) => nes.cpu_mem().peek(addr + off as u16),
|
||||
Self::NES(nes, addr, _) => nes.mem().peek_cpu(addr + off as u16),
|
||||
Self::Lit(items) => items.get(off).copied(),
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ impl fmt::Display for HexDump<'_> {
|
||||
|
||||
fn mem_cmp(nes: &NES, addr: u16, vals: &[u8]) {
|
||||
for (i, v) in vals.iter().enumerate() {
|
||||
if nes.cpu_mem().peek(addr + i as u16) != Some(*v) {
|
||||
if nes.mem().peek_cpu(addr + i as u16) != Some(*v) {
|
||||
panic!(
|
||||
"memcmp assertion failed:\nNES:{}\nTest:{}",
|
||||
HexDump::NES(nes, addr, vals.len()),
|
||||
@@ -46,7 +46,7 @@ fn mem_cmp(nes: &NES, addr: u16, vals: &[u8]) {
|
||||
}
|
||||
|
||||
rom_test!(crc_check, "crc_check.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x8026 HLT :2 []");
|
||||
assert_eq!(nes.last_instruction(), "0x8026 HLT :2 []");
|
||||
assert_eq!(nes.cpu.a, 0x00);
|
||||
mem_cmp(&nes, 0x20, &[0xFF, 0xFF, 0xFF, 0xFF]);
|
||||
mem_cmp(&nes, 0x24, &[0x69, 0xCF, 0xF8, 0x88]);
|
||||
@@ -58,6 +58,11 @@ rom_test!(crc_check, "crc_check.nes", |nes| {
|
||||
});
|
||||
|
||||
rom_test!(implied_instructions, "implied_instrs.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x8026 HLT :2 []");
|
||||
assert_eq!(nes.last_instruction(), "0x8026 HLT :2 []");
|
||||
assert_eq!(nes.cpu.a, 0x00);
|
||||
});
|
||||
|
||||
rom_test!(bit, "alu_bit.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction(), "0x8021 HLT :2 []");
|
||||
mem_cmp(&nes, 0x300, &[0xF6, 0xF6]);
|
||||
});
|
||||
@@ -1,27 +1,25 @@
|
||||
use crate::hex_view::Memory;
|
||||
|
||||
use super::rom_test;
|
||||
|
||||
rom_test!(int_nmi_timing, "int_nmi_timing.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x801B HLT :2 []");
|
||||
assert_eq!(nes.last_instruction(), "0x801B HLT :2 []");
|
||||
assert_eq!(nes.clock_count, 260881);
|
||||
assert_eq!(nes.cycle, 86967);
|
||||
assert_eq!(nes.cpu_cycle(), 86967);
|
||||
assert_eq!(nes.ppu().pixel, 40);
|
||||
assert_eq!(nes.ppu().scanline, 241);
|
||||
assert_eq!(nes.ppu().cycle, 260905);
|
||||
assert_eq!(nes.cpu_mem().peek(0x1FF), Some(0x80));
|
||||
assert_eq!(nes.cpu_mem().peek(0x1FE), Some(0x17));
|
||||
assert_eq!(nes.cpu_mem().peek(0x1FD), Some(0xA4));
|
||||
assert_eq!(nes.mem().peek_cpu(0x1FF), Some(0x80));
|
||||
assert_eq!(nes.mem().peek_cpu(0x1FE), Some(0x17));
|
||||
assert_eq!(nes.mem().peek_cpu(0x1FD), Some(0xA4));
|
||||
});
|
||||
|
||||
rom_test!(int_nmi_exit_timing, "int_nmi_exit_timing.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x801F HLT :2 []");
|
||||
assert_eq!(nes.last_instruction(), "0x801F HLT :2 []");
|
||||
// assert_eq!(nes.clock_count, 260881);
|
||||
assert_eq!(nes.cycle, 86980);
|
||||
assert_eq!(nes.cpu_cycle(), 86980);
|
||||
assert_eq!(nes.ppu().pixel, 79);
|
||||
assert_eq!(nes.ppu().scanline, 241);
|
||||
assert_eq!(nes.ppu().cycle, 260905 - 40 + 79);
|
||||
assert_eq!(nes.cpu_mem().peek(0x1FF), Some(0x80));
|
||||
assert_eq!(nes.cpu_mem().peek(0x1FE), Some(0x1B));
|
||||
assert_eq!(nes.cpu_mem().peek(0x1FD), Some(0x26));
|
||||
assert_eq!(nes.mem().peek_cpu(0x1FF), Some(0x80));
|
||||
assert_eq!(nes.mem().peek_cpu(0x1FE), Some(0x1B));
|
||||
assert_eq!(nes.mem().peek_cpu(0x1FD), Some(0x26));
|
||||
});
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
mod cpu_reset_ram;
|
||||
mod instr_test_v3;
|
||||
mod instructions;
|
||||
mod ppu;
|
||||
mod interrupts;
|
||||
|
||||
use crate::hex_view::Memory;
|
||||
|
||||
macro_rules! rom_test {
|
||||
($name:ident, $rom:literal, |$nes:ident| $eval:expr) => {
|
||||
rom_test!($name, $rom, timeout = 10000000, |$nes| $eval);
|
||||
@@ -38,9 +36,11 @@ 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.cycle, 10);
|
||||
assert_eq!(nes.last_instruction(), "0x8001 HLT :2 []");
|
||||
assert_eq!(nes.cpu_cycle(), 10);
|
||||
assert_eq!(nes.cpu.pc, 0x8001);
|
||||
assert_eq!(nes.ppu.pixel, 34);
|
||||
|
||||
@@ -48,8 +48,8 @@ rom_test!(basic_cpu, "basic-cpu.nes", |nes| {
|
||||
|
||||
nes.repl_nop();
|
||||
nes.run_with_timeout(200);
|
||||
assert_eq!(nes.last_instruction, "0x8002 HLT :2 []");
|
||||
assert_eq!(nes.cycle, 12);
|
||||
assert_eq!(nes.last_instruction(), "0x8002 HLT :2 []");
|
||||
assert_eq!(nes.cpu_cycle(), 12);
|
||||
assert_eq!(nes.cpu.pc, 0x8002);
|
||||
assert_eq!(nes.ppu.pixel, 40);
|
||||
|
||||
@@ -57,8 +57,8 @@ rom_test!(basic_cpu, "basic-cpu.nes", |nes| {
|
||||
});
|
||||
|
||||
rom_test!(basic_cpu_with_nop, "basic-cpu-nop.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x8002 HLT :2 []");
|
||||
assert_eq!(nes.cycle, 12);
|
||||
assert_eq!(nes.last_instruction(), "0x8002 HLT :2 []");
|
||||
assert_eq!(nes.cpu_cycle(), 12);
|
||||
assert_eq!(nes.cpu.pc, 0x8002);
|
||||
assert_eq!(nes.ppu.pixel, 40);
|
||||
|
||||
@@ -66,22 +66,22 @@ rom_test!(basic_cpu_with_nop, "basic-cpu-nop.nes", |nes| {
|
||||
});
|
||||
|
||||
rom_test!(read_write, "read_write.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x800C HLT :2 []");
|
||||
assert_eq!(nes.cycle, 25);
|
||||
assert_eq!(nes.last_instruction(), "0x800C HLT :2 []");
|
||||
assert_eq!(nes.cpu_cycle(), 25);
|
||||
assert_eq!(nes.cpu.pc, 0x800C);
|
||||
assert_eq!(nes.cpu.sp, 0xFD);
|
||||
|
||||
assert_eq!(nes.cpu.a, 0xAA);
|
||||
assert_eq!(nes.cpu.x, 0xAA);
|
||||
assert_eq!(nes.cpu.y, 0xAA);
|
||||
assert_eq!(nes.cpu_mem().peek(0x0000).unwrap(), 0xAA);
|
||||
assert_eq!(nes.cpu_mem().peek(0x0001).unwrap(), 0xAA);
|
||||
assert_eq!(nes.cpu_mem().peek(0x0002).unwrap(), 0xAA);
|
||||
assert_eq!(nes.mem().peek_cpu(0x0000).unwrap(), 0xAA);
|
||||
assert_eq!(nes.mem().peek_cpu(0x0001).unwrap(), 0xAA);
|
||||
assert_eq!(nes.mem().peek_cpu(0x0002).unwrap(), 0xAA);
|
||||
});
|
||||
|
||||
rom_test!(basic_init_0, "basic_init_0.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x8017 HLT :2 []");
|
||||
assert_eq!(nes.cycle, 40);
|
||||
assert_eq!(nes.last_instruction(), "0x8017 HLT :2 []");
|
||||
assert_eq!(nes.cpu_cycle(), 40);
|
||||
assert_eq!(nes.cpu.pc, 0x8017);
|
||||
assert_eq!(nes.cpu.sp, 0xFF);
|
||||
|
||||
@@ -91,8 +91,8 @@ rom_test!(basic_init_0, "basic_init_0.nes", |nes| {
|
||||
});
|
||||
|
||||
rom_test!(basic_init_1, "basic_init_1.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x801C HLT :2 []");
|
||||
assert_eq!(nes.cycle, 27402);
|
||||
assert_eq!(nes.last_instruction(), "0x801C HLT :2 []");
|
||||
assert_eq!(nes.cpu_cycle(), 27402);
|
||||
assert_eq!(nes.cpu.pc, 0x801C);
|
||||
assert_eq!(nes.ppu.pixel, 29);
|
||||
|
||||
@@ -103,8 +103,8 @@ rom_test!(basic_init_1, "basic_init_1.nes", |nes| {
|
||||
});
|
||||
|
||||
rom_test!(basic_init_2, "basic_init_2.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x8021 HLT :2 []");
|
||||
assert_eq!(nes.cycle, 57179);
|
||||
assert_eq!(nes.last_instruction(), "0x8021 HLT :2 []");
|
||||
assert_eq!(nes.cpu_cycle(), 57179);
|
||||
assert_eq!(nes.cpu.pc, 0x8021);
|
||||
assert_eq!(nes.ppu.pixel, 18);
|
||||
|
||||
@@ -115,8 +115,8 @@ rom_test!(basic_init_2, "basic_init_2.nes", |nes| {
|
||||
});
|
||||
|
||||
rom_test!(basic_init_3, "basic_init_3.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x8026 HLT :2 []");
|
||||
assert_eq!(nes.cycle, 86963);
|
||||
assert_eq!(nes.last_instruction(), "0x8026 HLT :2 []");
|
||||
assert_eq!(nes.cpu_cycle(), 86963);
|
||||
assert_eq!(nes.cpu.pc, 0x8026);
|
||||
assert_eq!(nes.ppu.pixel, 28);
|
||||
|
||||
@@ -127,8 +127,8 @@ rom_test!(basic_init_3, "basic_init_3.nes", |nes| {
|
||||
});
|
||||
|
||||
rom_test!(even_odd, "even_odd.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x8023 HLT :2 []");
|
||||
assert_eq!(nes.cycle, 57181);
|
||||
assert_eq!(nes.last_instruction(), "0x8023 HLT :2 []");
|
||||
assert_eq!(nes.cpu_cycle(), 57181);
|
||||
assert_eq!(nes.cpu.pc, 0x8023);
|
||||
assert_eq!(nes.ppu.pixel, 24);
|
||||
|
||||
@@ -138,9 +138,70 @@ rom_test!(even_odd, "even_odd.nes", |nes| {
|
||||
assert_eq!(nes.cpu.y, 0x00);
|
||||
});
|
||||
|
||||
fn run_frame(nes: &mut NES) {
|
||||
nes.run_one_clock_cycle(&Break::default());
|
||||
while nes.ppu().scanline != 241 || nes.ppu().pixel != 0 {
|
||||
nes.run_one_clock_cycle(&Break::default());
|
||||
}
|
||||
}
|
||||
|
||||
rom_test!(input_test, "input_test.nes", timeout = 86964*3, |nes| {
|
||||
const A: u16 = 0x23DC; // 0 || 0b01000000
|
||||
const B: u16 = 0x23DD; // 0 || 0b01000000
|
||||
const SELECT: u16 = 0x23DA; // 0 || 0b01000000
|
||||
const START: u16 = 0x23DB; // 0 || 0b01000000
|
||||
const DOWN: u16 = 0x23E1; // 0 || 1
|
||||
const LEFT: u16 = 0x23D8; // 0 || 0b01000000
|
||||
const UP_RIGHT: u16 = 0x23D9; // 0 || 0b01000000 | 1
|
||||
run_frame(&mut nes);
|
||||
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
|
||||
nes.controller_1().set_a(true);
|
||||
run_frame(&mut nes);
|
||||
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40);
|
||||
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
|
||||
nes.controller_1().set_b(true);
|
||||
run_frame(&mut nes);
|
||||
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40);
|
||||
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0x40);
|
||||
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
|
||||
nes.controller_1().0 = 0xFF;
|
||||
run_frame(&mut nes);
|
||||
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0x40);
|
||||
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0x40);
|
||||
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0x40);
|
||||
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0x40);
|
||||
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0x01);
|
||||
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0x40);
|
||||
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0x41);
|
||||
nes.controller_1().0 = 0;
|
||||
run_frame(&mut nes);
|
||||
assert_eq!(nes.mapped.peek_ppu(A).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(B).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(SELECT).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(START).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(DOWN).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(LEFT).unwrap(), 0);
|
||||
assert_eq!(nes.mapped.peek_ppu(UP_RIGHT).unwrap(), 0);
|
||||
});
|
||||
|
||||
// rom_test!(even_odd, "even_odd.nes", |nes| {
|
||||
// assert_eq!(nes.last_instruction, "0x8023 HLT :2 []");
|
||||
// assert_eq!(nes.cycle, 57182);
|
||||
// assert_eq!(nes.last_instruction(), "0x8023 HLT :2 []");
|
||||
// assert_eq!(nes.cpu_cycle(), 57182);
|
||||
// assert_eq!(nes.cpu.pc, 0x8024);
|
||||
// assert_eq!(nes.ppu.pixel, 25);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{hex_view::Memory, Color, RenderBuffer};
|
||||
use crate::{Color, RenderBuffer};
|
||||
|
||||
use super::rom_test;
|
||||
|
||||
@@ -19,7 +19,7 @@ const COLOR_0B: Color = Color {
|
||||
};
|
||||
|
||||
rom_test!(ppu_fill, "ppu_fill.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x802B HLT :2 []");
|
||||
assert_eq!(nes.last_instruction(), "0x802B HLT :2 []");
|
||||
assert_eq!(nes.cpu.a, 0x01);
|
||||
|
||||
let mut buffer = RenderBuffer::empty();
|
||||
@@ -32,7 +32,7 @@ rom_test!(ppu_fill, "ppu_fill.nes", |nes| {
|
||||
});
|
||||
|
||||
rom_test!(ppu_fill_red, "ppu_fill_red.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x803A HLT :2 []");
|
||||
assert_eq!(nes.last_instruction(), "0x803A HLT :2 []");
|
||||
assert_eq!(nes.cpu.a, 0x01);
|
||||
|
||||
let mut buffer = RenderBuffer::empty();
|
||||
@@ -45,7 +45,7 @@ rom_test!(ppu_fill_red, "ppu_fill_red.nes", |nes| {
|
||||
});
|
||||
|
||||
rom_test!(ppu_fill_palette_1, "ppu_fill_palette_1.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x8064 HLT :2 []");
|
||||
assert_eq!(nes.last_instruction(), "0x8064 HLT :2 []");
|
||||
assert_eq!(nes.cpu.a, 0x01);
|
||||
|
||||
let mut buffer = RenderBuffer::empty();
|
||||
@@ -62,7 +62,7 @@ rom_test!(ppu_fill_palette_1, "ppu_fill_palette_1.nes", |nes| {
|
||||
});
|
||||
|
||||
rom_test!(ppu_fill_name_table, "ppu_fill_name_table.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x80B3 HLT :2 []");
|
||||
assert_eq!(nes.last_instruction(), "0x80B3 HLT :2 []");
|
||||
assert_eq!(nes.cpu.a, 0x01);
|
||||
|
||||
let mut buffer = RenderBuffer::empty();
|
||||
@@ -81,10 +81,21 @@ 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.last_instruction(), "0x8040 HLT :2 []");
|
||||
|
||||
assert_eq!(nes.ppu().mem().peek(0x2000), Some(1));
|
||||
assert_eq!(nes.ppu().mem().peek(0x2020), Some(1));
|
||||
assert_eq!(nes.ppu().mem().peek(0x2400), Some(2));
|
||||
assert_eq!(nes.ppu().mem().peek(0x2401), Some(2));
|
||||
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 });
|
||||
});
|
||||
|
||||
306
src/test_roms/ppu_fine_x_scrolling.s
Normal file
306
src/test_roms/ppu_fine_x_scrolling.s
Normal 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
|
||||
70
src/test_roms/ppu_palette_shared.s
Normal file
70
src/test_roms/ppu_palette_shared.s
Normal 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
|
||||
96
src/test_roms/render-updating.s
Normal file
96
src/test_roms/render-updating.s
Normal 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
306
src/test_roms/scrolling.s
Normal 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
|
||||
334
src/test_roms/scrolling_colors.s
Normal file
334
src/test_roms/scrolling_colors.s
Normal 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
321
src/test_roms/sprites.s
Normal 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:
|
||||
Reference in New Issue
Block a user