diff --git a/Cargo.lock b/Cargo.lock index 14022ff..5906449 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,28 @@ dependencies = [ "equator", ] +[[package]] +name = "alsa" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3" +dependencies = [ + "alsa-sys", + "bitflags 2.10.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -771,6 +793,20 @@ dependencies = [ "libm", ] +[[package]] +name = "coreaudio-rs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +dependencies = [ + "bitflags 1.3.2", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + [[package]] name = "cosmic-text" version = "0.15.0" @@ -794,6 +830,36 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpal" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb" +dependencies = [ + "alsa", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2 0.6.3", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.62.2", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -858,6 +924,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "dispatch" version = "0.2.0" @@ -2006,6 +2078,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2197,9 +2278,11 @@ version = "0.1.0" dependencies = [ "bitfield", "bytes", + "cpal", "iced", "iced_core", "rfd", + "ringbuf", "thiserror 2.0.17", "tokio", "tracing", @@ -2403,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" @@ -2438,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" @@ -2468,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", ] @@ -3216,6 +3349,17 @@ version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +[[package]] +name = "ringbuf" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" +dependencies = [ + "crossbeam-utils", + "portable-atomic", + "portable-atomic-util", +] + [[package]] name = "roxmltree" version = "0.20.0" diff --git a/Cargo.toml b/Cargo.toml index ac3727f..5dbfc4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,5 @@ 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" diff --git a/src/apu.rs b/src/apu.rs index 9746b44..f7c1147 100644 --- a/src/apu.rs +++ b/src/apu.rs @@ -1,5 +1,13 @@ +use std::iter::repeat_n; + +use iced::{ + Element, Font, + widget::{column, text}, +}; use tracing::debug; +pub enum None {} + bitfield::bitfield! { #[derive(Clone, Copy, PartialEq, Eq)] pub struct DutyVol(u8); @@ -38,6 +46,8 @@ struct PulseChannel { cur_time: u16, cur_length: u8, + cur: u8, + sample: u8, } impl PulseChannel { @@ -50,6 +60,8 @@ impl PulseChannel { length_timer_high: LengthTimerHigh(0), cur_time: 0, cur_length: 0, + cur: 0, + sample: 0, } } fn use_envelope(&self) -> bool { @@ -61,58 +73,229 @@ impl PulseChannel { 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(); + self.cur_length = self.length_timer_high.length(); + self.cur = 0; } pub fn clock(&mut self) { if self.cur_time == 0 { self.cur_time = self.timer(); + 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.cur_time -= 1; } // TODO } + pub fn cur_sample(&self) -> u8 { + self.sample + } pub fn q_frame_clock(&mut self) { if !self.duty_vol.length_counter_halt() && self.cur_length > 0 { self.cur_length -= 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(); + // if !self.duty_vol.length_counter_halt() && self.cur_length > 0 { + // self.cur_length -= 1; + // } + } + + pub fn view(&self) -> Element<'_, T> { + text!( + " + Square Channel + " + ) + .font(Font::MONOSPACE) + .into() } } bitfield::bitfield! { - pub struct CounterLoad(u8); + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct LengthCounter(u8); impl Debug; halt, set_halt: 7; value, set_value: 6, 0; } +bitfield::bitfield! { + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct LengthLoad(u8); + impl Debug; + load, set_load: 7, 3; + timer_high, set_timer_high: 2, 0; +} + #[derive(Debug, Clone)] struct TriangleChannel { enabled: bool, + length: LengthCounter, + timer_low: u8, + length_load: LengthLoad, + reload: bool, + + length_counter: u16, + cur: u8, + cur_time: u16, + sample: u8, } + +impl TriangleChannel { + pub fn new() -> Self { + Self { + length: LengthCounter(0), + timer_low: 0, + length_load: LengthLoad(0), + reload: false, + enabled: false, + sample: 0, + cur_time: 0, + cur: 0, + length_counter: 0, + } + } + + fn timer(&self) -> u16 { + self.timer_low as u16 | ((self.length_load.timer_high() as u16) << 8) + } + + pub fn cur_sample(&self) -> u8 { + self.sample + } + + pub fn clock(&mut self) { + if self.length_counter > 0 && self.timer() > 0 { + if self.cur_time == 0 { + self.cur_time = self.timer(); + 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.cur_time -= 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(&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.timer(), + 0, + self.enabled, + self.cur_time, + self.cur, + 0, + self.length_counter, + self.reload, + self.cur_sample(), + ) + .font(Font::MONOSPACE) + .into() + } +} + #[derive(Debug, Clone)] struct NoiseChannel { enabled: bool, + sample: u8, } + +impl NoiseChannel { + pub fn new() -> Self { + Self { + enabled: false, + sample: 0, + } + } + + 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 view(&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 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(&self) -> Element<'_, T> { + text!("").font(Font::MONOSPACE).into() + } } #[derive(Debug, Clone)] @@ -131,6 +314,8 @@ pub struct APU { noise: NoiseChannel, dmc: DeltaChannel, frame_counter: FrameCounter, + + samples: Vec, } impl std::fmt::Debug for APU { @@ -149,15 +334,17 @@ 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], } } @@ -201,8 +388,15 @@ impl APU { self.pulse_2.reset(); } 0x09 => (), // Unused, technically noise channel? - 0x08 | 0x0A | 0x0B => { - // TODO: Triangle channel + 0x08 => { + self.triangle.length.0 = val; + } + 0x0A => { + self.triangle.timer_low = val; + } + 0x0B => { + self.triangle.length_load.0 = val; + self.triangle.reload = true; } 0x0D => (), // Unused, technically noise channel? 0x0C | 0x0E | 0x0F => { @@ -242,14 +436,41 @@ impl APU { fn q_frame_clock(&mut self) { self.pulse_1.q_frame_clock(); - self.pulse_2.q_frame_clock(); // TODO: clock all + 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.q_frame_clock(); self.pulse_1.h_frame_clock(); - self.pulse_2.q_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) { + 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 + }; + }; + } + 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 { @@ -275,10 +496,27 @@ impl APU { self.pulse_1.clock(); self.pulse_2.clock(); + self.noise.clock(); + self.dmc.clock(); + } + if ppu_cycle % 3 == 1 { + self.triangle.clock(); + } + if ppu_cycle % (6 * 4) == 1 { + self.gen_sample(); } false } + pub fn get_frame_samples(&self) -> &[u8] { + // println!("'Frame' of samples: {}", self.samples.len()); + &self.samples + } + + pub fn reset_frame_samples(&mut self) { + self.samples.clear(); + } + pub fn peek_nmi(&self) -> bool { false } @@ -294,4 +532,15 @@ impl APU { 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() + } } diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..2046488 --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,94 @@ +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>, 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, +} + +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); + } + + 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); + } + } +} diff --git a/src/debugger.rs b/src/debugger.rs index d1fb86a..96b9c57 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -154,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(""), diff --git a/src/lib.rs b/src/lib.rs index 1294272..3687480 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ mod ppu; pub mod resize_watcher; #[cfg(test)] mod test_roms; +pub mod audio; pub use ppu::{Color, PPU, RenderBuffer}; use tokio::io::AsyncReadExt as _; @@ -349,6 +350,10 @@ impl NES { &self.apu } + pub fn apu_mut(&mut self) -> &mut APU { + &mut self.apu + } + pub fn debug_log(&self) -> &DebugLog { &self.cpu.debug_log } diff --git a/src/main.rs b/src/main.rs index 9ae34e5..75d9577 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,13 +5,13 @@ use std::{ }; use iced::{ - Color, Element, + Element, Length::{Fill, Shrink}, Point, Rectangle, Renderer, Size, Subscription, Task, Theme, keyboard::{self, Key, Modifiers, key::Named}, mouse, time, widget::{ - self, Button, Canvas, button, + self, Canvas, button, canvas::{Frame, Program}, column, container, image, row, }, @@ -19,13 +19,13 @@ use iced::{ }; use nes_emu::{ Break, NES, + audio::Audio, debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image}, header_menu::header_menu, hex_view::{HexEvent, HexView}, resize_watcher::resize_watcher, }; use tokio::{io::AsyncWriteExt, runtime::Runtime}; -use tracing::instrument::WithSubscriber; use tracing_subscriber::EnvFilter; // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "even_odd.nes"); @@ -37,26 +37,14 @@ use tracing_subscriber::EnvFilter; // 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"), "/", "sprites.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_triangle.nes"); // const ROM_FILE: &str = "./Super Mario Bros. (World).nes"; // const ROM_FILE: &str = "./cpu_timing_test.nes"; // const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.nes"; -/// Disable button in debug mode - used to disable play/pause buttons since the performance isn't fast enough -fn debug_disable<'a, Message, Theme, Renderer>( - btn: Button<'a, Message, Theme, Renderer>, -) -> Button<'a, Message, Theme, Renderer> -where - Theme: button::Catalog + 'a, - Renderer: iced::advanced::Renderer, -{ - if cfg!(debug_assertions) { - btn.on_press_maybe(None) - } else { - btn - } -} - extern crate nes_emu; fn main() -> Result<(), iced::Error> { @@ -93,6 +81,7 @@ enum WindowType { TileViewer, Palette, Debugger, + Apu, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -118,6 +107,7 @@ impl fmt::Display for HeaderButton { Self::Open(WindowType::Debugger) => write!(f, "Open Debugger"), Self::Open(WindowType::Palette) => write!(f, "Open Palette"), Self::Open(WindowType::Main) => write!(f, "Create new Main window"), + Self::Open(WindowType::Apu) => write!(f, "Open APU info"), Self::Reset => write!(f, "Reset"), Self::PowerCycle => write!(f, "Power Cycle"), Self::OpenRom => write!(f, "Open ROM file"), @@ -132,6 +122,7 @@ struct Emulator { debugger: DebuggerState, main_win_size: Size, prev: [Instant; 2], + audio: Audio, } #[derive(Debug, Clone)] @@ -158,38 +149,32 @@ impl Emulator { fn new() -> (Self, Task) { 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() - }); + // let (win_2, task_2) = iced::window::open(Settings { + // min_size: None, + // ..Settings::default() + // }); ( Self { nes, windows: HashMap::from_iter([ (win, WindowType::Main), - (win_2, WindowType::Debugger) + // (win_2, WindowType::Debugger), ]), debugger: DebuggerState::new(), main_win_size: Size::new(0., 0.), running: false, prev: [Instant::now(); 2], + audio: Audio::init(), }, - Task::batch([task, task_2]).discard() - // task.discard(), + Task::batch([ + task, + // task_2 + ]) + .discard(), // task.discard(), ) } fn title(&self, win: Id) -> String { @@ -200,6 +185,7 @@ impl Emulator { 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!(), } } @@ -339,6 +325,17 @@ impl Emulator { 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 { @@ -366,14 +363,27 @@ impl Emulator { } _ => (), }, - Message::Periodic(i) => { + Message::Periodic(_i) => { if self.running { - // TODO: this should skip updating to avoid multiple frame skips - self.prev[1] = self.prev[0]; - self.prev[0] = i; - - self.nes.run_one_clock_cycle(&Break::default()); - while !self.nes.run_one_clock_cycle(&Break::default()).ppu_frame {} + // 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, @@ -387,7 +397,7 @@ impl Emulator { window::close_events().map(Message::WindowClosed), window::open_events().map(Message::WindowOpened), keyboard::listen().map(Message::Key), - time::every(Duration::from_millis(1000 / 60)).map(Message::Periodic), + time::every(Duration::from_secs_f64(1./60.)).map(Message::Periodic), ]) } @@ -436,20 +446,19 @@ impl Emulator { } Some(WindowType::Debugger) => column![ row![ - debug_disable( - button(image("./images/ic_fluent_play_24_filled.png")) - .on_press(Message::SetRunning(true)) - ), - debug_disable( - button(image("./images/ic_fluent_pause_24_filled.png")) - .on_press(Message::SetRunning(false)) - ), + 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!(), } @@ -476,6 +485,7 @@ impl Emulator { HeaderButton::Open(WindowType::TileMap), HeaderButton::Open(WindowType::TileViewer), HeaderButton::Open(WindowType::Palette), + HeaderButton::Open(WindowType::Apu), ], Message::Header ) @@ -488,21 +498,6 @@ impl Emulator { impl Program for Emulator { type State = (); - fn update( - &self, - _state: &mut Self::State, - _event: &iced::Event, - _bounds: Rectangle, - _cursor: iced::advanced::mouse::Cursor, - ) -> Option> { - // ~ 60 fps, I think? - // Some(widget::Action::request_redraw_at( - // Instant::now() + Duration::from_millis(10), - // )) - // Some(widget::Action::request_redraw()) - None - } - fn draw( &self, _state: &Self::State, @@ -511,17 +506,8 @@ impl Program for Emulator { bounds: iced::Rectangle, _cursor: mouse::Cursor, ) -> Vec> { - // let start = Instant::now(); - // 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.); - // TODO: use image for better? performance frame.draw_image( Rectangle::new(Point::new(0., 0.), Size::new(256., 240.)), widget::canvas::Image::new(widget::image::Handle::from_rgba( @@ -532,17 +518,6 @@ impl Program for Emulator { .filter_method(image::FilterMethod::Nearest) .snap(true), ); - // 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), - // ); - // } - // } - // println!("Rendered frame in {}ms", start.elapsed().as_millis()); vec![frame.into_geometry()] } } diff --git a/src/ppu.rs b/src/ppu.rs index 95ea349..ab2d239 100644 --- a/src/ppu.rs +++ b/src/ppu.rs @@ -128,7 +128,8 @@ pub struct OAM { oam_read_buffer: u8, edit_ver: usize, sprite_output_units: Vec, - overflow: bool, + large_sprites: bool, + pub overflow: bool, sprite_offset_0x1000: bool, } @@ -145,6 +146,7 @@ impl OAM { oam_read_buffer: 0, overflow: false, sprite_offset_0x1000: false, + large_sprites: false, } } @@ -272,15 +274,15 @@ impl OAM { // println!("Shifting: 0b{:08b} by {}", s.low, bit_pos); let lo = (s.low >> bit_pos) & 1; let idx = (hi << 1) | lo; - Some(( - if idx != 0 { - idx + s.attrs.palette() * 4 + 0x10 - } else { - 0 - }, - !s.attrs.priority(), - i == 0, - )) + if idx != 0 { + Some(( + idx + s.attrs.palette() * 4 + 0x10, + !s.attrs.priority(), + i == 0, + )) + } else { + None + } } else { None } @@ -292,8 +294,10 @@ impl OAM { #[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, @@ -301,18 +305,20 @@ pub struct Background { /// When false, writes to x pub w: bool, + copy_v: u8, + /// When true, v is incremented by 32 after each read pub vram_column: bool, pub second_pattern: bool, 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)] @@ -681,11 +687,11 @@ pub struct PPU { pub mask: Mask, pub vblank: bool, - sprite_zero_hit: bool, + pub sprite_zero_hit: bool, pub palette: Palette, pub background: Background, - oam: OAM, + pub oam: OAM, pub render_buffer: RenderBuffer<256, 240>, pub dbg_int: bool, pub cycle: usize, @@ -740,8 +746,6 @@ impl PPU { sprite_zero_hit: false, frame_count: 0, nmi_enabled: false, - // nmi_waiting: false, - // TODO: Is even in the right initial state? even: false, scanline: 0, pixel: 25, @@ -751,16 +755,17 @@ impl PPU { t: 0, x: 0, w: false, + copy_v: 0, vram_column: false, second_pattern: false, 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::new(), vram_buffer: 0, @@ -791,9 +796,6 @@ impl PPU { 5 => panic!("ppuscroll is write-only"), 6 => panic!("ppuaddr is write-only"), 7 => { - // TODO: read buffer only applies to ppu data, not palette ram... - // println!("Updating v for ppudata read"); - // self.vram_buffer = let val = match mem.read(self.background.v) { Value::Value(v) => { let val = self.vram_buffer; @@ -805,10 +807,6 @@ impl PPU { offset, } => self.palette.ram[offset as usize], }; - // .reg_map(|a, off| match a { - // PPUMMRegisters::Palette => self.palette.ram[off as usize], - // }); - // if self.background self.increment_v(); val } @@ -819,13 +817,16 @@ impl PPU { pub fn write_reg(&mut self, mem: &mut PpuMem, offset: u16, mut val: u8) { match offset { 0x00 => { - self.nmi_enabled = val & 0b1000_0000 != 0; self.background.t = - (self.background.t & !0b0001_1000_0000_0000) | (((val & 0b11) as u16) << 10); + (self.background.t & !0b0000_1100_0000_0000) | (((val & 0b11) as u16) << 10); self.background.vram_column = val & 0b0000_0100 != 0; - self.background.second_pattern = val & 0b0001_0000 != 0; self.oam.sprite_offset_0x1000 = val & 0b0000_1000 != 0; - // TODO: other control fields + self.background.second_pattern = val & 0b0001_0000 != 0; + 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; @@ -841,7 +842,6 @@ impl PPU { } 0x02 => { todo!("Unable to write to PPU status") - // TODO: ppu status } 0x03 => self.oam.addr = val, 0x04 => { @@ -868,19 +868,16 @@ impl PPU { } } 0x06 => { - // TODO: this actually sets T, which is copied to v later (~ a pixel later?) 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); @@ -897,7 +894,6 @@ impl PPU { } }); self.increment_v(); - // self.background.v += 1; // TODO: implement inc behavior } _ => panic!("No register at {:02X}", offset), } @@ -910,18 +906,17 @@ impl PPU { } else { self.background.v += 1; } - // self.background.v = self - // .background - // .v - // .wrapping_add(if self.background.vram_column { 32 } else { 1 }); } - // pub fn write_oamdma(&mut self, val: u8) { - // // TODO: OAM high addr - // } pub fn run_one_clock_cycle(&mut self, mem: &mut PpuMem<'_>) -> bool { self.cycle += 1; self.pixel += 1; + if self.background.copy_v > 0 { + self.background.copy_v -= 1; + if self.background.copy_v == 0 { + self.background.v = self.background.t; + } + } if self.scanline == 261 && (self.pixel == 341 || (self.pixel == 340 && self.even && self.rendering_enabled())) { @@ -944,16 +939,13 @@ 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 < 9 { + 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); } @@ -963,17 +955,16 @@ impl PPU { // self.dbg_int = true; // const POS: u32 = 1 << 15; let bit_pos = 15 - self.background.x; - // let pos: u32 = 1 << (15 + self.background.x*0); // TODO: handle this correctly // 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); let color = val as u8 + if val != 0 { palette * 4 } else { 0 }; if self.scanline < 240 && self.pixel < 257 { @@ -995,12 +986,14 @@ impl PPU { self.render_buffer.write( self.scanline, self.pixel - 1, - self.palette.color(color), // 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 { @@ -1021,8 +1014,8 @@ impl PPU { | ((self.background.v >> 2) & 0x07); // println!("Cur: {:04X}, comp: {:04X}", addr, addr_2); // assert_eq!(addr, addr_2); - let val = mem.read(addr).reg_map(|_, _| 0); // TODO: handle reg reads - self.background.next_attr_2 = val; + let val = mem.read(addr).reg_map(|_, offset| self.palette.ram(offset as u8)); + self.background.next_attr = val; } else if self.pixel % 8 == 6 { // BG pattern low let addr = self.background.cur_nametable as u16 * 16 @@ -1049,8 +1042,14 @@ impl PPU { self.background.cur_high = val; self.background.cur_shift_low |= self.background.cur_low as u32; self.background.cur_shift_high |= self.background.cur_high as u32; - self.background.cur_attr = self.background.next_attr; - self.background.next_attr = self.background.next_attr_2; + let h_off = (self.background.v >> 1) % 2; + let v_off = (self.background.v >> 6) % 2; + let off = h_off * 2 + v_off * 4; + let palette = (self.background.next_attr >> off) & 0b11; + self.background.cur_attr_shift_low |= + if palette & 0b01 == 0 { 0 } else { 0xFF }; + self.background.cur_attr_shift_high |= + if palette & 0b10 == 0 { 0 } else { 0xFF }; // Inc horizontal if self.background.v & 0x1F == 31 { self.background.v = (self.background.v & !0x1F) ^ 0x400; @@ -1080,8 +1079,6 @@ impl PPU { } } } - } else { - // TODO: Sprite fetches } } } diff --git a/src/test_roms/apu_pulse_channel_1.s b/src/test_roms/apu_pulse_channel_1.s new file mode 100644 index 0000000..d4786a2 --- /dev/null +++ b/src/test_roms/apu_pulse_channel_1.s @@ -0,0 +1,139 @@ +.include "testing.s" + +reset: + sei + cld + ldx #$FF + txs + + ; Init PPU + bit PPUSTATUS +vwait1: + bit PPUSTATUS + bpl vwait1 +vwait2: + bit PPUSTATUS + bpl vwait2 + + 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 + + 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 diff --git a/src/test_roms/apu_triangle.s b/src/test_roms/apu_triangle.s new file mode 100644 index 0000000..99a67f2 --- /dev/null +++ b/src/test_roms/apu_triangle.s @@ -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 diff --git a/src/test_roms/common/neshw.inc b/src/test_roms/common/neshw.inc index 814d772..954993f 100644 --- a/src/test_roms/common/neshw.inc +++ b/src/test_roms/common/neshw.inc @@ -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 diff --git a/src/test_roms/scrolling_colors.s b/src/test_roms/scrolling_colors.s new file mode 100644 index 0000000..57f8554 --- /dev/null +++ b/src/test_roms/scrolling_colors.s @@ -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