Partial audio implementation
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 10s

This commit is contained in:
2026-03-14 21:19:16 -05:00
parent 3372559c19
commit b9a30c286a
12 changed files with 1257 additions and 171 deletions

144
Cargo.lock generated
View File

@@ -70,6 +70,28 @@ dependencies = [
"equator",
]
[[package]]
name = "alsa"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3"
dependencies = [
"alsa-sys",
"bitflags 2.10.0",
"cfg-if",
"libc",
]
[[package]]
name = "alsa-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "android-activity"
version = "0.6.0"
@@ -771,6 +793,20 @@ dependencies = [
"libm",
]
[[package]]
name = "coreaudio-rs"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
dependencies = [
"bitflags 1.3.2",
"libc",
"objc2-audio-toolbox",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
]
[[package]]
name = "cosmic-text"
version = "0.15.0"
@@ -794,6 +830,36 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cpal"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb"
dependencies = [
"alsa",
"coreaudio-rs",
"dasp_sample",
"jni",
"js-sys",
"libc",
"mach2",
"ndk",
"ndk-context",
"num-derive",
"num-traits",
"objc2 0.6.3",
"objc2-audio-toolbox",
"objc2-avf-audio",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows 0.62.2",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -858,6 +924,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
[[package]]
name = "dasp_sample"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -2006,6 +2078,15 @@ dependencies = [
"num-traits",
]
[[package]]
name = "mach2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -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"

View File

@@ -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"

View File

@@ -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<T>(&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<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.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<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 cur_sample(&self) -> u8 {
self.sample
}
pub fn clock(&mut self) {}
pub fn q_frame_clock(&mut self) {}
pub fn h_frame_clock(&mut self) {}
pub fn int(&self) -> bool {
false
}
pub fn view<T>(&self) -> Element<'_, T> {
text!("").font(Font::MONOSPACE).into()
}
}
#[derive(Debug, Clone)]
@@ -131,6 +314,8 @@ pub struct APU {
noise: NoiseChannel,
dmc: DeltaChannel,
frame_counter: FrameCounter,
samples: Vec<u8>,
}
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()
}
}

94
src/audio.rs Normal file
View File

@@ -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<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);
}
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);
}
}
}

View File

@@ -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(""),

View File

@@ -10,6 +10,7 @@ mod ppu;
pub mod resize_watcher;
#[cfg(test)]
mod test_roms;
pub mod audio;
pub use ppu::{Color, PPU, RenderBuffer};
use tokio::io::AsyncReadExt as _;
@@ -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
}

View File

@@ -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<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()
});
// 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<Message> for Emulator {
type State = ();
fn update(
&self,
_state: &mut Self::State,
_event: &iced::Event,
_bounds: Rectangle,
_cursor: iced::advanced::mouse::Cursor,
) -> Option<widget::Action<Message>> {
// ~ 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<Message> for Emulator {
bounds: iced::Rectangle,
_cursor: mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
// 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<Message> 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()]
}
}

View File

@@ -128,7 +128,8 @@ pub struct OAM {
oam_read_buffer: u8,
edit_ver: usize,
sprite_output_units: Vec<SpriteOutputUnit>,
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
}
}
}

View File

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

View File

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

View File

@@ -34,6 +34,18 @@ JOY1 = $4016
JOY2 = $4017
SNDMODE = $4017
PULSE_CH1_DLCV = $4000
PULSE_CH1_SWEEP = $4001
PULSE_CH1_TLOW = $4002
PULSE_CH1_LCTH = $4003
PULSE_CH2_DLCV = $4004
PULSE_CH2_SWEEP = $4005
PULSE_CH2_TLOW = $4006
PULSE_CH2_LCTH = $4007
TRIANGLE_LINEAR_C = $4008
TRIANGLE_TIMER_LOW = $400A
TRIANGLE_LEN_T_HIGH = $400B
SNDMODE_NOIRQ = $40
.if CLOCK_RATE = 1789773

View File

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