commit
d4fffa7c46
|
@ -8,6 +8,8 @@ This is an NES emulator and a work in progress. The CPU and PPU work, though the
|
||||||
|
|
||||||
- NTSC timing
|
- NTSC timing
|
||||||
|
|
||||||
|
<img src="pics/smb.png" width=600>
|
||||||
|
|
||||||
## Controls:
|
## Controls:
|
||||||
```
|
```
|
||||||
Button | Key
|
Button | Key
|
||||||
|
@ -30,6 +32,8 @@ Thanks to Michael Fogleman's https://github.com/fogleman/nes for getting me unst
|
||||||
|
|
||||||
- More mappers (only NROM/mapper 0 implemented so far)
|
- More mappers (only NROM/mapper 0 implemented so far)
|
||||||
|
|
||||||
- Audio
|
- DMC audio channel, high- and low-pass filters, APU cleanup/timing fix
|
||||||
|
|
||||||
|
- Save/load functionality and battery-backed RAM solution
|
||||||
|
|
||||||
- Player 2 controller?
|
- Player 2 controller?
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
|
@ -0,0 +1,39 @@
|
||||||
|
pub struct DMC {
|
||||||
|
pub sample: u16,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub interrupt: bool,
|
||||||
|
pub length_counter: usize,
|
||||||
|
pub bytes_remaining: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DMC {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
DMC {
|
||||||
|
sample: 0,
|
||||||
|
enabled: false,
|
||||||
|
bytes_remaining: 0,
|
||||||
|
interrupt: false,
|
||||||
|
length_counter: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock(&mut self) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_control(&mut self, value: u8) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn direct_load(&mut self, value: u8) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_sample_address(&mut self, value: u8) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_sample_length(&mut self, value: u8) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,276 @@
|
||||||
|
mod noise;
|
||||||
|
mod square;
|
||||||
|
mod triangle;
|
||||||
|
mod dmc;
|
||||||
|
|
||||||
|
use noise::Noise;
|
||||||
|
use square::Square;
|
||||||
|
use triangle::Triangle;
|
||||||
|
use dmc::DMC;
|
||||||
|
|
||||||
|
// APU clock ticks every other CPU cycle.
|
||||||
|
// Frame counter only ticks every 3728.5 APU ticks, and in audio frames of 4 or 5.
|
||||||
|
// Length counter controls note durations.
|
||||||
|
|
||||||
|
// We need to take a sample 44100 times per second. The CPU clocks (not steps) at 1.789773 MHz. Meaning the APU, going half as fast,
|
||||||
|
// clocks 894,886.5 times per second. 894,886.5/44,100=20.29 APU clocks per audio sample.
|
||||||
|
|
||||||
|
// TODO: organize APU structs
|
||||||
|
|
||||||
|
const FRAME_COUNTER_STEPS: [usize; 5] = [3728, 7456, 11185, 14914, 18640];
|
||||||
|
const CYCLES_PER_SAMPLE: f32 = 894_886.5/44_100.0; // APU frequency over sample frequency. May need to turn this down slightly as it's outputting less than 44_100Hz.
|
||||||
|
// const CYCLES_PER_SAMPLE: f32 = 20.0;
|
||||||
|
const LENGTH_COUNTER_TABLE: [u8; 32] = [
|
||||||
|
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, 30,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct Apu {
|
||||||
|
square1: Square,
|
||||||
|
square2: Square,
|
||||||
|
triangle: Triangle,
|
||||||
|
noise: Noise,
|
||||||
|
dmc: DMC,
|
||||||
|
|
||||||
|
square_table: Vec<f32>,
|
||||||
|
tnd_table: Vec<f32>,
|
||||||
|
|
||||||
|
frame_counter: u8,
|
||||||
|
current_frame: u8,
|
||||||
|
interrupt_inhibit: bool,
|
||||||
|
frame_interrupt: bool,
|
||||||
|
cycle: usize,
|
||||||
|
remainder: f32, // keep sample at 44100Hz
|
||||||
|
pub trigger_irq: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apu {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let square_table = (0..31).map(|x| 95.52/((8128.0 / x as f32) + 100.0)).collect();
|
||||||
|
let tnd_table = (0..203).map(|x| 163.67/((24329.0 / x as f32) + 100.0)).collect();
|
||||||
|
Apu {
|
||||||
|
square1: Square::new(false),
|
||||||
|
square2: Square::new(true),
|
||||||
|
triangle: Triangle::new(),
|
||||||
|
noise: Noise::new(),
|
||||||
|
dmc: DMC::new(),
|
||||||
|
|
||||||
|
square_table: square_table,
|
||||||
|
tnd_table: tnd_table,
|
||||||
|
|
||||||
|
frame_counter: 0,
|
||||||
|
current_frame: 0,
|
||||||
|
interrupt_inhibit: false,
|
||||||
|
frame_interrupt: false,
|
||||||
|
cycle: 0,
|
||||||
|
remainder: 0_f32,
|
||||||
|
trigger_irq: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock(&mut self) -> Option<f32> {
|
||||||
|
let mut sample = None;
|
||||||
|
|
||||||
|
// Clock each channel
|
||||||
|
self.square1.clock();
|
||||||
|
self.square2.clock();
|
||||||
|
self.triangle.clock();
|
||||||
|
self.triangle.clock(); // TODO: hacky. clocking triangle twice because it runs every CPU cycle
|
||||||
|
self.noise.clock();
|
||||||
|
self.dmc.clock();
|
||||||
|
|
||||||
|
// Send sample to buffer if necessary
|
||||||
|
if self.remainder > CYCLES_PER_SAMPLE {
|
||||||
|
sample = Some(self.mix());
|
||||||
|
self.remainder -= 20.0;
|
||||||
|
}
|
||||||
|
self.remainder += 1.0;
|
||||||
|
|
||||||
|
|
||||||
|
// Step frame counter if necessary
|
||||||
|
// if (self.frame_counter == 4 && FRAME_COUNTER_STEPS[..4].contains(&self.cycle))
|
||||||
|
// || (self.frame_counter == 5 && FRAME_COUNTER_STEPS.contains(&self.cycle)) {
|
||||||
|
if FRAME_COUNTER_STEPS.contains(&self.cycle) {
|
||||||
|
self.clock_frame_counter();
|
||||||
|
}
|
||||||
|
self.cycle += 1;
|
||||||
|
if (self.frame_counter == 4 && self.cycle == 14915) || self.cycle == 18641 {
|
||||||
|
self.cycle = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sample
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mix(&self) -> f32 {
|
||||||
|
let square_out = self.square_table[(self.square1.sample + self.square2.sample) as usize];
|
||||||
|
let tnd_out = self.tnd_table[((3*self.triangle.sample)+(2*self.noise.sample) + self.dmc.sample) as usize];
|
||||||
|
square_out + tnd_out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_reg(&mut self, address: usize, value: u8) {
|
||||||
|
// println!("writing 0b{:08b} to 0x{:X}", value, address);
|
||||||
|
match address {
|
||||||
|
0x4000 => self.square1.write_duty(value),
|
||||||
|
0x4001 => self.square1.write_sweep(value),
|
||||||
|
0x4002 => self.square1.write_timer_low(value),
|
||||||
|
0x4003 => self.square1.write_timer_high(value),
|
||||||
|
0x4004 => self.square2.write_duty(value),
|
||||||
|
0x4005 => self.square2.write_sweep(value),
|
||||||
|
0x4006 => self.square2.write_timer_low(value),
|
||||||
|
0x4007 => self.square2.write_timer_high(value),
|
||||||
|
0x4008 => self.triangle.write_counter(value),
|
||||||
|
0x4009 => (),
|
||||||
|
0x400A => self.triangle.write_timer_low(value),
|
||||||
|
0x400B => self.triangle.write_timer_high(value),
|
||||||
|
0x400C => self.noise.write_envelope(value),
|
||||||
|
0x400D => (),
|
||||||
|
0x400E => self.noise.write_loop_noise(value),
|
||||||
|
0x400F => self.noise.write_length_counter(value),
|
||||||
|
0x4010 => self.dmc.write_control(value),
|
||||||
|
0x4011 => self.dmc.direct_load(value),
|
||||||
|
0x4012 => self.dmc.write_sample_address(value),
|
||||||
|
0x4013 => self.dmc.write_sample_length(value),
|
||||||
|
0x4014 => (),
|
||||||
|
0x4015 => self.write_control(value),
|
||||||
|
0x4016 => (),
|
||||||
|
0x4017 => self.write_frame_counter(value),
|
||||||
|
_ => panic!("bad address written: 0x{:X}", address),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mode 0: mode 1: function
|
||||||
|
// --------- ----------- -----------------------------
|
||||||
|
// - - - f - - - - - IRQ (if bit 6 is clear)
|
||||||
|
// - l - l - l - - l Length counter and sweep
|
||||||
|
// e e e e e e e - e Envelope and linear counter
|
||||||
|
fn clock_frame_counter(&mut self) {
|
||||||
|
if !(self.frame_counter == 5 && self.current_frame == 3) {
|
||||||
|
// step envelopes
|
||||||
|
self.square1.clock_envelope();
|
||||||
|
self.square2.clock_envelope();
|
||||||
|
self.triangle.clock_linear_counter();
|
||||||
|
self.noise.clock_envelope();
|
||||||
|
}
|
||||||
|
if (self.current_frame == 1)
|
||||||
|
|| (self.frame_counter == 4 && self.current_frame == 3)
|
||||||
|
|| (self.frame_counter == 5 && self.current_frame == 4)
|
||||||
|
{
|
||||||
|
// step length counters and sweep units
|
||||||
|
self.square1.clock_sweep();
|
||||||
|
self.square2.clock_sweep();
|
||||||
|
self.square1.clock_length_counter();
|
||||||
|
self.square2.clock_length_counter();
|
||||||
|
self.triangle.clock_length_counter();
|
||||||
|
self.noise.clock_length_counter();
|
||||||
|
}
|
||||||
|
if self.frame_counter == 4 && self.current_frame == 3 && !self.interrupt_inhibit {
|
||||||
|
self.trigger_irq = true;
|
||||||
|
}
|
||||||
|
// advance counter
|
||||||
|
self.current_frame += 1;
|
||||||
|
if self.current_frame == self.frame_counter {
|
||||||
|
self.current_frame = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU writes to $4015
|
||||||
|
fn write_control(&mut self, value: u8) {
|
||||||
|
// Writing to this register clears the DMC interrupt flag.
|
||||||
|
self.dmc.interrupt = false;
|
||||||
|
// Writing a zero to any of the channel enable bits will silence that channel and immediately set its length counter to 0.
|
||||||
|
if value & (1<<0) != 0 {
|
||||||
|
self.square1.enabled = true;
|
||||||
|
} else {
|
||||||
|
self.square1.enabled = false;
|
||||||
|
self.square1.length_counter = 0;
|
||||||
|
}
|
||||||
|
if value & (1<<1) != 0 {
|
||||||
|
self.square2.enabled = true;
|
||||||
|
} else {
|
||||||
|
self.square2.enabled = false;
|
||||||
|
self.square2.length_counter = 0;
|
||||||
|
}
|
||||||
|
if value & (1<<2) != 0 {
|
||||||
|
self.triangle.enabled = true;
|
||||||
|
} else {
|
||||||
|
self.triangle.enabled = false;
|
||||||
|
self.triangle.length_counter = 0;
|
||||||
|
}
|
||||||
|
if value & (1<<3) != 0 {
|
||||||
|
self.noise.enabled = true;
|
||||||
|
} else {
|
||||||
|
self.noise.enabled = false;
|
||||||
|
self.noise.length_counter = 0;
|
||||||
|
}
|
||||||
|
if value & (1<<4) != 0 {
|
||||||
|
self.dmc.enabled = true;
|
||||||
|
// If the DMC bit is set, the DMC sample will be restarted only if its bytes remaining is 0.
|
||||||
|
// If there are bits remaining in the 1-byte sample buffer, these will finish playing before the next sample is fetched.
|
||||||
|
if self.dmc.bytes_remaining != 0 {
|
||||||
|
// TODO: how does dmc repeat?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.dmc.enabled = false;
|
||||||
|
self.dmc.length_counter = 0;
|
||||||
|
// If the DMC bit is clear, the DMC bytes remaining will be set to 0 and the DMC will silence when it empties.
|
||||||
|
self.dmc.bytes_remaining = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU reads from $4015
|
||||||
|
pub fn read_status(&mut self) -> u8 {
|
||||||
|
// IF-D NT21: DMC interrupt (I), frame interrupt (F), DMC active (D), length counter > 0 (N/T/2/1)
|
||||||
|
let mut val = 0;
|
||||||
|
// N/T/2/1 will read as 1 if the corresponding length counter is greater than 0. For the triangle channel, the status of the linear counter is irrelevant.
|
||||||
|
if self.square1.length_counter != 0 {
|
||||||
|
val |= 1<<0;
|
||||||
|
}
|
||||||
|
if self.square2.length_counter != 0 {
|
||||||
|
val |= 1<<1;
|
||||||
|
}
|
||||||
|
if self.triangle.length_counter != 0 {
|
||||||
|
val |= 1<<2;
|
||||||
|
}
|
||||||
|
if self.noise.length_counter != 0 {
|
||||||
|
val |= 1<<3;
|
||||||
|
}
|
||||||
|
// D will read as 1 if the DMC bytes remaining is more than 0.
|
||||||
|
if self.dmc.bytes_remaining != 0 {
|
||||||
|
val |= 1<<4;
|
||||||
|
}
|
||||||
|
if self.frame_interrupt {
|
||||||
|
val |= 1<<6;
|
||||||
|
}
|
||||||
|
if self.dmc.interrupt {
|
||||||
|
val |= 1<<7;
|
||||||
|
}
|
||||||
|
// Reading this register clears the frame interrupt flag (but not the DMC interrupt flag).
|
||||||
|
self.frame_interrupt = false;
|
||||||
|
// TODO: If an interrupt flag was set at the same moment of the read, it will read back as 1 but it will not be cleared.
|
||||||
|
val
|
||||||
|
}
|
||||||
|
|
||||||
|
// $4017
|
||||||
|
fn write_frame_counter(&mut self, value: u8) {
|
||||||
|
// 0 selects 4-step sequence, 1 selects 5-step sequence
|
||||||
|
self.frame_counter = if value & (1<<7) == 0 { 4 } else { 5 };
|
||||||
|
// If set, the frame interrupt flag is cleared, otherwise it is unaffected.
|
||||||
|
if value & (1<<6) != 0 {
|
||||||
|
self.interrupt_inhibit = false;
|
||||||
|
}
|
||||||
|
// If the mode flag is set, then both "quarter frame" and "half frame" signals are also generated.
|
||||||
|
if self.frame_counter == 5 {
|
||||||
|
// Clock envelopes, length counters, and sweep units
|
||||||
|
self.square1.clock_envelope();
|
||||||
|
self.square1.clock_sweep();
|
||||||
|
self.square1.clock_length_counter();
|
||||||
|
self.square2.clock_envelope();
|
||||||
|
self.square2.clock_sweep();
|
||||||
|
self.square2.clock_length_counter();
|
||||||
|
self.triangle.clock_linear_counter();
|
||||||
|
self.triangle.clock_length_counter();
|
||||||
|
self.noise.clock_envelope();
|
||||||
|
self.noise.clock_length_counter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
|
||||||
|
const NOISE_TABLE: [u16; 16] = [4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068];
|
||||||
|
|
||||||
|
// $400E M---.PPPP Mode and period (write)
|
||||||
|
// bit 7 M--- ---- Mode flag
|
||||||
|
pub struct Noise {
|
||||||
|
pub sample: u16,
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
envelope: u16, // constant volume/envelope period. reflects what was last written to $4000/$4004
|
||||||
|
envelope_divider: u16,
|
||||||
|
decay_counter: u16, // remainder of envelope divider
|
||||||
|
constant_volume_flag: bool, // (0: use volume from envelope; 1: use constant volume)
|
||||||
|
start: bool, // restarts envelope
|
||||||
|
|
||||||
|
timer: u16,
|
||||||
|
timer_period: u16,
|
||||||
|
|
||||||
|
pub length_counter: u8,
|
||||||
|
length_counter_halt: bool,
|
||||||
|
|
||||||
|
linear_feedback_sr: u16,
|
||||||
|
mode: bool, // also called loop noise, bit 7 of $400E
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Noise {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Noise {
|
||||||
|
sample: 0,
|
||||||
|
enabled: false,
|
||||||
|
envelope: 0,
|
||||||
|
envelope_divider: 0,
|
||||||
|
decay_counter: 0,
|
||||||
|
constant_volume_flag: false,
|
||||||
|
start: false,
|
||||||
|
timer: 0,
|
||||||
|
timer_period: 0,
|
||||||
|
length_counter: 0,
|
||||||
|
length_counter_halt: false,
|
||||||
|
linear_feedback_sr: 1, // On power-up, the shift register is loaded with the value 1.
|
||||||
|
mode: false, // also called loop noise, bit 7 of $400E
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock(&mut self) {
|
||||||
|
if self.timer == 0 {
|
||||||
|
self.clock_linear_counter();
|
||||||
|
} else {
|
||||||
|
self.timer -= 1;
|
||||||
|
}
|
||||||
|
// The mixer receives the current envelope volume except when
|
||||||
|
// Bit 0 of the shift register is set, or the length counter is zero
|
||||||
|
self.sample = if self.linear_feedback_sr & 1 == 1 || self.length_counter == 0 {
|
||||||
|
0
|
||||||
|
} else if self.constant_volume_flag {
|
||||||
|
self.envelope
|
||||||
|
} else {
|
||||||
|
self.decay_counter
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock_linear_counter(&mut self) {
|
||||||
|
// When the timer clocks the shift register, the following actions occur in order:
|
||||||
|
// Feedback is calculated as the exclusive-OR of bit 0
|
||||||
|
let bit0 = self.linear_feedback_sr & (1 << 0);
|
||||||
|
// and one other bit: bit 6 if Mode flag is set, otherwise bit 1.
|
||||||
|
let bit_num = if self.mode { 6 } else { 1 };
|
||||||
|
let other_bit = (self.linear_feedback_sr & (1 << bit_num)) >> bit_num;
|
||||||
|
let feedback = bit0 ^ other_bit;
|
||||||
|
// The shift register is shifted right by one bit.
|
||||||
|
self.linear_feedback_sr >>= 1;
|
||||||
|
// Bit 14, the leftmost bit, is set to the feedback calculated earlier.
|
||||||
|
self.linear_feedback_sr |= feedback << 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock_envelope(&mut self) {
|
||||||
|
// When clocked by the frame counter, one of two actions occurs:
|
||||||
|
// if the start flag is clear, the divider is clocked,
|
||||||
|
if !self.start {
|
||||||
|
self.clock_envelope_divider();
|
||||||
|
} else {
|
||||||
|
self.start = false; // otherwise the start flag is cleared,
|
||||||
|
self.decay_counter = 15; // the decay level counter is loaded with 15,
|
||||||
|
self.envelope_divider = self.envelope; // and the divider's period is immediately reloaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_envelope_divider(&mut self) {
|
||||||
|
// When the divider is clocked while at 0, it is loaded with V and clocks the decay level counter.
|
||||||
|
if self.envelope_divider == 0 {
|
||||||
|
self.envelope_divider = self.envelope;
|
||||||
|
// Then one of two actions occurs: If the counter is non-zero, it is decremented,
|
||||||
|
if self.decay_counter != 0 {
|
||||||
|
self.decay_counter -= 1;
|
||||||
|
} else if self.length_counter_halt {
|
||||||
|
// otherwise if the loop flag is set, the decay level counter is loaded with 15.
|
||||||
|
self.decay_counter = 15;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.envelope_divider -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock_length_counter(&mut self) {
|
||||||
|
if !(self.length_counter == 0 || self.length_counter_halt) {
|
||||||
|
self.length_counter -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// $400C
|
||||||
|
pub fn write_envelope(&mut self, value: u8) {
|
||||||
|
self.length_counter_halt = (value >> 5) & 1 == 1;
|
||||||
|
self.constant_volume_flag = (value >> 4) & 1 == 1;
|
||||||
|
self.envelope = value as u16 & 0b1111;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $400E
|
||||||
|
pub fn write_loop_noise(&mut self, value: u8) {
|
||||||
|
self.mode = value >> 7 == 1;
|
||||||
|
self.timer_period = NOISE_TABLE[(value & 0b1111) as usize];
|
||||||
|
}
|
||||||
|
|
||||||
|
// $400F
|
||||||
|
pub fn write_length_counter(&mut self, value: u8) {
|
||||||
|
self.length_counter = value >> 3;
|
||||||
|
self.start = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the timer clocks the shift register, the following actions occur in order:
|
||||||
|
|
||||||
|
// 1. Feedback is calculated as the exclusive-OR of bit 0 and one other bit: bit 6 if Mode flag is set, otherwise bit 1.
|
||||||
|
// 2. The shift register is shifted right by one bit.
|
||||||
|
// 3. Bit 14, the leftmost bit, is set to the feedback calculated earlier.
|
||||||
|
|
||||||
|
// This results in a pseudo-random bit sequence, 32767 steps long when Mode flag is clear,
|
||||||
|
// and randomly 93 or 31 steps long otherwise. (The particular 31- or 93-step sequence depends
|
||||||
|
// on where in the 32767-step sequence the shift register was when Mode flag was set).
|
|
@ -0,0 +1,208 @@
|
||||||
|
const DUTY_CYCLE_SEQUENCES: [[u8; 8]; 4] = [
|
||||||
|
[0, 1, 0, 0, 0, 0, 0, 0],
|
||||||
|
[0, 1, 1, 0, 0, 0, 0, 0],
|
||||||
|
[0, 1, 1, 1, 1, 0, 0, 0],
|
||||||
|
[1, 0, 0, 1, 1, 1, 1, 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct Square {
|
||||||
|
pub sample: u16,
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
duty_cycle: [u8; 8], // "sequencer", set to one of the lines in DUTY_CYCLE_SEQUENCES
|
||||||
|
duty_counter: usize, // current index within the duty_cycle
|
||||||
|
|
||||||
|
envelope: u16, // constant volume/envelope period. reflects what was last written to $4000/$4004
|
||||||
|
envelope_divider: u16,
|
||||||
|
decay_counter: u16, // remainder of envelope divider
|
||||||
|
constant_volume_flag: bool, // (0: use volume from envelope; 1: use constant volume)
|
||||||
|
start: bool, // restarts envelope
|
||||||
|
|
||||||
|
length_counter_halt: bool, // (this bit is also the envelope's loop flag)
|
||||||
|
pub length_counter: u8,
|
||||||
|
|
||||||
|
timer: u16,
|
||||||
|
timer_period: u16,
|
||||||
|
|
||||||
|
target_period: u16,
|
||||||
|
sweep_divider: u16, // Period, P
|
||||||
|
sweep_counter: u16,
|
||||||
|
shift_count: u8,
|
||||||
|
sweep_enabled: bool,
|
||||||
|
sweep_negate: bool,
|
||||||
|
sweep_reload: bool,
|
||||||
|
|
||||||
|
second_channel: bool, // hack to detect timing difference in clock_sweep()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Square {
|
||||||
|
pub fn new(second_channel: bool) -> Self {
|
||||||
|
Square {
|
||||||
|
sample: 0,
|
||||||
|
enabled: false,
|
||||||
|
|
||||||
|
duty_cycle: DUTY_CYCLE_SEQUENCES[0],
|
||||||
|
duty_counter: 0,
|
||||||
|
|
||||||
|
envelope: 0,
|
||||||
|
envelope_divider: 0,
|
||||||
|
decay_counter: 0,
|
||||||
|
constant_volume_flag: false,
|
||||||
|
start: false,
|
||||||
|
|
||||||
|
timer: 0,
|
||||||
|
timer_period: 0,
|
||||||
|
|
||||||
|
target_period: 0,
|
||||||
|
sweep_divider: 0,
|
||||||
|
sweep_counter: 0,
|
||||||
|
shift_count: 0,
|
||||||
|
sweep_enabled: false,
|
||||||
|
sweep_negate: false,
|
||||||
|
sweep_reload: false,
|
||||||
|
|
||||||
|
length_counter: 0,
|
||||||
|
length_counter_halt: false,
|
||||||
|
|
||||||
|
second_channel: second_channel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock(&mut self) {
|
||||||
|
// The sequencer is clocked by an 11-bit timer. Given the timer value t = HHHLLLLLLLL formed by timer high and timer low, this timer is updated every APU cycle
|
||||||
|
// (i.e., every second CPU cycle), and counts t, t-1, ..., 0, t, t-1, ..., clocking the waveform generator when it goes from 0 to t.
|
||||||
|
if self.timer == 0 {
|
||||||
|
self.timer = self.timer_period;
|
||||||
|
self.duty_counter = (self.duty_counter + 1) % 8;
|
||||||
|
} else {
|
||||||
|
self.timer -= 1;
|
||||||
|
}
|
||||||
|
// Update volume for this channel
|
||||||
|
// The mixer receives the current envelope volume except when
|
||||||
|
self.sample = if self.duty_cycle[self.duty_counter] == 0 // the sequencer output is zero, or
|
||||||
|
|| self.timer_period > 0x7FF // overflow from the sweep unit's adder is silencing the channel,
|
||||||
|
|| self.length_counter == 0 // the length counter is zero, or
|
||||||
|
|| self.timer_period < 8 // the timer has a value less than eight.
|
||||||
|
{
|
||||||
|
0
|
||||||
|
} else if self.constant_volume_flag {
|
||||||
|
self.envelope
|
||||||
|
} else {
|
||||||
|
self.decay_counter
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock_envelope(&mut self) {
|
||||||
|
// When clocked by the frame counter, one of two actions occurs:
|
||||||
|
// if the start flag is clear, the divider is clocked,
|
||||||
|
if !self.start {
|
||||||
|
self.clock_envelope_divider();
|
||||||
|
} else {
|
||||||
|
self.start = false; // otherwise the start flag is cleared,
|
||||||
|
self.decay_counter = 15; // the decay level counter is loaded with 15,
|
||||||
|
self.envelope_divider = self.envelope; // and the divider's period is immediately reloaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_envelope_divider(&mut self) {
|
||||||
|
// When the divider is clocked while at 0, it is loaded with V and clocks the decay level counter.
|
||||||
|
if self.envelope_divider == 0 {
|
||||||
|
self.envelope_divider = self.envelope;
|
||||||
|
// Then one of two actions occurs: If the counter is non-zero, it is decremented,
|
||||||
|
if self.decay_counter != 0 {
|
||||||
|
self.decay_counter -= 1;
|
||||||
|
} else if self.length_counter_halt {
|
||||||
|
// otherwise if the loop flag is set, the decay level counter is loaded with 15.
|
||||||
|
self.decay_counter = 15;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.envelope_divider -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock_length_counter(&mut self) {
|
||||||
|
if !(self.length_counter == 0 || self.length_counter_halt) {
|
||||||
|
self.length_counter -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock_sweep(&mut self) {
|
||||||
|
self.calculate_target_period();
|
||||||
|
// When the frame counter sends a half-frame clock (at 120 or 96 Hz), two things happen.
|
||||||
|
// If the divider's counter is zero, the sweep is enabled, and the sweep unit is not muting the channel: The pulse's period is adjusted.
|
||||||
|
if self.sweep_counter == 0 && self.sweep_enabled && !(self.timer_period < 8 || self.target_period > 0x7FF) {
|
||||||
|
self.timer_period = self.target_period;
|
||||||
|
}
|
||||||
|
// If the divider's counter is zero or the reload flag is true: The counter is set to P and the reload flag is cleared. Otherwise, the counter is decremented.
|
||||||
|
if self.sweep_counter == 0 || self.sweep_reload {
|
||||||
|
self.sweep_counter = self.sweep_divider;
|
||||||
|
self.sweep_reload = false;
|
||||||
|
if self.sweep_enabled { self.timer_period = self.target_period; } // This fixes the DK walking sound. Why? Not reflected in documentation.
|
||||||
|
} else {
|
||||||
|
self.sweep_counter -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whenever the current period changes for any reason, whether by $400x writes or by sweep, the target period also changes.
|
||||||
|
pub fn calculate_target_period(&mut self) {
|
||||||
|
// The sweep unit continuously calculates each channel's target period in this way:
|
||||||
|
// A barrel shifter shifts the channel's 11-bit raw timer period right by the shift count, producing the change amount.
|
||||||
|
let change = self.timer_period >> self.shift_count;
|
||||||
|
// If the negate flag is true, the change amount is made negative.
|
||||||
|
// The target period is the sum of the current period and the change amount.
|
||||||
|
if self.sweep_negate {
|
||||||
|
self.target_period = self.timer_period - change;
|
||||||
|
// The two pulse channels have their adders' carry inputs wired differently,
|
||||||
|
// which produces different results when each channel's change amount is made negative:
|
||||||
|
// Pulse 1 adds the ones' complement (−c − 1). Making 20 negative produces a change amount of −21.
|
||||||
|
// Pulse 2 adds the two's complement (−c). Making 20 negative produces a change amount of −20.
|
||||||
|
if !self.second_channel {
|
||||||
|
self.target_period -= 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.target_period = self.timer_period + change;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// $4000/$4004
|
||||||
|
pub fn write_duty(&mut self, value: u8) {
|
||||||
|
// The duty cycle is changed (see table below), but the sequencer's current position isn't affected.
|
||||||
|
self.duty_cycle = DUTY_CYCLE_SEQUENCES[(value >> 6) as usize];
|
||||||
|
self.length_counter_halt = value & (1<<5) != 0;
|
||||||
|
self.constant_volume_flag = value & (1<<4) != 0;
|
||||||
|
self.envelope = value as u16 & 0b1111;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $4001/$4005
|
||||||
|
pub fn write_sweep(&mut self, value: u8) {
|
||||||
|
self.sweep_enabled = value >> 7 == 1;
|
||||||
|
self.sweep_divider = ((value as u16 >> 4) & 0b111) + 1;
|
||||||
|
self.sweep_negate = value & 0b1000 != 0;
|
||||||
|
self.shift_count = value & 0b111;
|
||||||
|
self.sweep_reload = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $4002/$4006
|
||||||
|
pub fn write_timer_low(&mut self, value: u8) {
|
||||||
|
self.timer_period &= 0b00000111_00000000; // mask off everything but high 3 bits of 11-bit timer
|
||||||
|
self.timer_period |= value as u16; // apply low 8 bits
|
||||||
|
self.calculate_target_period();
|
||||||
|
}
|
||||||
|
|
||||||
|
// $4003/$4007
|
||||||
|
pub fn write_timer_high(&mut self, value: u8) {
|
||||||
|
// LLLL.Lttt Pulse channel 1 length counter load and timer (write)
|
||||||
|
// TODO: thought the below meant that the length counter was only set if the channel was enabled, but apparently not as not having it fixes start game noise in DK.
|
||||||
|
// When the enabled bit is cleared (via $4015), the length counter is forced to 0 and cannot be changed until enabled is set again (the length counter's previous value is lost).
|
||||||
|
if self.enabled {
|
||||||
|
self.length_counter = super::LENGTH_COUNTER_TABLE[value as usize >> 3];
|
||||||
|
}
|
||||||
|
let timer_high = value as u16 & 0b0000_0111;
|
||||||
|
self.timer_period &= 0b11111000_11111111; // mask off high 3 bits of 11-bit timer
|
||||||
|
self.timer_period |= timer_high << 8; // apply high timer bits in their place
|
||||||
|
self.calculate_target_period();
|
||||||
|
// The sequencer is immediately restarted at the first value of the current sequence. The envelope is also restarted. The period divider is not reset.
|
||||||
|
self.duty_counter = 0;
|
||||||
|
self.start = true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
const WAVEFORM: [u16; 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,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct Triangle {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub sample: u16,
|
||||||
|
|
||||||
|
timer: u16,
|
||||||
|
timer_period: u16,
|
||||||
|
waveform_counter: usize,
|
||||||
|
|
||||||
|
pub length_counter: u8,
|
||||||
|
length_counter_halt: bool, // (this bit is also the linear counter's control flag) / (this bit is also the length counter halt flag)
|
||||||
|
|
||||||
|
linear_counter: u8,
|
||||||
|
counter_reload_value: u8,
|
||||||
|
linear_counter_reload: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Triangle {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Triangle {
|
||||||
|
enabled: false,
|
||||||
|
sample: 0,
|
||||||
|
timer: 0,
|
||||||
|
timer_period: 0,
|
||||||
|
waveform_counter: 0,
|
||||||
|
length_counter: 0,
|
||||||
|
length_counter_halt: false,
|
||||||
|
linear_counter: 0,
|
||||||
|
counter_reload_value: 0,
|
||||||
|
linear_counter_reload: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock(&mut self) {
|
||||||
|
if self.timer == 0 {
|
||||||
|
self.timer = self.timer_period;
|
||||||
|
// The sequencer is clocked by the timer as long as both the linear counter and the length counter are nonzero.
|
||||||
|
if self.linear_counter != 0 && self.length_counter != 0 {
|
||||||
|
self.waveform_counter = (self.waveform_counter + 1) % 32;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.timer -= 1;
|
||||||
|
}
|
||||||
|
self.sample = WAVEFORM[self.waveform_counter];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock_linear_counter(&mut self) {
|
||||||
|
// When the frame counter generates a linear counter clock, the following actions occur in order:
|
||||||
|
// If the linear counter reload flag is set, the linear counter is reloaded with the counter reload value,
|
||||||
|
if self.linear_counter_reload {
|
||||||
|
self.linear_counter = self.counter_reload_value;
|
||||||
|
} else if self.linear_counter != 0 { // otherwise if the linear counter is non-zero, it is decremented.
|
||||||
|
self.linear_counter -= 1;
|
||||||
|
}
|
||||||
|
// If the control flag is clear, the linear counter reload flag is cleared.
|
||||||
|
if !self.length_counter_halt {
|
||||||
|
self.linear_counter_reload = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock_length_counter(&mut self) {
|
||||||
|
if !(self.length_counter == 0 || self.length_counter_halt) {
|
||||||
|
self.length_counter -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// $4008
|
||||||
|
pub fn write_counter(&mut self, value: u8) {
|
||||||
|
self.length_counter_halt = value >> 7 != 0;
|
||||||
|
self.counter_reload_value = (value << 1) >> 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $400A
|
||||||
|
pub fn write_timer_low(&mut self, value: u8) {
|
||||||
|
self.timer_period &= 0b00000111_00000000;
|
||||||
|
self.timer_period |= value as u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $400B
|
||||||
|
pub fn write_timer_high(&mut self, value: u8) {
|
||||||
|
if self.enabled {
|
||||||
|
self.length_counter = super::LENGTH_COUNTER_TABLE[value as usize >> 3];
|
||||||
|
}
|
||||||
|
self.timer_period &= 0b00000000_11111111;
|
||||||
|
let timer_high = value & 0b0000_0111;
|
||||||
|
self.timer_period |= (timer_high as u16) << 8;
|
||||||
|
self.linear_counter_reload = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
extern crate sdl2;
|
||||||
|
|
||||||
|
use sdl2::audio::{AudioCallback, AudioSpecDesired};
|
||||||
|
|
||||||
|
pub fn initialize(context: &sdl2::Sdl) -> Result<sdl2::audio::AudioQueue<f32>, String> {
|
||||||
|
let audio_subsystem = context.audio()?;
|
||||||
|
|
||||||
|
let desired_spec = AudioSpecDesired {
|
||||||
|
freq: Some(44_100),
|
||||||
|
channels: Some(1), // mono
|
||||||
|
samples: None, // default sample size
|
||||||
|
};
|
||||||
|
|
||||||
|
audio_subsystem.open_queue(None, &desired_spec)
|
||||||
|
}
|
|
@ -76,6 +76,9 @@ pub struct Cpu {
|
||||||
// ppu
|
// ppu
|
||||||
pub ppu: super::Ppu,
|
pub ppu: super::Ppu,
|
||||||
|
|
||||||
|
// apu
|
||||||
|
pub apu: super::Apu,
|
||||||
|
|
||||||
// controller
|
// controller
|
||||||
pub strobe: u8,
|
pub strobe: u8,
|
||||||
pub button_states: u8, // Player 1 controller
|
pub button_states: u8, // Player 1 controller
|
||||||
|
@ -83,7 +86,7 @@ pub struct Cpu {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cpu {
|
impl Cpu {
|
||||||
pub fn new(cart: &super::Cartridge, ppu: super::Ppu) -> Self {
|
pub fn new(cart: &super::Cartridge, ppu: super::Ppu, apu: super::Apu) -> Self {
|
||||||
let mut cpu = Cpu{
|
let mut cpu = Cpu{
|
||||||
mem: vec![0; 0x2000],
|
mem: vec![0; 0x2000],
|
||||||
A: 0, X: 0, Y: 0,
|
A: 0, X: 0, Y: 0,
|
||||||
|
@ -95,6 +98,7 @@ impl Cpu {
|
||||||
prg_rom: cart.prg_rom.clone(),
|
prg_rom: cart.prg_rom.clone(),
|
||||||
mapper_func: cart.cpu_mapper_func,
|
mapper_func: cart.cpu_mapper_func,
|
||||||
ppu: ppu,
|
ppu: ppu,
|
||||||
|
apu: apu,
|
||||||
strobe: 0,
|
strobe: 0,
|
||||||
button_states: 0,
|
button_states: 0,
|
||||||
button_number: 0,
|
button_number: 0,
|
||||||
|
@ -142,16 +146,22 @@ impl Cpu {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn step(&mut self) -> u64 {
|
pub fn step(&mut self) -> u64 {
|
||||||
|
|
||||||
// skip cycles from OAM DMA if necessary
|
// skip cycles from OAM DMA if necessary
|
||||||
if self.delay > 0 {
|
if self.delay > 0 {
|
||||||
self.delay -= 1;
|
self.delay -= 1;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle interrupts
|
||||||
if self.ppu.trigger_nmi {
|
if self.ppu.trigger_nmi {
|
||||||
self.nmi();
|
self.nmi();
|
||||||
}
|
}
|
||||||
self.ppu.trigger_nmi = false;
|
self.ppu.trigger_nmi = false;
|
||||||
|
if self.apu.trigger_irq && (self.P & INTERRUPT_DISABLE_FLAG == 0) {
|
||||||
|
self.irq();
|
||||||
|
}
|
||||||
|
self.apu.trigger_irq = false;
|
||||||
|
|
||||||
// back up clock so we know how many cycles we complete
|
// back up clock so we know how many cycles we complete
|
||||||
let clock = self.clock;
|
let clock = self.clock;
|
||||||
|
@ -184,14 +194,15 @@ impl Cpu {
|
||||||
// memory interface
|
// memory interface
|
||||||
fn read(&mut self, address: usize) -> u8 {
|
fn read(&mut self, address: usize) -> u8 {
|
||||||
let val = match address {
|
let val = match address {
|
||||||
0x0000...0x1FFF => self.mem[address % 0x0800],
|
0x0000..=0x1FFF => self.mem[address % 0x0800],
|
||||||
0x2000...0x3FFF => self.read_ppu_reg(address % 8),
|
0x2000..=0x3FFF => self.read_ppu_reg(address % 8),
|
||||||
0x4014 => self.read_ppu_reg(8),
|
0x4014 => self.read_ppu_reg(8),
|
||||||
|
0x4015 => self.apu.read_status(),
|
||||||
0x4016 => self.read_controller(),
|
0x4016 => self.read_controller(),
|
||||||
0x4000...0x4017 => 0, // APU stuff
|
0x4000..=0x4017 => 0, // can't read from these APU registers
|
||||||
0x4018...0x401F => 0, // APU and I/O functionality that is normally disabled. See CPU Test Mode.
|
0x4018..=0x401F => 0, // APU and I/O functionality that is normally disabled. See CPU Test Mode.
|
||||||
0x4020...0xFFFF => { // Cartridge space: PRG ROM, PRG RAM, and mapper registers
|
0x4020..=0xFFFF => { // Cartridge space: PRG ROM, PRG RAM, and mapper registers
|
||||||
*(self.mapper_func)(self, address, false).unwrap() // unwraping because mapper funcs won't return None for reads.
|
*(self.mapper_func)(self, address, false).unwrap() // unwrapping because mapper funcs won't return None for reads.
|
||||||
},
|
},
|
||||||
_ => panic!("invalid read from 0x{:02x}", address),
|
_ => panic!("invalid read from 0x{:02x}", address),
|
||||||
};
|
};
|
||||||
|
@ -201,13 +212,13 @@ impl Cpu {
|
||||||
// memory interface
|
// memory interface
|
||||||
fn write(&mut self, address: usize, val: u8) {
|
fn write(&mut self, address: usize, val: u8) {
|
||||||
match address {
|
match address {
|
||||||
0x0000...0x1FFF => self.mem[address % 0x0800] = val,
|
0x0000..=0x1FFF => self.mem[address % 0x0800] = val,
|
||||||
0x2000...0x3FFF => self.write_ppu_reg(address % 8, val),
|
0x2000..=0x3FFF => self.write_ppu_reg(address % 8, val),
|
||||||
0x4014 => self.write_ppu_reg(8, val),
|
0x4014 => self.write_ppu_reg(8, val),
|
||||||
0x4016 => self.write_controller(val),
|
0x4016 => self.write_controller(val),
|
||||||
0x4000...0x4017 => (), // APU stuff
|
0x4000..=0x4017 => self.apu.write_reg(address, val), // APU stuff
|
||||||
0x4018...0x401F => (), // APU and I/O functionality that is normally disabled. See CPU Test Mode.
|
0x4018..=0x401F => (), // APU and I/O functionality that is normally disabled. See CPU Test Mode.
|
||||||
0x4020...0xFFFF => { // Cartridge space: PRG ROM, PRG RAM, and mapper registers
|
0x4020..=0xFFFF => { // Cartridge space: PRG ROM, PRG RAM, and mapper registers
|
||||||
match (self.mapper_func)(self, address, true) {
|
match (self.mapper_func)(self, address, true) {
|
||||||
Some(loc) => *loc = val,
|
Some(loc) => *loc = val,
|
||||||
None => (),
|
None => (),
|
||||||
|
|
|
@ -123,7 +123,7 @@ impl super::Cpu {
|
||||||
self.push((self.PC & 0xFF) as u8); // push low byte
|
self.push((self.PC & 0xFF) as u8); // push low byte
|
||||||
self.push(self.P | 0b00110000); // push status register with break bits set
|
self.push(self.P | 0b00110000); // push status register with break bits set
|
||||||
self.P |= INTERRUPT_DISABLE_FLAG; // set interrupt disable flag
|
self.P |= INTERRUPT_DISABLE_FLAG; // set interrupt disable flag
|
||||||
self.PC = ((self.read(IRQ_VECTOR + 1) as usize) << 8) // set program counter to IRQ/BRK vector, taking high _byte
|
self.PC = ((self.read(IRQ_VECTOR + 1) as usize) << 8) // set program counter to IRQ/BRK vector, taking high byte
|
||||||
+ (self.read(IRQ_VECTOR) as usize); // and low byte
|
+ (self.read(IRQ_VECTOR) as usize); // and low byte
|
||||||
self.clock += 5; // total of 7 cycles, 2 come from implied()
|
self.clock += 5; // total of 7 cycles, 2 come from implied()
|
||||||
}
|
}
|
||||||
|
@ -527,9 +527,19 @@ impl super::Cpu {
|
||||||
self.push((self.PC & 0xFF) as u8); // push low byte
|
self.push((self.PC & 0xFF) as u8); // push low byte
|
||||||
self.push(self.P | 0b00110000); // push status register with break bits set
|
self.push(self.P | 0b00110000); // push status register with break bits set
|
||||||
self.P |= INTERRUPT_DISABLE_FLAG; // set interrupt disable flag
|
self.P |= INTERRUPT_DISABLE_FLAG; // set interrupt disable flag
|
||||||
self.PC = ((self.read(NMI_VECTOR + 1) as usize) << 8) // set program counter to IRQ/BRK vector, taking high _byte
|
self.PC = ((self.read(NMI_VECTOR + 1) as usize) << 8) // set program counter to NMI vector, taking high byte
|
||||||
+ (self.read(NMI_VECTOR) as usize); // and low byte
|
+ (self.read(NMI_VECTOR) as usize); // and low byte
|
||||||
self.clock += 7;
|
self.clock += 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn irq(&mut self) {
|
||||||
|
self.push((self.PC >> 8) as u8); // push high byte
|
||||||
|
self.push((self.PC & 0xFF) as u8); // push low byte
|
||||||
|
self.push(self.P & 0b11001111); // push status register with break bits cleared
|
||||||
|
self.P |= INTERRUPT_DISABLE_FLAG; // set interrupt disable flag
|
||||||
|
self.PC = ((self.read(IRQ_VECTOR + 1) as usize) << 8) // set program counter to IRQ/BRK vector, taking high byte
|
||||||
|
+ (self.read(IRQ_VECTOR) as usize); // and low byte
|
||||||
|
self.clock += 7;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
88
src/main.rs
88
src/main.rs
|
@ -2,15 +2,19 @@ use std::time::{Instant, Duration};
|
||||||
|
|
||||||
mod cpu;
|
mod cpu;
|
||||||
mod ppu;
|
mod ppu;
|
||||||
|
mod apu;
|
||||||
mod cartridge;
|
mod cartridge;
|
||||||
mod screen;
|
|
||||||
mod input;
|
mod input;
|
||||||
|
mod screen;
|
||||||
|
mod audio;
|
||||||
|
|
||||||
use cpu::Cpu;
|
use cpu::Cpu;
|
||||||
use ppu::Ppu;
|
use ppu::Ppu;
|
||||||
|
use apu::Apu;
|
||||||
use cartridge::Cartridge;
|
use cartridge::Cartridge;
|
||||||
use screen::{init_window, draw_pixel, draw_to_window};
|
|
||||||
use input::poll_buttons;
|
use input::poll_buttons;
|
||||||
|
use screen::{init_window, draw_pixel, draw_to_window};
|
||||||
|
use audio::initialize;
|
||||||
|
|
||||||
use sdl2::keyboard::Keycode;
|
use sdl2::keyboard::Keycode;
|
||||||
use sdl2::event::Event;
|
use sdl2::event::Event;
|
||||||
|
@ -20,50 +24,78 @@ use sdl2::pixels::PixelFormatEnum;
|
||||||
|
|
||||||
fn main() -> Result<(), String> {
|
fn main() -> Result<(), String> {
|
||||||
// Set up screen
|
// Set up screen
|
||||||
let sdl_context = sdl2::init()?;
|
let sdl_context = sdl2::init()?;
|
||||||
let mut event_pump = sdl_context.event_pump()?;
|
let mut event_pump = sdl_context.event_pump()?;
|
||||||
let (mut canvas, texture_creator) = init_window(&sdl_context).expect("Could not create window");
|
let (mut canvas, texture_creator) = init_window(&sdl_context).expect("Could not create window");
|
||||||
let mut texture = texture_creator.create_texture_streaming(
|
let mut texture = texture_creator.create_texture_streaming(
|
||||||
PixelFormatEnum::RGB24, 256*screen::SCALE_FACTOR as u32, 240*screen::SCALE_FACTOR as u32)
|
PixelFormatEnum::RGB24, 256*screen::SCALE_FACTOR as u32, 240*screen::SCALE_FACTOR as u32)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let byte_width = 256 * 3 * screen::SCALE_FACTOR; // 256 NES pixels, 3 bytes for each pixel (RGB 24-bit), and NES-to-SDL scale factor
|
let byte_width = 256 * 3 * screen::SCALE_FACTOR; // 256 NES pixels, 3 bytes for each pixel (RGB 24-bit), and NES-to-SDL scale factor
|
||||||
let byte_height = 240 * screen::SCALE_FACTOR; // NES image is 240 pixels tall, multiply by scale factor for total number of rows needed
|
let byte_height = 240 * screen::SCALE_FACTOR; // NES image is 240 pixels tall, multiply by scale factor for total number of rows needed
|
||||||
let mut screen_buffer = vec![0; byte_width * byte_height]; // contains raw RGB data for the screen
|
let mut screen_buffer = vec![0; byte_width * byte_height]; // contains raw RGB data for the screen
|
||||||
|
|
||||||
|
// Set up audio
|
||||||
|
let mut audio_device = audio::initialize(&sdl_context).expect("Could not create audio device");
|
||||||
|
let mut half_cycle = false;
|
||||||
|
audio_device.resume();
|
||||||
|
|
||||||
// Initialize hardware components
|
// Initialize hardware components
|
||||||
let cart = Cartridge::new();
|
let cart = Cartridge::new();
|
||||||
let ppu = Ppu::new(&cart);
|
let ppu = Ppu::new(&cart);
|
||||||
let mut cpu = Cpu::new(&cart, ppu);
|
let apu = Apu::new();
|
||||||
|
let mut cpu = Cpu::new(&cart, ppu, apu);
|
||||||
|
|
||||||
// For throttling to 60 FPS
|
// For throttling to 60 FPS
|
||||||
let mut timer = Instant::now();
|
let mut timer = Instant::now();
|
||||||
let mut fps_timer = Instant::now();
|
let mut fps_timer = Instant::now();
|
||||||
let mut fps = 0;
|
let mut fps = 0;
|
||||||
|
let mut sps = 0;
|
||||||
|
|
||||||
// PROFILER.lock().unwrap().start("./main.profile").unwrap();
|
// PROFILER.lock().unwrap().start("./main.profile").unwrap();
|
||||||
'running: loop {
|
'running: loop {
|
||||||
// perform 1 cpu instruction, getting back number of clock cycles it took
|
// step CPU: perform 1 cpu instruction, getting back number of clock cycles it took
|
||||||
let num_cycles = cpu.step();
|
let cpu_cycles = cpu.step();
|
||||||
// maintain ratio of 3 ppu cycles for 1 cpu step
|
// clock APU every other CPU cycle
|
||||||
for _i in 0..num_cycles * 3 {
|
let mut apu_cycles = cpu_cycles / 2;
|
||||||
let (pixel, end_of_frame) = cpu.ppu.step();
|
if cpu_cycles & 1 == 1 { // if cpu step took an odd number of cycles
|
||||||
|
if half_cycle { // and we have a half-cycle stored
|
||||||
|
apu_cycles += 1; // use it
|
||||||
|
half_cycle = false;
|
||||||
|
} else {
|
||||||
|
half_cycle = true; // or save it for next odd cpu step
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _ in 0..apu_cycles {
|
||||||
|
match cpu.apu.clock() {
|
||||||
|
Some(sample) => {
|
||||||
|
sps += 1;
|
||||||
|
if sps < 44_100 {audio_device.queue(&vec![sample]);} // TODO: fix this
|
||||||
|
// audio_device.queue(&vec![sample]);
|
||||||
|
},
|
||||||
|
None => (),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// clock PPU three times for every CPU cycle
|
||||||
|
for _ in 0..cpu_cycles * 3 {
|
||||||
|
let (pixel, end_of_frame) = cpu.ppu.clock();
|
||||||
match pixel {
|
match pixel {
|
||||||
Some((x, y, color)) => draw_pixel(&mut screen_buffer, x, y, color),
|
Some((x, y, color)) => draw_pixel(&mut screen_buffer, x, y, color),
|
||||||
None => (),
|
None => (),
|
||||||
};
|
};
|
||||||
if end_of_frame {
|
if end_of_frame {
|
||||||
fps += 1;
|
fps += 1; // keep track of how many frames we've rendered this second
|
||||||
draw_to_window(&mut texture, &mut canvas, &screen_buffer)?;
|
draw_to_window(&mut texture, &mut canvas, &screen_buffer)?; // draw the buffer to the window with SDL
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
// if we're running faster than 60Hz, kill time
|
||||||
if now < timer + Duration::from_millis(1000/60) {
|
if now < timer + Duration::from_millis(1000/60) {
|
||||||
std::thread::sleep(timer + Duration::from_millis(1000/60) - now);
|
std::thread::sleep(timer + Duration::from_millis(1000/60) - now);
|
||||||
}
|
}
|
||||||
timer = Instant::now();
|
timer = Instant::now();
|
||||||
|
// listen for Esc or window close. TODO: does this prevent keyboard events from being handled?
|
||||||
for event in event_pump.poll_iter() {
|
for event in event_pump.poll_iter() {
|
||||||
match event {
|
match event {
|
||||||
Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
|
Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. }
|
||||||
break 'running
|
=> { break 'running },
|
||||||
},
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,11 +112,31 @@ fn main() -> Result<(), String> {
|
||||||
println!("fps: {}", fps);
|
println!("fps: {}", fps);
|
||||||
fps = 0;
|
fps = 0;
|
||||||
fps_timer = now;
|
fps_timer = now;
|
||||||
|
|
||||||
|
println!("samples per second: {}", sps);
|
||||||
|
sps = 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// PROFILER.lock().unwrap().stop().unwrap();
|
// PROFILER.lock().unwrap().stop().unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: reset function?
|
/*
|
||||||
// TODO: save/load functionality
|
TODO:
|
||||||
|
- DMC audio channel, high- and low-pass filters, refactor envelope
|
||||||
|
- name audio variables (dividers, counters, etc.) more consistently
|
||||||
|
- common mappers
|
||||||
|
- battery-backed RAM solution
|
||||||
|
- fix mysterious Mario pipe non-locations
|
||||||
|
- GUI? drag and drop ROMs?
|
||||||
|
- reset function
|
||||||
|
- save/load/pause functionality
|
||||||
|
|
||||||
|
|
||||||
|
Timing notes:
|
||||||
|
The PPU is throttled to 60Hz by sleeping in the main loop. This locks the CPU to roughly its intended speed, 1.789773MHz NTSC. The APU runs at half that.
|
||||||
|
The SDL audio device samples/outputs at 44,100Hz, so as long as the APU queues up 44,100 samples per second, it works.
|
||||||
|
But it's not doing so evenly. If PPU runs faster than 60Hz, audio will get skipped, and if slower, audio will pop/have gaps.
|
||||||
|
Need to probably lock everything to the APU but worried about checking time that often. Can do for some division of 44_100.
|
||||||
|
*/
|
||||||
|
|
|
@ -64,8 +64,8 @@ pub struct Ppu {
|
||||||
grayscale: bool,
|
grayscale: bool,
|
||||||
show_background_left: bool, // 1: Show background in leftmost 8 pixels of screen, 0: Hide
|
show_background_left: bool, // 1: Show background in leftmost 8 pixels of screen, 0: Hide
|
||||||
show_sprites_left: bool, // 1: Show sprites in leftmost 8 pixels of screen, 0: Hide
|
show_sprites_left: bool, // 1: Show sprites in leftmost 8 pixels of screen, 0: Hide
|
||||||
show_background: bool, // 1: Show background
|
show_background: bool, // 1: Show background
|
||||||
show_sprites: bool, // 1: Show sprites
|
show_sprites: bool, // 1: Show sprites
|
||||||
emphasize_red: bool, // Emphasize red
|
emphasize_red: bool, // Emphasize red
|
||||||
emphasize_green: bool, // Emphasize green
|
emphasize_green: bool, // Emphasize green
|
||||||
emphasize_blue: bool, // Emphasize blue
|
emphasize_blue: bool, // Emphasize blue
|
||||||
|
@ -143,7 +143,7 @@ impl Ppu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn step(&mut self) -> (Option<(usize, usize, (u8, u8, u8))>, bool) {
|
pub fn clock(&mut self) -> (Option<(usize, usize, (u8, u8, u8))>, bool) {
|
||||||
if self.nmi_delay > 0 {
|
if self.nmi_delay > 0 {
|
||||||
self.nmi_delay -= 1;
|
self.nmi_delay -= 1;
|
||||||
if self.nmi_delay == 0 && self.should_generate_nmi && self.vertical_blank {
|
if self.nmi_delay == 0 && self.should_generate_nmi && self.vertical_blank {
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
extern crate sdl2;
|
||||||
|
|
||||||
|
use sdl2::Sdl;
|
||||||
|
use sdl2::pixels::Color;
|
||||||
|
use sdl2::render::{Canvas, Texture, TextureCreator};
|
||||||
|
use sdl2::video::{Window, WindowContext};
|
||||||
|
|
||||||
|
pub const SCALE_FACTOR: usize = 3;
|
||||||
|
const BYTES_IN_COL: usize = SCALE_FACTOR * 3; // 3 bytes per pixel in RGB24. This represents a thick, SCALE_FACTOR-pixel-wide column.
|
||||||
|
const BYTES_IN_ROW: usize = BYTES_IN_COL * 256; // 256 = screen width in NES pixels. This represents a thin, one-SDL-pixel-tall row.
|
||||||
|
|
||||||
|
type RGBColor = (u8, u8, u8);
|
||||||
|
|
||||||
|
pub fn init_window(context: &Sdl) -> Result<(Canvas<Window>, TextureCreator<WindowContext>), String> {
|
||||||
|
let video_subsystem = context.video()?;
|
||||||
|
let window = video_subsystem.window("NESTUR", (256 * SCALE_FACTOR) as u32, (240 * SCALE_FACTOR) as u32)
|
||||||
|
.position_centered()
|
||||||
|
.opengl()
|
||||||
|
.build()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let mut canvas = window.into_canvas().build().map_err(|e| e.to_string())?;
|
||||||
|
let texture_creator = canvas.texture_creator();
|
||||||
|
canvas.set_draw_color(Color::RGB(0, 0, 0));
|
||||||
|
canvas.clear();
|
||||||
|
canvas.present();
|
||||||
|
Ok((canvas, texture_creator))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_pixel(buffer: &mut Vec<u8>, x: usize, y: usize, color: RGBColor) {
|
||||||
|
let (r, g, b) = color;
|
||||||
|
let nes_y_offset = y * BYTES_IN_ROW * SCALE_FACTOR; // find offset for thick, SCALE_FACTOR-pixel tall row
|
||||||
|
for sdl_row_num in 0..SCALE_FACTOR { // looping over one-pixel tall rows up to SCALE_FACTOR
|
||||||
|
let row_offset = nes_y_offset + (sdl_row_num * BYTES_IN_ROW); // row_offset is the offset within buffer of the thin row we're on
|
||||||
|
let nes_x_offset = x * BYTES_IN_COL; // find horizontal offset within row (in byte terms) of NES x-coordinate
|
||||||
|
for sdl_col_num in 0..SCALE_FACTOR { // for pixels up to SCALE_FACTOR, moving horizontally
|
||||||
|
let col_offset = nes_x_offset + (sdl_col_num * 3); // skip 3 bytes at a time, R/G/B for each pixel
|
||||||
|
let offset = row_offset + col_offset;
|
||||||
|
buffer[offset + 0] = r;
|
||||||
|
buffer[offset + 1] = g;
|
||||||
|
buffer[offset + 2] = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_to_window(texture: &mut Texture, canvas: &mut Canvas<Window>, buffer: &Vec<u8>) -> Result<(), String> {
|
||||||
|
texture.update(None, buffer, 256*3*SCALE_FACTOR)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
canvas.copy(&texture, None, None)?;
|
||||||
|
canvas.present();
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,51 +0,0 @@
|
||||||
extern crate sdl2;
|
|
||||||
|
|
||||||
use sdl2::Sdl;
|
|
||||||
use sdl2::pixels::Color;
|
|
||||||
use sdl2::render::{Canvas, Texture, TextureCreator};
|
|
||||||
use sdl2::video::{Window, WindowContext};
|
|
||||||
|
|
||||||
pub const SCALE_FACTOR: usize = 3;
|
|
||||||
const BYTES_IN_COL: usize = SCALE_FACTOR * 3; // 3 bytes per pixel in RGB24. This represents a thick, SCALE_FACTOR-pixel-wide column.
|
|
||||||
const BYTES_IN_ROW: usize = BYTES_IN_COL * 256; // 256 = screen width in NES pixels. This represents a thin, one-SDL-pixel-tall row.
|
|
||||||
|
|
||||||
type RGBColor = (u8, u8, u8);
|
|
||||||
|
|
||||||
pub fn init_window(context: &Sdl) -> Result<(Canvas<Window>, TextureCreator<WindowContext>), String> {
|
|
||||||
let video_subsystem = context.video()?;
|
|
||||||
let window = video_subsystem.window("NESTUR", (256 * SCALE_FACTOR) as u32, (240 * SCALE_FACTOR) as u32)
|
|
||||||
.position_centered()
|
|
||||||
.opengl()
|
|
||||||
.build()
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
let mut canvas = window.into_canvas().build().map_err(|e| e.to_string())?;
|
|
||||||
let texture_creator = canvas.texture_creator();
|
|
||||||
canvas.set_draw_color(Color::RGB(0, 0, 0));
|
|
||||||
canvas.clear();
|
|
||||||
canvas.present();
|
|
||||||
Ok((canvas, texture_creator))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_pixel(buffer: &mut Vec<u8>, x: usize, y: usize, color: RGBColor) {
|
|
||||||
let (r, g, b) = color;
|
|
||||||
let nes_y_offset = y * BYTES_IN_ROW * SCALE_FACTOR; // find offset for thick, SCALE_FACTOR-pixel tall row
|
|
||||||
for sdl_row_num in 0..SCALE_FACTOR { // looping over one-pixel tall rows up to SCALE_FACTOR
|
|
||||||
let row_offset = nes_y_offset + (sdl_row_num * BYTES_IN_ROW); // row_offset is the offset within buffer of the thin row we're on
|
|
||||||
let nes_x_offset = x * BYTES_IN_COL; // find horizontal offset within row (in byte terms) of NES x-coordinate
|
|
||||||
for sdl_col_num in 0..SCALE_FACTOR { // for pixels up to SCALE_FACTOR, moving horizontally
|
|
||||||
let col_offset = nes_x_offset + (sdl_col_num * 3); // skip 3 bytes at a time, R/G/B for each pixel
|
|
||||||
let offset = row_offset + col_offset;
|
|
||||||
buffer[offset + 0] = r;
|
|
||||||
buffer[offset + 1] = g;
|
|
||||||
buffer[offset + 2] = b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_to_window(texture: &mut Texture, canvas: &mut Canvas<Window>, buffer: &Vec<u8>) -> Result<(), String> {
|
|
||||||
texture.update(None, buffer, 256*3*SCALE_FACTOR)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
canvas.copy(&texture, None, None)?;
|
|
||||||
canvas.present();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
Loading…
Reference in New Issue