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
|
||||
|
||||
<img src="pics/smb.png" width=600>
|
||||
|
||||
## Controls:
|
||||
```
|
||||
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)
|
||||
|
||||
- Audio
|
||||
- DMC audio channel, high- and low-pass filters, APU cleanup/timing fix
|
||||
|
||||
- Save/load functionality and battery-backed RAM solution
|
||||
|
||||
- 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
|
||||
pub ppu: super::Ppu,
|
||||
|
||||
// apu
|
||||
pub apu: super::Apu,
|
||||
|
||||
// controller
|
||||
pub strobe: u8,
|
||||
pub button_states: u8, // Player 1 controller
|
||||
|
@ -83,7 +86,7 @@ pub struct 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{
|
||||
mem: vec![0; 0x2000],
|
||||
A: 0, X: 0, Y: 0,
|
||||
|
@ -95,6 +98,7 @@ impl Cpu {
|
|||
prg_rom: cart.prg_rom.clone(),
|
||||
mapper_func: cart.cpu_mapper_func,
|
||||
ppu: ppu,
|
||||
apu: apu,
|
||||
strobe: 0,
|
||||
button_states: 0,
|
||||
button_number: 0,
|
||||
|
@ -142,16 +146,22 @@ impl Cpu {
|
|||
}
|
||||
|
||||
pub fn step(&mut self) -> u64 {
|
||||
|
||||
// skip cycles from OAM DMA if necessary
|
||||
if self.delay > 0 {
|
||||
self.delay -= 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// handle interrupts
|
||||
if self.ppu.trigger_nmi {
|
||||
self.nmi();
|
||||
}
|
||||
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
|
||||
let clock = self.clock;
|
||||
|
@ -184,14 +194,15 @@ impl Cpu {
|
|||
// memory interface
|
||||
fn read(&mut self, address: usize) -> u8 {
|
||||
let val = match address {
|
||||
0x0000...0x1FFF => self.mem[address % 0x0800],
|
||||
0x2000...0x3FFF => self.read_ppu_reg(address % 8),
|
||||
0x0000..=0x1FFF => self.mem[address % 0x0800],
|
||||
0x2000..=0x3FFF => self.read_ppu_reg(address % 8),
|
||||
0x4014 => self.read_ppu_reg(8),
|
||||
0x4015 => self.apu.read_status(),
|
||||
0x4016 => self.read_controller(),
|
||||
0x4000...0x4017 => 0, // APU stuff
|
||||
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
|
||||
*(self.mapper_func)(self, address, false).unwrap() // unwraping because mapper funcs won't return None for reads.
|
||||
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.
|
||||
0x4020..=0xFFFF => { // Cartridge space: PRG ROM, PRG RAM, and mapper registers
|
||||
*(self.mapper_func)(self, address, false).unwrap() // unwrapping because mapper funcs won't return None for reads.
|
||||
},
|
||||
_ => panic!("invalid read from 0x{:02x}", address),
|
||||
};
|
||||
|
@ -201,13 +212,13 @@ impl Cpu {
|
|||
// memory interface
|
||||
fn write(&mut self, address: usize, val: u8) {
|
||||
match address {
|
||||
0x0000...0x1FFF => self.mem[address % 0x0800] = val,
|
||||
0x2000...0x3FFF => self.write_ppu_reg(address % 8, val),
|
||||
0x0000..=0x1FFF => self.mem[address % 0x0800] = val,
|
||||
0x2000..=0x3FFF => self.write_ppu_reg(address % 8, val),
|
||||
0x4014 => self.write_ppu_reg(8, val),
|
||||
0x4016 => self.write_controller(val),
|
||||
0x4000...0x4017 => (), // APU stuff
|
||||
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
|
||||
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.
|
||||
0x4020..=0xFFFF => { // Cartridge space: PRG ROM, PRG RAM, and mapper registers
|
||||
match (self.mapper_func)(self, address, true) {
|
||||
Some(loc) => *loc = val,
|
||||
None => (),
|
||||
|
|
|
@ -123,7 +123,7 @@ impl super::Cpu {
|
|||
self.push((self.PC & 0xFF) as u8); // push low byte
|
||||
self.push(self.P | 0b00110000); // push status register with break bits set
|
||||
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.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.P | 0b00110000); // push status register with break bits set
|
||||
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.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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
82
src/main.rs
82
src/main.rs
|
@ -2,15 +2,19 @@ use std::time::{Instant, Duration};
|
|||
|
||||
mod cpu;
|
||||
mod ppu;
|
||||
mod apu;
|
||||
mod cartridge;
|
||||
mod screen;
|
||||
mod input;
|
||||
mod screen;
|
||||
mod audio;
|
||||
|
||||
use cpu::Cpu;
|
||||
use ppu::Ppu;
|
||||
use apu::Apu;
|
||||
use cartridge::Cartridge;
|
||||
use screen::{init_window, draw_pixel, draw_to_window};
|
||||
use input::poll_buttons;
|
||||
use screen::{init_window, draw_pixel, draw_to_window};
|
||||
use audio::initialize;
|
||||
|
||||
use sdl2::keyboard::Keycode;
|
||||
use sdl2::event::Event;
|
||||
|
@ -30,40 +34,68 @@ fn main() -> Result<(), String> {
|
|||
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
|
||||
|
||||
// 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
|
||||
let cart = Cartridge::new();
|
||||
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
|
||||
let mut timer = Instant::now();
|
||||
let mut fps_timer = Instant::now();
|
||||
let mut fps = 0;
|
||||
let mut sps = 0;
|
||||
|
||||
// PROFILER.lock().unwrap().start("./main.profile").unwrap();
|
||||
'running: loop {
|
||||
// perform 1 cpu instruction, getting back number of clock cycles it took
|
||||
let num_cycles = cpu.step();
|
||||
// maintain ratio of 3 ppu cycles for 1 cpu step
|
||||
for _i in 0..num_cycles * 3 {
|
||||
let (pixel, end_of_frame) = cpu.ppu.step();
|
||||
// step CPU: perform 1 cpu instruction, getting back number of clock cycles it took
|
||||
let cpu_cycles = cpu.step();
|
||||
// clock APU every other CPU cycle
|
||||
let mut apu_cycles = cpu_cycles / 2;
|
||||
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 {
|
||||
Some((x, y, color)) => draw_pixel(&mut screen_buffer, x, y, color),
|
||||
None => (),
|
||||
};
|
||||
if end_of_frame {
|
||||
fps += 1;
|
||||
draw_to_window(&mut texture, &mut canvas, &screen_buffer)?;
|
||||
fps += 1; // keep track of how many frames we've rendered this second
|
||||
draw_to_window(&mut texture, &mut canvas, &screen_buffer)?; // draw the buffer to the window with SDL
|
||||
let now = Instant::now();
|
||||
// if we're running faster than 60Hz, kill time
|
||||
if now < timer + Duration::from_millis(1000/60) {
|
||||
std::thread::sleep(timer + Duration::from_millis(1000/60) - 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() {
|
||||
match event {
|
||||
Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
|
||||
break 'running
|
||||
},
|
||||
Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. }
|
||||
=> { break 'running },
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
@ -80,11 +112,31 @@ fn main() -> Result<(), String> {
|
|||
println!("fps: {}", fps);
|
||||
fps = 0;
|
||||
fps_timer = now;
|
||||
|
||||
println!("samples per second: {}", sps);
|
||||
sps = 0;
|
||||
|
||||
}
|
||||
}
|
||||
// PROFILER.lock().unwrap().stop().unwrap();
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -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 {
|
||||
self.nmi_delay -= 1;
|
||||
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