use crate::gb::cpu::opcode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GbCpuDisasmLineSnapshot {
pub addr: u16,
pub bytes: Vec<u8>,
pub text: String,
pub is_current: bool,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct GbCpuDisasmWindowState {
pub(super) start: Option<u16>,
}
#[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 format_instruction(opcode: u8, pc: u16, bytes: &[u8]) -> String {
let (meta, is_cb) = if bytes.len() >= 2 && bytes[0] == 0xCB {
(opcode::lookup_cb(opcode), true)
} else {
(opcode::lookup(opcode), false)
};
let mnemonic = meta.mnemonic;
resolve_operands(mnemonic, pc, bytes, is_cb)
}
pub fn format_disasm_bytes(bytes: &[u8]) -> String {
match bytes.len() {
0 => String::from(" "),
1 => format!("{:02X} ", bytes[0]),
2 => format!("{:02X} {:02X} ", bytes[0], bytes[1]),
_ => format!("{:02X} {:02X} {:02X} ", bytes[0], bytes[1], bytes[2]),
}
}
fn resolve_operands(mnemonic: &str, pc: u16, bytes: &[u8], is_cb: bool) -> String {
if !mnemonic.contains("n8") && !mnemonic.contains("n16") && !mnemonic.contains("e8") {
return mnemonic.to_string();
}
let mut result = mnemonic.to_string();
let operand_start = if is_cb { 2 } else { 1 };
if result.contains("n8")
&& let Some(&byte) = bytes.get(operand_start)
{
result = result.replace("n8", &format!("${:02X}", byte));
}
if result.contains("n16") && bytes.len() >= operand_start + 2 {
let lo = bytes[operand_start];
let hi = bytes[operand_start + 1];
let addr = u16::from_le_bytes([lo, hi]);
result = result.replace("n16", &format!("${:04X}", addr));
}
if result.contains("e8")
&& let Some(&offset_byte) = bytes.get(operand_start)
{
let offset = offset_byte as i8;
let target = pc.wrapping_add(2).wrapping_add(offset as i16 as u16);
result = result.replace("e8", &format!("${:04X}", target));
}
result
}
pub fn disassemble_window<F: Fn(u16) -> u8>(
read: F,
pc: u16,
config: DisasmWindowConfig,
) -> Vec<GbCpuDisasmLineSnapshot> {
let mut start = pc;
for _ in 0..config.before {
let Some(prev) = prev_instruction_start(&read, start) else {
break;
};
if prev > start {
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 GbCpuDisasmWindowState,
config: DisasmWindowConfig,
) -> Vec<GbCpuDisasmLineSnapshot> {
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<GbCpuDisasmLineSnapshot> {
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);
if op == 0xCB && len >= 2 {
if len == 2 {
return Some(start);
}
} else {
let meta = opcode::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) -> GbCpuDisasmLineSnapshot {
let op = read(addr);
let (len, actual_op) = if op == 0xCB {
let cb_op = read(addr.wrapping_add(1));
(2, cb_op)
} else {
let meta = opcode::lookup(op);
(meta.bytes() as usize, op)
};
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(actual_op, addr, &bytes);
GbCpuDisasmLineSnapshot {
addr,
bytes,
text,
is_current: addr == pc,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_disasm_bytes_one_byte() {
let bytes = vec![0x3E];
assert_eq!(format_disasm_bytes(&bytes), "3E ");
}
#[test]
fn test_format_disasm_bytes_two_bytes() {
let bytes = vec![0x3E, 0x50];
assert_eq!(format_disasm_bytes(&bytes), "3E 50 ");
}
#[test]
fn test_format_disasm_bytes_three_bytes() {
let bytes = vec![0xC3, 0x00, 0x01];
assert_eq!(format_disasm_bytes(&bytes), "C3 00 01 ");
}
#[test]
fn test_format_instruction_ld_a_n8() {
let opcode = 0x3E;
let pc = 0x0100;
let bytes = vec![0x3E, 0x50];
let result = format_instruction(opcode, pc, &bytes);
assert_eq!(result, "LD A,$50");
}
#[test]
fn test_format_instruction_jp_n16() {
let opcode = 0xC3;
let pc = 0x0100;
let bytes = vec![0xC3, 0x00, 0x01];
let result = format_instruction(opcode, pc, &bytes);
assert_eq!(result, "JP $0100");
}
#[test]
fn test_format_instruction_jr_e8() {
let opcode = 0x18;
let pc = 0x0100;
let bytes = vec![0x18, 0x0A]; let result = format_instruction(opcode, pc, &bytes);
assert_eq!(result, "JR $010C");
}
#[test]
fn test_format_instruction_jr_e8_negative() {
let opcode = 0x18;
let pc = 0x0100;
let bytes = vec![0x18, 0xFB]; let result = format_instruction(opcode, pc, &bytes);
assert_eq!(result, "JR $00FD");
}
#[test]
fn test_format_instruction_nop() {
let opcode = 0x00;
let pc = 0x0100;
let bytes = vec![0x00];
let result = format_instruction(opcode, pc, &bytes);
assert_eq!(result, "NOP");
}
#[test]
fn test_format_instruction_ld_bc_n16() {
let opcode = 0x01;
let pc = 0x0100;
let bytes = vec![0x01, 0x34, 0x12]; let result = format_instruction(opcode, pc, &bytes);
assert_eq!(result, "LD BC,$1234");
}
#[test]
fn test_format_instruction_cb_prefix() {
let opcode = 0x00; let pc = 0x0100;
let bytes = vec![0xCB, 0x00];
let result = format_instruction(opcode, pc, &bytes);
assert_eq!(result, "RLC B");
}
#[test]
fn test_format_instruction_ldh_n8_a() {
let opcode = 0xE0;
let pc = 0x0100;
let bytes = vec![0xE0, 0x90];
let result = format_instruction(opcode, pc, &bytes);
assert_eq!(result, "LDH ($90),A");
}
}