Skip to content

Commit ee1a59d

Browse files
committed
chore: start building out APU data structures
1 parent ecd9e56 commit ee1a59d

File tree

6 files changed

+228
-132
lines changed

6 files changed

+228
-132
lines changed

TODO.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
- [x] Figure out why sprites aren't drawing
22
- wasn't writing to OAM DMA correctly
3+
- [ ] try out code coverage
4+
https://github.com/xd009642/tarpaulin
5+
https://medium.com/@gnanaganesh/robust-rust-how-code-coverage-powers-rust-software-quality-417ef3ac2360
6+
https://blog.balthazar-rouberol.com/measuring-the-coverage-of-a-rust-program-in-github-actions
37
- [ ] Why are some sprites flipped around?
48
- [ ] Try an external debugger and setting breakpts
59
- [ ] Debugger view

justfile

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
alias cc := code_coverage
2+
13
run:
24
cargo run
35

@@ -16,8 +18,15 @@ build:
1618
test:
1719
cargo test
1820

19-
test_watch:
20-
git ls-files | entr just test
21+
test_watch_all:
22+
git ls-files | entr cargo test
23+
24+
test_watch TEST:
25+
git ls-files | entr cargo test {{TEST}}::tests
26+
27+
28+
code_coverage:
29+
cargo tarpaulin -o html
2130

2231
nestest:
2332
NESTEST_HACK=1 cargo run roms/nestest.nes > myout.log

src/apu/mod.rs

+62-128
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,100 @@
1+
mod pulse_register;
2+
13
use crate::core::Mem;
24

3-
use bitflags::bitflags;
5+
use pulse_register::PulseRegister;
46

57
/// Apu is the Audio Processing Unit
8+
/// https://www.nesdev.org/wiki/APU
69
pub struct Apu {
7-
// pulse_register:
8-
// Pulse ($4000–$4007)
910
pulse1: PulseRegister,
1011
pulse2: PulseRegister,
11-
// Triangle ($4008–$400B)
12-
// 2.4 Noise ($400C–$400F)
13-
// 2.5 DMC ($4010–$4013)
14-
// 2.5.1 Other Uses
15-
// 2.6 Status ($4015)
16-
// 2.7 Frame Counter ($4017)
17-
// 2.7.1 Length Counter
18-
length_counter: usize,
19-
}
20-
21-
bitflags! {
22-
/// Pulse
23-
/// https://www.nesdev.org/wiki/APU_Sweep
24-
struct Pulse: u8 {
25-
/// Duty Cycle
26-
const D = 0b1100_0000;
27-
/// Envelope loop / length counter halt
28-
const L = 0b0010_0000;
29-
/// Constant volume/envelope flag
30-
const C = 0b0001_0000;
31-
/// Volume/envelope divider period
32-
const V = 0b0000_1111;
33-
}
34-
}
35-
36-
bitflags! {
37-
/// APU Sweep
38-
/// https://www.nesdev.org/wiki/APU_Sweep
39-
struct Sweep: u8 {
40-
/// Enabled flag
41-
const E = 0b1000_0000;
42-
/// Period
43-
/// The divider's period is P + 1 half-frames
44-
const P = 0b0111_0000;
45-
/// Negate flag
46-
/// 0: add to period, sweeping toward lower frequencies
47-
/// 1: subtract from period, sweeping toward higher frequencies
48-
const N = 0b0000_1000;
49-
/// Shift count (number of bits).
50-
/// If SSS is 0, then behaves like E=0.
51-
const S = 0b0000_0111;
52-
}
53-
}
54-
55-
/// https://www.nesdev.org/wiki/APU_Pulse
56-
struct PulseRegister {
57-
data: [u8; 4],
12+
triangle: [u8; 4],
13+
noise: [u8; 4],
14+
dmc: [u8; 4],
15+
status: u8,
16+
frame_counter: u8,
5817
}
5918

60-
/// https://www.nesdev.org/wiki/APU_Length_Counter
61-
const LENGTH_TABLE: [u8; 32] = [
62-
10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22,
63-
192, 24, 72, 26, 16, 28, 32, 30,
64-
];
65-
66-
impl PulseRegister {
67-
fn duty_cycle(&self) -> u8 {
68-
let duty = (self.data[0] & Pulse::D.bits()) >> 6;
69-
assert!(duty < 4);
70-
duty
71-
}
72-
73-
fn is_length_counter_halted(&self) -> bool {
74-
Pulse::from_bits_truncate(self.data[0]).contains(Pulse::L)
75-
}
76-
77-
fn is_constant_volume(&self) -> bool {
78-
Pulse::from_bits_truncate(self.data[1]).contains(Pulse::C)
79-
}
80-
81-
/// The sequencer is clocked by an 11-bit timer.
82-
/// The timer value t = HHHLLLLLLLL is formed by timer high and timer low.
83-
fn timer(&self) -> u16 {
84-
let low8 = self.data[2] as u16;
85-
let hi3 = ((self.data[3] & 0b0000_0111) as u16) << 8;
86-
hi3 + low8
87-
}
88-
89-
/// Gives an index into the LENGTH_TABLE
90-
fn length_counter_load(&self) -> u8 {
91-
(self.data[3] & 0b1111_1000) >> 3
92-
}
93-
94-
fn is_sweep_enabled(&self) -> bool {
95-
let ssc = self.sweep_shift_count();
96-
let enabled = Sweep::from_bits_truncate(self.data[1]).contains(Sweep::E);
97-
ssc > 0 && enabled
98-
}
99-
100-
fn is_sweep_negated(&self) -> bool {
101-
Sweep::from_bits_truncate(self.data[1]).contains(Sweep::N)
102-
}
103-
104-
fn sweep_period(&self) -> u8 {
105-
let s = Sweep::from_bits_truncate(self.data[1]);
106-
Sweep::P.intersection(s).bits() >> 4
107-
}
108-
109-
fn sweep_shift_count(&self) -> u8 {
110-
let s = Sweep::from_bits_truncate(self.data[1]);
111-
Sweep::S.intersection(s).bits()
19+
impl Apu {
20+
pub fn new() -> Self {
21+
Self {
22+
pulse1: PulseRegister::new(),
23+
pulse2: PulseRegister::new(),
24+
triangle: [0; 4],
25+
noise: [0; 4],
26+
dmc: [0; 4],
27+
status: 0,
28+
frame_counter: 0,
29+
}
11230
}
11331
}
11432

115-
// // https://www.nesdev.org/wiki/APU_registers
33+
// https://www.nesdev.org/wiki/APU_Length_Counter
34+
// const LENGTH_TABLE: [u8; 32] = [
35+
// 10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22,
36+
// 192, 24, 72, 26, 16, 28, 32, 30,
37+
// ];
11638

11739
impl Mem for Apu {
11840
fn mem_read(&mut self, addr: u16) -> u8 {
11941
match addr {
120-
// They are write-only except $4015 which is read/write
12142
0x4000..=0x4013 | 0x4017 => panic!(
12243
"attempt to read from write only APU register: 0x{:04X}",
12344
addr
12445
),
125-
0x04015 => todo!("read APU status"),
46+
0x04015 => self.status,
12647
_ => panic!("invalid lookup: 0x{:04X} is not in APU memory map", addr),
12748
}
12849
}
12950

13051
fn mem_write(&mut self, addr: u16, val: u8) {
52+
let idx = (addr % 4) as usize;
13153
match addr {
132-
0x4000..=0x4003 => self.pulse1.data[(0x4000 - addr) as usize] = val,
133-
0x4004..=0x4007 => self.pulse2.data[(0x4004 - addr) as usize] = val,
134-
// 0x4008..=0x400B => panic!("APU write-only register: triangle register"),
135-
// 0x400C..=0x400F => panic!("APU write-only register: noise register"),
136-
// 0x4010..=0x4013 => todo!("dmc"),
137-
// They are write-only except $4015 which is read/write
138-
// 0x04015 => todo!("status"),
139-
// 0x04017 => todo!("frame counter"),
140-
_ => todo!(),
54+
0x4000..=0x4003 => self.pulse1.write_data(idx, val),
55+
0x4004..=0x4007 => self.pulse2.write_data(idx, val),
56+
0x4008..=0x400B => self.triangle[idx] = val,
57+
0x400C..=0x400F => self.noise[idx] = val,
58+
0x4010..=0x4013 => self.dmc[idx] = val,
59+
0x04015 => self.status = val,
60+
0x04017 => self.frame_counter = val,
61+
_ => panic!("invalid lookup: 0x{:04X} is not in APU memory map", addr),
14162
}
14263
}
14364
}
14465

14566
#[cfg(test)]
14667
mod tests {
147-
use super::PulseRegister;
68+
use super::*;
14869

14970
#[test]
150-
fn test_pulse_register_getters() {
151-
let mut p = PulseRegister { data: [0; 4] };
152-
assert_eq!(p.timer(), 0);
153-
assert_eq!(p.length_counter_load(), 0);
154-
p.data[2] = 0xff;
155-
p.data[3] = 0b0000_0111;
156-
assert_eq!(p.length_counter_load(), 0);
157-
assert_eq!(p.timer(), 2048 - 1);
158-
p.data[3] = 0xff;
159-
assert_eq!(p.length_counter_load(), 32 - 1);
71+
fn test_apu_mem_writes() {
72+
let mut apu = Apu::new();
73+
for x in 0..=0x13 {
74+
apu.mem_write(0x4000 + x, 0xff);
75+
}
76+
77+
apu.mem_write(0x4015, 123);
78+
assert_eq!(apu.status, 123);
79+
apu.mem_write(0x4017, 222);
80+
assert_eq!(apu.frame_counter, 222);
81+
}
16082

161-
assert_eq!(p.duty_cycle(), 0);
162-
assert_eq!(p.is_constant_volume(), false);
83+
#[test]
84+
#[should_panic(expected = "attempt to read from write only APU register")]
85+
fn test_apu_invalid_mem_read_panics() {
86+
let mut apu = Apu::new();
87+
_ = apu.mem_read(0x4000);
88+
}
16389

164-
assert_eq!(p.is_length_counter_halted(), true);
90+
#[test]
91+
fn test_apu_mem_read_status() {
92+
let mut apu = Apu::new();
93+
let data = apu.mem_read(0x4015);
94+
assert_eq!(data, 0);
95+
96+
apu.status = 123;
97+
let data = apu.mem_read(0x4015);
98+
assert_eq!(data, 123);
16599
}
166100
}

0 commit comments

Comments
 (0)