rustynes_cpu/
trace.rs

1//! CPU trace logging for nestest.log-compatible output.
2//!
3//! This module provides functionality to generate execution traces matching
4//! the nestest golden log format, essential for CPU validation.
5
6use crate::addressing::AddressingMode;
7use crate::bus::Bus;
8use crate::cpu::Cpu;
9use crate::opcodes::OPCODE_TABLE;
10use std::fmt::Write;
11
12/// Trace entry representing a single instruction execution.
13#[derive(Debug, Clone)]
14pub struct TraceEntry {
15    /// Program counter
16    pub pc: u16,
17    /// Opcode byte
18    pub opcode: u8,
19    /// Operand bytes (0-2 bytes)
20    pub operand_bytes: Vec<u8>,
21    /// Disassembled instruction string
22    pub disassembly: String,
23    /// Accumulator register
24    pub a: u8,
25    /// X register
26    pub x: u8,
27    /// Y register
28    pub y: u8,
29    /// Status register
30    pub p: u8,
31    /// Stack pointer
32    pub sp: u8,
33    /// Total CPU cycles
34    pub cycles: u64,
35}
36
37impl TraceEntry {
38    /// Format the trace entry in nestest.log format.
39    ///
40    /// Format: PC  OPCODE_BYTES  DISASM    A:XX X:XX Y:XX P:XX SP:XX CYC:XXXXX
41    pub fn format(&self) -> String {
42        // Format opcode bytes
43        let mut bytes_str = String::new();
44        let opcode = self.opcode;
45        // Writing to a String never fails
46        let _ = write!(bytes_str, "{opcode:02X}");
47        for byte in &self.operand_bytes {
48            let _ = write!(bytes_str, " {byte:02X}");
49        }
50
51        // Unofficial opcodes have the * prefix "steal" one space from bytes field
52        // Official: bytes=10 chars, disasm=32 chars
53        // Unofficial: bytes=9 chars, disasm=33 chars (starts with *)
54        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        // Disassembly field is always formatted to fit (32-33 chars)
62        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
84/// CPU trace logger for generating nestest-compatible logs.
85pub struct CpuTracer {
86    entries: Vec<String>,
87}
88
89impl CpuTracer {
90    /// Create a new CPU tracer.
91    pub fn new() -> Self {
92        Self {
93            entries: Vec::new(),
94        }
95    }
96
97    /// Log the current CPU state before executing the instruction.
98    ///
99    /// IMPORTANT: This must be called BEFORE the instruction executes,
100    /// as the log shows the state at the start of the instruction.
101    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    /// Get all logged entries as a single string.
107    pub fn get_log(&self) -> String {
108        self.entries.join("\n")
109    }
110
111    /// Get the number of logged entries.
112    pub fn len(&self) -> usize {
113        self.entries.len()
114    }
115
116    /// Check if the log is empty.
117    pub fn is_empty(&self) -> bool {
118        self.entries.is_empty()
119    }
120
121    /// Create a trace entry for the current CPU state.
122    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        // Fetch operand bytes
128        let operand_bytes = self.fetch_operand_bytes(pc, opcode_info.addr_mode, bus);
129
130        // Generate disassembly
131        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    /// Fetch operand bytes for the instruction.
148    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    /// Disassemble the instruction at PC.
161    #[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                // Special handling for JMP and JSR (no value read)
210                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                // Read the target address (with page-wrap bug)
242                let target_lo = bus.read(ptr) as u16;
243                let target_hi = if (ptr & 0x00FF) == 0x00FF {
244                    // Page boundary bug: read from same page
245                    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        // LDA #$42
336        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        // JMP $C5F5
361        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}