Skip to content

Commit 9920f25

Browse files
committed
feat: track CPU and PPU cycles
1 parent a6d2ca3 commit 9920f25

File tree

4 files changed

+133
-40
lines changed

4 files changed

+133
-40
lines changed

src/bus.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ impl Bus {
4747

4848
self.rom.prg_rom[idx as usize]
4949
}
50+
51+
/// returns (scanline, clock_cycles)
52+
pub(crate) fn get_ppu_tick_status(&self) -> (usize, usize) {
53+
self.ppu.get_tick_status()
54+
}
5055
}
5156

5257
impl Mem for Bus {
@@ -95,13 +100,18 @@ impl Mem for Bus {
95100
7 => self.ppu.write_to_data(data),
96101
8..=u16::MAX => panic!("invalid PPU register IDX: {}", register_idx),
97102
}
98-
} else if addr == 0x4016 {
103+
} else if addr == 0x4014 {
99104
// 2.9 OAMDMA - Sprite DMA ($4014 write)
100105
panic!("attempt to read from write-only PPU register: 0x4016 (OAMDMA - Sprite DMA)");
101106
} else if (PRG_ROM_START..=PRG_ROM_END).contains(&addr) {
102107
panic!("attempt to write to ROM cartridge")
108+
} else if [0x4000, 0x4001, 0x4002, 0x4003, 0x4004, 0x4015, 0x4017].contains(&addr) {
109+
todo!(
110+
"attempt to write to addr=0x{:04X}. This will be the APU, later!",
111+
addr
112+
)
103113
} else {
104-
panic!("attempt to write to NYI section of memory")
114+
todo!("attempt to write to memory addr=0x{:04X}", addr)
105115
};
106116
}
107117
}

src/core.rs

+107-37
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{
44
bus::Bus,
55
ops::{addressing_mode_to_size, is_official, lookup_opcode, OpName},
66
rom::Rom,
7-
utility::addr_from,
7+
utility::{addr_from, is_addr_at_page_edge, is_page_cross},
88
};
99

1010
/// CPU (Central Processing Unit)
@@ -33,6 +33,9 @@ pub struct Cpu {
3333

3434
/// Bus
3535
bus: Bus,
36+
37+
/// CPU Cycles
38+
cycles: usize,
3639
}
3740

3841
#[derive(Debug, PartialEq, Eq)]
@@ -113,6 +116,7 @@ impl Cpu {
113116
y: 0,
114117
status: DEFAULT_STATUS,
115118
bus: Bus::new(rom),
119+
cycles: 0,
116120
}
117121
}
118122

@@ -156,27 +160,49 @@ impl Cpu {
156160
//
157161

158162
/// used for the "index indirect" and "indirect indexed" lookups
159-
fn mem_read_zero_page_wrapping(&mut self, ptr: u8) -> u16 {
163+
fn mem_read_zero_page_wrapping(&mut self, ptr: u8) -> (u16, bool) {
160164
let lo = self.mem_read(ptr as u16);
161-
let hi = self.mem_read(ptr.wrapping_add(1) as u16);
162-
(hi as u16) << 8 | (lo as u16)
165+
let (hi_ptr, is_page_crossed) = ptr.overflowing_add(1);
166+
let hi = self.mem_read(hi_ptr as u16);
167+
168+
let result = (hi as u16) << 8 | (lo as u16);
169+
170+
(result, is_page_crossed)
163171
}
164172

165-
fn get_operand_address(&mut self, mode: &AddressingMode) -> u16 {
166-
match mode {
173+
/// returns (u16: operand addresss, bool: is_page_crossed)
174+
fn get_operand_address2(&mut self, mode: &AddressingMode, tick_on_page_cross: bool) -> u16 {
175+
let mut page_was_crossed = false;
176+
let addr = match mode {
167177
AddressingMode::Immediate => self.pc,
168178
AddressingMode::ZeroPage => self.mem_read(self.pc) as u16,
169179
AddressingMode::ZeroPageX => self.mem_read(self.pc).wrapping_add(self.x) as u16,
170180
AddressingMode::ZeroPageY => self.mem_read(self.pc).wrapping_add(self.y) as u16,
171181
AddressingMode::Absolute => self.mem_read_u16(self.pc),
172-
AddressingMode::AbsoluteX => self.mem_read_u16(self.pc).wrapping_add(self.x as u16),
173-
AddressingMode::AbsoluteY => self.mem_read_u16(self.pc).wrapping_add(self.y as u16),
174-
AddressingMode::Indirect => {
182+
AddressingMode::AbsoluteX => {
183+
let base = self.mem_read_u16(self.pc);
184+
let result = base.wrapping_add(self.x as u16);
185+
186+
if is_page_cross(base, result) {
187+
page_was_crossed = true;
188+
}
189+
190+
result
191+
}
192+
AddressingMode::AbsoluteY => {
175193
let base = self.mem_read_u16(self.pc);
194+
let result = base.wrapping_add(self.y as u16);
176195

177-
let is_edge_of_page = base & 0x00ff == 0xff;
196+
if is_page_cross(base, result) {
197+
page_was_crossed = true;
198+
}
199+
200+
result
201+
}
202+
AddressingMode::Indirect => {
203+
let base = self.mem_read_u16(self.pc);
178204

179-
if is_edge_of_page {
205+
if is_addr_at_page_edge(base) {
180206
let first = self.mem_read(base);
181207
// Intentionally wrap incorrectly, to recreate buggy behavior from original 6502
182208
let second = self.mem_read(base & 0xff00);
@@ -189,16 +215,38 @@ impl Cpu {
189215
// "Indexed indirect"
190216
let base = self.mem_read(self.pc);
191217
let target = base.wrapping_add(self.x); // indexed
192-
self.mem_read_zero_page_wrapping(target) // indirect
218+
let (result, _) = self.mem_read_zero_page_wrapping(target); // indirect
219+
220+
if is_addr_at_page_edge(result) {
221+
page_was_crossed = true;
222+
}
223+
224+
result
193225
}
194226
AddressingMode::IndirectY => {
195227
// "Indirect indexed"
196228
let base = self.mem_read(self.pc);
197-
let target = self.mem_read_zero_page_wrapping(base); // indirect
198-
target.wrapping_add(self.y as u16) // indexed
229+
let (target, _) = self.mem_read_zero_page_wrapping(base); // indirect
230+
let result = target.wrapping_add(self.y as u16); // indexed
231+
232+
if is_addr_at_page_edge(base as u16) || is_addr_at_page_edge(target) {
233+
page_was_crossed = true;
234+
}
235+
236+
result
199237
}
200238
_ => panic!("mode {:?} is not supported", mode),
239+
};
240+
241+
if tick_on_page_cross && page_was_crossed {
242+
self.tick(1)
201243
}
244+
245+
addr
246+
}
247+
248+
fn get_operand_address(&mut self, mode: &AddressingMode) -> u16 {
249+
self.get_operand_address2(mode, false)
202250
}
203251

204252
//
@@ -244,6 +292,9 @@ impl Cpu {
244292
where
245293
F: FnMut(&mut Cpu),
246294
{
295+
// This simulates the wait time for the PPU to start
296+
self.tick(7);
297+
247298
loop {
248299
if self.bus.poll_nmi_status() {
249300
self.interrupt_nmi();
@@ -286,7 +337,7 @@ impl Cpu {
286337
OpName::LDX => self.ldx(&mode),
287338
OpName::LDY => self.ldy(&mode),
288339
OpName::LSR => self.lsr(&mode),
289-
OpName::NOP => self.nop(),
340+
OpName::NOP => self.nop(&mode, cycles.1),
290341
OpName::ORA => self.ora(&mode),
291342
OpName::ROL => self.rol(&mode),
292343
OpName::ROR => self.ror(&mode),
@@ -347,12 +398,7 @@ impl Cpu {
347398
OpName::RRA => self.rra(&mode),
348399
}
349400

350-
// TODO: Handle extra cycles behavior
351-
// (1) page wrapping behavior for some ops that adds +1 to the number of cycles
352-
// Memory page size is 256 bytes. For example, the range [0x0000 .. 0x00FF]- belongs to page 0, [0x0100 .. 0x01FF] belongs to page 1, etc.
353-
// It's enough to compare the upper byte of the addresses to see if they are on the same page.
354-
// (2) branching behavior that adds +1 or +2 to cycles
355-
self.bus.tick(cycles.0 as usize);
401+
self.tick(cycles.0 as usize);
356402

357403
// some operations modify the pc, like JMP. We shouldn't override that.
358404
if self.pc == saved_pc {
@@ -361,6 +407,11 @@ impl Cpu {
361407
}
362408
}
363409

410+
fn tick(&mut self, cycles: usize) {
411+
self.cycles += cycles;
412+
self.bus.tick(cycles);
413+
}
414+
364415
fn jsr(&mut self, mode: &AddressingMode) {
365416
let jump_dest = self.get_operand_address(mode);
366417
// +2 for consumed u16 destination
@@ -446,7 +497,18 @@ impl Cpu {
446497
let displacement = self.mem_read(self.pc) as i8;
447498

448499
if self.get_flag(flag) == is_set {
449-
self.pc = (self.pc as isize + 1 + displacement as isize) as u16
500+
let before = self.pc;
501+
let after = (self.pc as isize + 1 + displacement as isize) as u16;
502+
self.pc = after;
503+
504+
// We set `before+2` so that we only check if we've moved to a DIFFERENT page given the branching scenario.
505+
let before_plus = before + 2;
506+
// This behavior is based on nestest.log line 1107
507+
if is_page_cross(before_plus, after) {
508+
self.tick(2);
509+
} else {
510+
self.tick(1);
511+
}
450512
}
451513
}
452514

@@ -598,7 +660,7 @@ impl Cpu {
598660

599661
/// LDA (LoaD Accumulator)
600662
fn lda(&mut self, mode: &AddressingMode) {
601-
let addr = self.get_operand_address(mode);
663+
let addr = self.get_operand_address2(mode, true);
602664
let param = self.mem_read(addr);
603665

604666
self.a = param;
@@ -607,7 +669,7 @@ impl Cpu {
607669

608670
/// LDX (LoaD X register)
609671
fn ldx(&mut self, mode: &AddressingMode) {
610-
let addr = self.get_operand_address(mode);
672+
let addr = self.get_operand_address2(mode, true);
611673
let param = self.mem_read(addr);
612674

613675
self.x = param;
@@ -616,7 +678,7 @@ impl Cpu {
616678

617679
/// LDY (LoaD Y register)
618680
fn ldy(&mut self, mode: &AddressingMode) {
619-
let addr = self.get_operand_address(mode);
681+
let addr = self.get_operand_address2(mode, true);
620682
let param = self.mem_read(addr);
621683

622684
self.y = param;
@@ -646,7 +708,12 @@ impl Cpu {
646708
}
647709

648710
/// NOP (No OPeration)
649-
fn nop(&mut self) {}
711+
fn nop(&mut self, mode: &AddressingMode, tick_on_page_cross: bool) {
712+
if tick_on_page_cross {
713+
// Run this for the side-effect of possibly ticking one cycle
714+
self.get_operand_address2(mode, tick_on_page_cross);
715+
}
716+
}
650717

651718
/// ORA (bitwise OR with Accumulator)
652719
fn ora(&mut self, mode: &AddressingMode) {
@@ -847,6 +914,9 @@ impl Cpu {
847914
pub fn trace(&mut self) -> String {
848915
// C000 4C F5 C5 JMP $C5F5 A:00 X:00 Y:00 P:24 SP:FD PPU: 0, 21 CYC:7
849916

917+
let cpu_cycles = self.cycles;
918+
let (ppu_frame, ppu_cycles) = self.bus.get_ppu_tick_status();
919+
850920
let code = self.mem_read(self.pc);
851921
let param1 = self.mem_read(self.pc + 1);
852922
let param2 = self.mem_read(self.pc + 2);
@@ -904,7 +974,7 @@ impl Cpu {
904974
}
905975
AddressingMode::IndirectX => {
906976
let indexed = param1.wrapping_add(self.x);
907-
let indirect = self.mem_read_zero_page_wrapping(indexed);
977+
let (indirect, _) = self.mem_read_zero_page_wrapping(indexed);
908978
let data = self.mem_read(indirect);
909979

910980
format!(
@@ -913,7 +983,7 @@ impl Cpu {
913983
)
914984
}
915985
AddressingMode::IndirectY => {
916-
let indirect = self.mem_read_zero_page_wrapping(param1);
986+
let (indirect, _) = self.mem_read_zero_page_wrapping(param1);
917987
let indexed = indirect.wrapping_add(self.y as u16);
918988
let data = self.mem_read(indexed);
919989
format!(
@@ -952,7 +1022,7 @@ impl Cpu {
9521022
};
9531023

9541024
format!(
955-
"{:04X} {:02X} {} {} {:>4} {:<28}A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X}", // PPU:{:>3},{:>3} CYC:{:>4}
1025+
"{:04X} {:02X} {} {} {:>4} {:<28}A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X} PPU:{:>3},{:>3} CYC:{}",
9561026
self.pc,
9571027
code,
9581028
if size >= 2 {
@@ -972,9 +1042,9 @@ impl Cpu {
9721042
self.y,
9731043
self.status,
9741044
self.sp,
975-
// 0,
976-
// 0,
977-
// 0
1045+
ppu_frame,
1046+
ppu_cycles % 341,
1047+
cpu_cycles,
9781048
)
9791049
.to_string()
9801050
}
@@ -1673,15 +1743,15 @@ mod tests {
16731743
result.push(cpu.trace());
16741744
});
16751745
assert_eq!(
1676-
"0064 A2 01 LDX #$01 A:01 X:02 Y:03 P:24 SP:FD",
1746+
"0064 A2 01 LDX #$01 A:01 X:02 Y:03 P:24 SP:FD PPU: 0, 21 CYC:7",
16771747
result[0]
16781748
);
16791749
assert_eq!(
1680-
"0066 CA DEX A:01 X:01 Y:03 P:24 SP:FD",
1750+
"0066 CA DEX A:01 X:01 Y:03 P:24 SP:FD PPU: 0, 27 CYC:9",
16811751
result[1]
16821752
);
16831753
assert_eq!(
1684-
"0067 88 DEY A:01 X:00 Y:03 P:26 SP:FD",
1754+
"0067 88 DEY A:01 X:00 Y:03 P:26 SP:FD PPU: 0, 33 CYC:11",
16851755
result[2]
16861756
);
16871757
}
@@ -1710,7 +1780,7 @@ mod tests {
17101780
result.push(cpu.trace());
17111781
});
17121782
assert_eq!(
1713-
"0064 11 33 ORA ($33),Y = 0400 @ 0400 = AA A:00 X:00 Y:00 P:24 SP:FD",
1783+
"0064 11 33 ORA ($33),Y = 0400 @ 0400 = AA A:00 X:00 Y:00 P:24 SP:FD PPU: 0, 21 CYC:7",
17141784
result[0]
17151785
);
17161786
}
@@ -1737,7 +1807,7 @@ mod tests {
17371807
})
17381808
});
17391809

1740-
let expected = fs::read("nestest_no_cycles.log").unwrap();
1810+
let expected = fs::read("nestest.log").unwrap();
17411811

17421812
let actual = match mutex_result.lock() {
17431813
Ok(m) => m,

src/ppu.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ impl Ppu {
6868
pub fn tick(&mut self, cycles: usize) {
6969
self.clock_cycles += cycles;
7070
// each scanline lasts for 341 PPU clock cycles
71-
let scanline = cycles / 341;
71+
let scanline = self.clock_cycles / 341;
7272
if self.scanline < 241
7373
&& scanline >= 241
7474
&& self
@@ -93,6 +93,11 @@ impl Ppu {
9393
self.scanline = scanline % 262;
9494
}
9595

96+
/// returns (scanline, clock_cycles)
97+
pub fn get_tick_status(&self) -> (usize, usize) {
98+
(self.scanline, self.clock_cycles)
99+
}
100+
96101
#[allow(dead_code)]
97102
pub fn is_nmi_interrupt_triggered(&self) -> bool {
98103
self.nmi_interrupt

src/utility.rs

+8
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@ pub fn split_addr(addr: u16) -> (u8, u8) {
77
let hi = (addr >> 8) as u8;
88
(lo, hi)
99
}
10+
11+
pub fn is_addr_at_page_edge(addr: u16) -> bool {
12+
addr & 0x00ff == 0xff
13+
}
14+
15+
pub fn is_page_cross(addr1: u16, addr2: u16) -> bool {
16+
(addr1 & 0xFF00) != (addr2 & 0xFF00)
17+
}

0 commit comments

Comments
 (0)