use crate::nes::cpu;
use crate::nes::cpu::opcode::AddrMode;
use super::types::{CpuDisasmLineSnapshot, CpuDisasmWindowState};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DisasmWindowConfig {
pub before: usize,
pub after: usize,
pub top_margin: usize,
pub bottom_margin: usize,
}
impl Default for DisasmWindowConfig {
fn default() -> Self {
Self {
before: 10,
after: 9,
top_margin: 3,
bottom_margin: 3,
}
}
}
pub fn disassemble_window<F: Fn(u16) -> u8>(
read: F,
pc: u16,
config: DisasmWindowConfig,
) -> Vec<CpuDisasmLineSnapshot> {
let mut start = pc;
for _ in 0..config.before {
let Some(prev) = prev_instruction_start(&read, start) else {
break;
};
start = prev;
}
let target_lines = config.before + 1 + config.after;
disassemble_from_start(&read, start, pc, target_lines)
}
pub fn disassemble_window_with_state<F: Fn(u16) -> u8>(
read: F,
pc: u16,
state: &mut CpuDisasmWindowState,
config: DisasmWindowConfig,
) -> Vec<CpuDisasmLineSnapshot> {
let target_lines = config.before + 1 + config.after;
let mut lines = if let Some(start) = state.start {
disassemble_from_start(&read, start, pc, target_lines)
} else {
disassemble_window(&read, pc, config)
};
let current_index = lines.iter().position(|l| l.is_current);
if let Some(idx) = current_index {
let last_two_start = lines.len().saturating_sub(2);
if idx >= last_two_start {
lines = disassemble_window(&read, pc, config);
state.start = lines.first().map(|l| l.addr);
return lines;
}
state.start = lines.first().map(|l| l.addr);
return lines;
}
lines = disassemble_window(&read, pc, config);
state.start = lines.first().map(|l| l.addr);
lines
}
fn disassemble_from_start<F: Fn(u16) -> u8>(
read: &F,
start: u16,
pc: u16,
target_lines: usize,
) -> Vec<CpuDisasmLineSnapshot> {
let mut lines = Vec::with_capacity(target_lines);
let mut addr = start;
for _ in 0..target_lines {
let line = disassemble_one(read, addr, pc);
let step = (line.bytes.len() as u16).max(1);
addr = addr.wrapping_add(step);
lines.push(line);
if addr == 0 {
break;
}
}
lines
}
fn prev_instruction_start<F: Fn(u16) -> u8>(read: &F, pc: u16) -> Option<u16> {
for len in (1u16..=3u16).rev() {
let start = pc.wrapping_sub(len);
let op = read(start);
let meta = cpu::lookup(op);
if meta.bytes() as u16 == len {
return Some(start);
}
}
None
}
fn disassemble_one<F: Fn(u16) -> u8>(read: &F, addr: u16, pc: u16) -> CpuDisasmLineSnapshot {
let op = read(addr);
let meta = cpu::lookup(op);
let len = meta.bytes() as usize;
let mut bytes = Vec::with_capacity(len);
for i in 0..len {
bytes.push(read(addr.wrapping_add(i as u16)));
}
let text = format_instruction(meta, addr, &bytes);
CpuDisasmLineSnapshot {
addr,
bytes,
text,
is_current: addr == pc,
}
}
fn format_instruction(meta: &cpu::OpCode, addr: u16, bytes: &[u8]) -> String {
let operand = match meta.mode {
AddrMode::IMP => String::new(),
AddrMode::ACC => "A".to_string(),
AddrMode::IMM => format!("#${:02X}", bytes.get(1).copied().unwrap_or(0)),
AddrMode::ZP => format!("${:02X}", bytes.get(1).copied().unwrap_or(0)),
AddrMode::ZPX => format!("${:02X},X", bytes.get(1).copied().unwrap_or(0)),
AddrMode::ZPY => format!("${:02X},Y", bytes.get(1).copied().unwrap_or(0)),
AddrMode::INDX => format!("(${:02X},X)", bytes.get(1).copied().unwrap_or(0)),
AddrMode::INDY | AddrMode::INDYW => {
format!("(${:02X}),Y", bytes.get(1).copied().unwrap_or(0))
}
AddrMode::REL => {
let off = bytes.get(1).copied().unwrap_or(0) as i8;
let next = addr.wrapping_add(2);
let target = next.wrapping_add(off as i16 as u16);
format!("${:04X}", target)
}
AddrMode::ABS => {
let lo = bytes.get(1).copied().unwrap_or(0);
let hi = bytes.get(2).copied().unwrap_or(0);
format!("${:04X}", u16::from_le_bytes([lo, hi]))
}
AddrMode::ABSX | AddrMode::ABSXW => {
let lo = bytes.get(1).copied().unwrap_or(0);
let hi = bytes.get(2).copied().unwrap_or(0);
format!("${:04X},X", u16::from_le_bytes([lo, hi]))
}
AddrMode::ABSY | AddrMode::ABSYW => {
let lo = bytes.get(1).copied().unwrap_or(0);
let hi = bytes.get(2).copied().unwrap_or(0);
format!("${:04X},Y", u16::from_le_bytes([lo, hi]))
}
AddrMode::IND => {
let lo = bytes.get(1).copied().unwrap_or(0);
let hi = bytes.get(2).copied().unwrap_or(0);
format!("(${:04X})", u16::from_le_bytes([lo, hi]))
}
};
if operand.is_empty() {
meta.mnemonic.to_string()
} else {
format!("{} {}", meta.mnemonic, operand)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_disasm_window_recenters_when_current_reaches_second_last_row() {
let read = |_addr: u16| 0xEA;
let config = DisasmWindowConfig {
before: 4,
after: 4,
top_margin: 3,
bottom_margin: 3,
};
let mut state = CpuDisasmWindowState::default();
let base_pc = 0xC000;
let lines0 = disassemble_window_with_state(read, base_pc, &mut state, config);
let idx0 = lines0.iter().position(|l| l.is_current).unwrap();
assert_eq!(idx0, config.before);
let target_lines = config.before + 1 + config.after;
let second_last_index = target_lines - 2;
let pc_at_second_last = base_pc.wrapping_add((second_last_index - idx0) as u16);
let lines1 = disassemble_window_with_state(read, pc_at_second_last, &mut state, config);
let idx1 = lines1.iter().position(|l| l.is_current).unwrap();
assert_eq!(idx1, config.before);
}
#[test]
fn test_disasm_window_keeps_start_when_current_is_inside_window_and_not_in_last_two_rows() {
let read = |_addr: u16| 0xEA;
let config = DisasmWindowConfig {
before: 8,
after: 8,
top_margin: 3,
bottom_margin: 3,
};
let mut state = CpuDisasmWindowState::default();
let base_pc = 0xC000;
let lines0 = disassemble_window_with_state(read, base_pc, &mut state, config);
let start0 = lines0.first().unwrap().addr;
let lines1 =
disassemble_window_with_state(read, base_pc.wrapping_add(1), &mut state, config);
let idx1 = lines1.iter().position(|l| l.is_current).unwrap();
assert_eq!(idx1, config.before + 1);
assert_eq!(lines1.first().unwrap().addr, start0);
}
#[test]
fn test_disasm_window_recenters_when_current_is_outside_existing_window() {
let read = |_addr: u16| 0xEA;
let config = DisasmWindowConfig {
before: 8,
after: 8,
top_margin: 3,
bottom_margin: 3,
};
let mut state = CpuDisasmWindowState::default();
let base_pc = 0xC000;
let _ = disassemble_window_with_state(read, base_pc, &mut state, config);
let jumped_pc = base_pc.wrapping_add(0x40);
let lines1 = disassemble_window_with_state(read, jumped_pc, &mut state, config);
let idx1 = lines1.iter().position(|l| l.is_current).unwrap();
assert_eq!(idx1, config.before);
}
}