diff --git a/src/apu.rs b/src/apu.rs index f7c1147..c474050 100644 --- a/src/apu.rs +++ b/src/apu.rs @@ -6,7 +6,64 @@ use iced::{ }; use tracing::debug; -pub enum None {} +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)] @@ -28,26 +85,23 @@ bitfield::bitfield! { shift, set_shift: 2, 0; } -bitfield::bitfield! { - #[derive(Clone, Copy, PartialEq, Eq)] - pub struct LengthTimerHigh(u8); - impl Debug; - length, set_length: 7, 3; - timer_high, set_timer_high: 2, 0; -} - #[derive(Debug, Clone)] struct PulseChannel { enabled: bool, duty_vol: DutyVol, sweep: Sweep, - timer_low: u8, - length_timer_high: LengthTimerHigh, + sweep_reload: bool, - cur_time: u16, - cur_length: u8, + counter: LengthCounter, + + period: u16, + period_timer: u16, cur: u8, sample: u8, + + envelope_start: bool, + envelope_counter: u8, + envelope_divider: u8, } impl PulseChannel { @@ -56,33 +110,55 @@ 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) - } - pub fn reset(&mut self) { - // TODO - self.cur_time = self.timer(); - self.cur_length = self.length_timer_high.length(); - self.cur = 0; + pub fn write(&mut self, offset: u16, val: u8) { + match offset { + 0x00 => { + self.duty_vol.0 = val; + self.envelope_start = true; + } + 0x01 => { + self.sweep.0 = val; + self.sweep_reload = true; + }, + 0x02 => self.period = self.period & 0x700 | (val as u16), + 0x03 => { + let reg = LengthTimerHigh(val); + self.counter.write(reg.length()); + self.period = (self.period & 0xFF) | (reg.timer_high() << 8); + self.period_timer = self.period; + self.cur = 0; + } + _ => unreachable!(), + } } + + fn volume(&self) -> u8 { + if self.duty_vol.const_vol() { + self.duty_vol.volume() + } else { + self.envelope_counter + } + } + pub fn clock(&mut self) { - if self.cur_time == 0 { - self.cur_time = self.timer(); + 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], @@ -96,30 +172,81 @@ impl PulseChannel { 0x00 }; } else { - self.cur_time -= 1; + self.period_timer -= 1; } // TODO } pub fn cur_sample(&self) -> u8 { - self.sample + 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) { 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 - " + "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() @@ -128,51 +255,59 @@ impl PulseChannel { bitfield::bitfield! { #[derive(Clone, Copy, PartialEq, Eq, Hash)] - pub struct LengthCounter(u8); + pub struct LengthCounterReg(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, + length: LengthCounterReg, reload: bool, + counter: LengthCounter, length_counter: u16, cur: u8, - cur_time: u16, + period: u16, + period_timer: u16, sample: u8, } impl TriangleChannel { pub fn new() -> Self { Self { - length: LengthCounter(0), - timer_low: 0, - length_load: LengthLoad(0), + length: LengthCounterReg(0), + counter: LengthCounter::new(), reload: false, enabled: false, sample: 0, - cur_time: 0, + period: 0, + period_timer: 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 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 { @@ -180,9 +315,9 @@ impl TriangleChannel { } pub fn clock(&mut self) { - if self.length_counter > 0 && self.timer() > 0 { - if self.cur_time == 0 { - self.cur_time = self.timer(); + 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, @@ -190,7 +325,7 @@ impl TriangleChannel { self.cur = (self.cur + 1) % SAMPLES.len() as u8; self.sample = SAMPLES[self.cur as usize]; } else { - self.cur_time -= 1; + self.period_timer -= 1; } } } @@ -227,10 +362,10 @@ Output: {10:>3} ${10:02X} ", self.length.value(), self.length.halt(), - self.timer(), + self.period, 0, self.enabled, - self.cur_time, + self.period_timer, self.cur, 0, self.length_counter, @@ -242,26 +377,93 @@ Output: {10:>3} ${10:02X} } } +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, - sample: u8, + 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, - sample: 0, + 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 { - self.sample + 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 clock(&mut self) {} pub fn q_frame_clock(&mut self) {} - pub fn h_frame_clock(&mut self) {} + pub fn h_frame_clock(&mut self) { + self.q_frame_clock(); + } pub fn view(&self) -> Element<'_, T> { text!("").font(Font::MONOSPACE).into() @@ -282,6 +484,16 @@ impl DeltaChannel { } } + pub fn write(&mut self, offset: u16, val: u8) { + match offset { + 0x00 => (), + 0x01 => (), + 0x02 => (), + 0x03 => (), + _ => unreachable!(), + } + } + pub fn cur_sample(&self) -> u8 { self.sample } @@ -373,48 +585,12 @@ impl APU { pub fn write_reg(&mut self, offset: u16, val: u8) { // println!("APU write: {offset:02X} <= {val:02X}"); match offset { - 0x00 => self.pulse_1.duty_vol.0 = val, - 0x01 => self.pulse_1.sweep.0 = val, - 0x02 => self.pulse_1.timer_low = val, - 0x03 => { - self.pulse_1.length_timer_high.0 = val; - self.pulse_1.reset(); - } - 0x04 => self.pulse_2.duty_vol.0 = val, - 0x05 => self.pulse_2.sweep.0 = val, - 0x06 => self.pulse_2.timer_low = val, - 0x07 => { - self.pulse_2.length_timer_high.0 = val; - self.pulse_2.reset(); - } - 0x09 => (), // Unused, technically noise channel? - 0x08 => { - 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 => { - // TODO: Noise channel - } - 0x10 => { - assert_eq!(val, 0x00); - // TODO: implement this value - } - 0x11 => { - // TODO: load dmc counter with (val & 7F) - } - 0x12 => { - // TODO: DMC - } - 0x13 => { - // TODO: DMC - } + 0x00..0x04 => self.pulse_1.write(offset - 0x00, val), + 0x04..0x08 => self.pulse_2.write(offset - 0x04, val), + 0x08..0x0C => self.triangle.write(offset - 0x08, val), + 0x0C..0x10 => self.noise.write(offset - 0x0C, val), + 0x10..0x14 => self.dmc.write(offset - 0x10, val), + 0x14 => (), 0x15 => { self.dmc.enabled = val & 0b0001_0000 != 0; self.noise.enabled = val & 0b0000_1000 != 0; @@ -451,19 +627,6 @@ impl APU { } 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); diff --git a/src/audio.rs b/src/audio.rs index 2046488..f024497 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -74,6 +74,7 @@ impl Audio { 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]) { @@ -89,6 +90,7 @@ impl Audio { self.last = self.rb.occupied_len(); if self.last > 9000 { self.paused.store(false, std::sync::atomic::Ordering::Release); + let _ = self._stream.play(); } } } diff --git a/src/main.rs b/src/main.rs index 75d9577..dd1bb91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,9 @@ use tracing_subscriber::EnvFilter; // 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 = concat!(env!("ROM_DIR"), "/", "apu_pulse_channel_1_evelope.nes"); +const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_pulse_1.nes"); +// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "apu_triangle.nes"); // const ROM_FILE: &str = "./Super Mario Bros. (World).nes"; // const ROM_FILE: &str = "./cpu_timing_test.nes"; // const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.nes"; diff --git a/src/test_roms/apu_pulse_1.s b/src/test_roms/apu_pulse_1.s new file mode 100644 index 0000000..b0c6587 --- /dev/null +++ b/src/test_roms/apu_pulse_1.s @@ -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 diff --git a/src/test_roms/apu_pulse_channel_1.s b/src/test_roms/apu_pulse_channel_1.s index d4786a2..ccee354 100644 --- a/src/test_roms/apu_pulse_channel_1.s +++ b/src/test_roms/apu_pulse_channel_1.s @@ -1,4 +1,5 @@ .include "testing.s" +.include "minimal_ppu.s" reset: sei @@ -6,15 +7,6 @@ reset: 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 @@ -31,34 +23,7 @@ vwait2: ; 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 + jmp init_ppu_and_wait ; zp_res zp_res TIMER_LOW diff --git a/src/test_roms/apu_pulse_channel_1_evelope.s b/src/test_roms/apu_pulse_channel_1_evelope.s new file mode 100644 index 0000000..d0c68af --- /dev/null +++ b/src/test_roms/apu_pulse_channel_1_evelope.s @@ -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 diff --git a/src/test_roms/common/audio_inp.s b/src/test_roms/common/audio_inp.s new file mode 100644 index 0000000..83e059b --- /dev/null +++ b/src/test_roms/common/audio_inp.s @@ -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 diff --git a/src/test_roms/common/bitmap_font.s b/src/test_roms/common/bitmap_font.s new file mode 100644 index 0000000..bf66c88 --- /dev/null +++ b/src/test_roms/common/bitmap_font.s @@ -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 diff --git a/src/test_roms/common/joysticks.s b/src/test_roms/common/joysticks.s new file mode 100644 index 0000000..bd99f88 --- /dev/null +++ b/src/test_roms/common/joysticks.s @@ -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 + diff --git a/src/test_roms/common/minimal_ppu.s b/src/test_roms/common/minimal_ppu.s new file mode 100644 index 0000000..c90256b --- /dev/null +++ b/src/test_roms/common/minimal_ppu.s @@ -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