1use crate::addressing::AddressingMode;
7use crate::bus::Bus;
8use crate::cpu::Cpu;
9use crate::opcodes::OPCODE_TABLE;
10use std::fmt::Write;
11
12#[derive(Debug, Clone)]
14pub struct TraceEntry {
15 pub pc: u16,
17 pub opcode: u8,
19 pub operand_bytes: Vec<u8>,
21 pub disassembly: String,
23 pub a: u8,
25 pub x: u8,
27 pub y: u8,
29 pub p: u8,
31 pub sp: u8,
33 pub cycles: u64,
35}
36
37impl TraceEntry {
38 pub fn format(&self) -> String {
42 let mut bytes_str = String::new();
44 let opcode = self.opcode;
45 let _ = write!(bytes_str, "{opcode:02X}");
47 for byte in &self.operand_bytes {
48 let _ = write!(bytes_str, " {byte:02X}");
49 }
50
51 let bytes_width = if self.disassembly.starts_with('*') {
55 9
56 } else {
57 10
58 };
59 let bytes_field = format!("{bytes_str:<bytes_width$}");
60
61 let disasm_width = if self.disassembly.starts_with('*') {
63 33
64 } else {
65 32
66 };
67 let disasm_field = format!("{:<width$}", self.disassembly, width = disasm_width);
68
69 format!(
70 "{:04X} {}{}A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X} CYC:{}",
71 self.pc,
72 bytes_field,
73 disasm_field,
74 self.a,
75 self.x,
76 self.y,
77 self.p,
78 self.sp,
79 self.cycles
80 )
81 }
82}
83
84pub struct CpuTracer {
86 entries: Vec<String>,
87}
88
89impl CpuTracer {
90 pub fn new() -> Self {
92 Self {
93 entries: Vec::new(),
94 }
95 }
96
97 pub fn trace(&mut self, cpu: &Cpu, bus: &mut impl Bus) {
102 let entry = self.create_trace_entry(cpu, bus);
103 self.entries.push(entry.format());
104 }
105
106 pub fn get_log(&self) -> String {
108 self.entries.join("\n")
109 }
110
111 pub fn len(&self) -> usize {
113 self.entries.len()
114 }
115
116 pub fn is_empty(&self) -> bool {
118 self.entries.is_empty()
119 }
120
121 fn create_trace_entry(&self, cpu: &Cpu, bus: &mut impl Bus) -> TraceEntry {
123 let pc = cpu.pc;
124 let opcode = bus.read(pc);
125 let opcode_info = &OPCODE_TABLE[opcode as usize];
126
127 let operand_bytes = self.fetch_operand_bytes(pc, opcode_info.addr_mode, bus);
129
130 let disassembly = self.disassemble(cpu, bus, pc, opcode, opcode_info);
132
133 TraceEntry {
134 pc,
135 opcode,
136 operand_bytes,
137 disassembly,
138 a: cpu.a,
139 x: cpu.x,
140 y: cpu.y,
141 p: cpu.status.bits(),
142 sp: cpu.sp,
143 cycles: cpu.cycles,
144 }
145 }
146
147 fn fetch_operand_bytes(
149 &self,
150 pc: u16,
151 addr_mode: AddressingMode,
152 bus: &mut impl Bus,
153 ) -> Vec<u8> {
154 let num_bytes = addr_mode.operand_bytes();
155 (1..=num_bytes)
156 .map(|i| bus.read(pc.wrapping_add(i as u16)))
157 .collect()
158 }
159
160 #[allow(clippy::too_many_lines)]
162 fn disassemble(
163 &self,
164 cpu: &Cpu,
165 bus: &mut impl Bus,
166 pc: u16,
167 _opcode: u8,
168 opcode_info: &crate::opcodes::OpcodeInfo,
169 ) -> String {
170 let mnemonic = opcode_info.mnemonic;
171 let addr_mode = opcode_info.addr_mode;
172 let prefix = if opcode_info.unofficial { "*" } else { "" };
173
174 match addr_mode {
175 AddressingMode::Implied => format!("{prefix}{mnemonic}"),
176
177 AddressingMode::Accumulator => format!("{prefix}{mnemonic} A"),
178
179 AddressingMode::Immediate => {
180 let value = bus.read(pc.wrapping_add(1));
181 format!("{prefix}{mnemonic} #${value:02X}")
182 }
183
184 AddressingMode::ZeroPage => {
185 let addr = bus.read(pc.wrapping_add(1));
186 let value = bus.read(addr as u16);
187 format!("{prefix}{mnemonic} ${addr:02X} = {value:02X}")
188 }
189
190 AddressingMode::ZeroPageX => {
191 let base = bus.read(pc.wrapping_add(1));
192 let addr = base.wrapping_add(cpu.x);
193 let value = bus.read(addr as u16);
194 format!("{prefix}{mnemonic} ${base:02X},X @ {addr:02X} = {value:02X}")
195 }
196
197 AddressingMode::ZeroPageY => {
198 let base = bus.read(pc.wrapping_add(1));
199 let addr = base.wrapping_add(cpu.y);
200 let value = bus.read(addr as u16);
201 format!("{prefix}{mnemonic} ${base:02X},Y @ {addr:02X} = {value:02X}")
202 }
203
204 AddressingMode::Absolute => {
205 let lo = bus.read(pc.wrapping_add(1));
206 let hi = bus.read(pc.wrapping_add(2));
207 let addr = u16::from_le_bytes([lo, hi]);
208
209 if mnemonic == "JMP" || mnemonic == "JSR" {
211 format!("{prefix}{mnemonic} ${addr:04X}")
212 } else {
213 let value = bus.read(addr);
214 format!("{prefix}{mnemonic} ${addr:04X} = {value:02X}")
215 }
216 }
217
218 AddressingMode::AbsoluteX => {
219 let lo = bus.read(pc.wrapping_add(1));
220 let hi = bus.read(pc.wrapping_add(2));
221 let base = u16::from_le_bytes([lo, hi]);
222 let addr = base.wrapping_add(cpu.x as u16);
223 let value = bus.read(addr);
224 format!("{prefix}{mnemonic} ${base:04X},X @ {addr:04X} = {value:02X}")
225 }
226
227 AddressingMode::AbsoluteY => {
228 let lo = bus.read(pc.wrapping_add(1));
229 let hi = bus.read(pc.wrapping_add(2));
230 let base = u16::from_le_bytes([lo, hi]);
231 let addr = base.wrapping_add(cpu.y as u16);
232 let value = bus.read(addr);
233 format!("{prefix}{mnemonic} ${base:04X},Y @ {addr:04X} = {value:02X}")
234 }
235
236 AddressingMode::Indirect => {
237 let lo = bus.read(pc.wrapping_add(1));
238 let hi = bus.read(pc.wrapping_add(2));
239 let ptr = u16::from_le_bytes([lo, hi]);
240
241 let target_lo = bus.read(ptr) as u16;
243 let target_hi = if (ptr & 0x00FF) == 0x00FF {
244 bus.read(ptr & 0xFF00) as u16
246 } else {
247 bus.read(ptr.wrapping_add(1)) as u16
248 };
249 let target = (target_hi << 8) | target_lo;
250
251 format!("{prefix}{mnemonic} (${ptr:04X}) = {target:04X}")
252 }
253
254 AddressingMode::IndexedIndirectX => {
255 let base = bus.read(pc.wrapping_add(1));
256 let ptr = base.wrapping_add(cpu.x);
257
258 let lo = bus.read(ptr as u16) as u16;
259 let hi = bus.read(ptr.wrapping_add(1) as u16) as u16;
260 let addr = (hi << 8) | lo;
261 let value = bus.read(addr);
262
263 format!("{prefix}{mnemonic} (${base:02X},X) @ {ptr:02X} = {addr:04X} = {value:02X}")
264 }
265
266 AddressingMode::IndirectIndexedY => {
267 let ptr = bus.read(pc.wrapping_add(1));
268
269 let lo = bus.read(ptr as u16) as u16;
270 let hi = bus.read(ptr.wrapping_add(1) as u16) as u16;
271 let base = (hi << 8) | lo;
272
273 let addr = base.wrapping_add(cpu.y as u16);
274 let value = bus.read(addr);
275
276 format!("{prefix}{mnemonic} (${ptr:02X}),Y = {base:04X} @ {addr:04X} = {value:02X}")
277 }
278
279 AddressingMode::Relative => {
280 let offset = bus.read(pc.wrapping_add(1)) as i8;
281 let target = pc.wrapping_add(2).wrapping_add(offset as u16);
282 format!("{prefix}{mnemonic} ${target:04X}")
283 }
284 }
285 }
286}
287
288impl Default for CpuTracer {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use crate::status::StatusFlags;
298
299 struct TestBus {
300 memory: Vec<u8>,
301 }
302
303 impl TestBus {
304 fn new() -> Self {
305 Self {
306 memory: vec![0; 0x10000],
307 }
308 }
309 }
310
311 impl Bus for TestBus {
312 fn read(&mut self, addr: u16) -> u8 {
313 self.memory[addr as usize]
314 }
315
316 fn write(&mut self, addr: u16, value: u8) {
317 self.memory[addr as usize] = value;
318 }
319 }
320
321 #[test]
322 fn test_trace_lda_immediate() {
323 let mut cpu = Cpu::new();
324 let mut bus = TestBus::new();
325 let mut tracer = CpuTracer::new();
326
327 cpu.pc = 0xC000;
328 cpu.cycles = 7;
329 cpu.a = 0x00;
330 cpu.x = 0x00;
331 cpu.y = 0x00;
332 cpu.sp = 0xFD;
333 cpu.status = StatusFlags::from_bits_truncate(0x24);
334
335 bus.memory[0xC000] = 0xA9;
337 bus.memory[0xC001] = 0x42;
338
339 tracer.trace(&cpu, &mut bus);
340 let log = tracer.get_log();
341
342 assert!(log.contains("C000"));
343 assert!(log.contains("A9 42"));
344 assert!(log.contains("LDA #$42"));
345 assert!(log.contains("A:00 X:00 Y:00 P:24 SP:FD"));
346 assert!(log.contains("CYC:7"));
347 }
348
349 #[test]
350 fn test_trace_jmp_absolute() {
351 let mut cpu = Cpu::new();
352 let mut bus = TestBus::new();
353 let mut tracer = CpuTracer::new();
354
355 cpu.pc = 0xC000;
356 cpu.cycles = 7;
357 cpu.status = StatusFlags::from_bits_truncate(0x24);
358 cpu.sp = 0xFD;
359
360 bus.memory[0xC000] = 0x4C;
362 bus.memory[0xC001] = 0xF5;
363 bus.memory[0xC002] = 0xC5;
364
365 tracer.trace(&cpu, &mut bus);
366 let log = tracer.get_log();
367
368 assert!(log.contains("C000"));
369 assert!(log.contains("4C F5 C5"));
370 assert!(log.contains("JMP $C5F5"));
371 }
372}