boy: Initial public commit

- SM83 implementation kinda works
- Live disassembly, memory write tracing
- Pretty snazzy debugger with custom memory editor
- hexadecimal calculator with novel operator precedence rules
This commit is contained in:
2024-06-22 07:25:59 -05:00
commit acfbb8f1cf
16 changed files with 4646 additions and 0 deletions

11
boy-debug/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "boy-debug"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rustyline = "14.0.0"
boy = { path = ".." }
boy-utils = { path = "../boy-utils" }

119
boy-debug/src/bus.rs Normal file
View File

@@ -0,0 +1,119 @@
//! A dummied out implementation of a Bus, which is filled entirely with RAM
use std::{borrow::Cow, io::Result as IoResult, path::Path};
use boy::memory::io::BusIO;
pub trait BusIOTools: BusIO {
/// Prints all successful reads and writes
fn trace(&mut self) -> TracingBus<Self>;
fn ascii(&mut self) -> AsciiSerial<Self>;
fn read_file(self, path: impl AsRef<Path>) -> IoResult<Self>
where
Self: Sized;
}
impl<T: BusIO> BusIOTools for T {
fn trace(&mut self) -> TracingBus<Self> {
TracingBus { bus: self }
}
fn ascii(&mut self) -> AsciiSerial<Self> {
AsciiSerial {
data: 0,
buf: vec![],
bus: self,
}
}
fn read_file(mut self, path: impl AsRef<Path>) -> IoResult<Self> {
let data = std::fs::read(path)?;
eprintln!("Read {} bytes.", data.len());
for (addr, data) in data.into_iter().enumerate() {
self.write(addr, data);
}
Ok(self)
}
}
/// A [BusIO] wrapper which prints every read and write operation
#[repr(transparent)]
pub struct TracingBus<'t, T: BusIO + ?Sized> {
bus: &'t mut T,
}
impl<'t, T: BusIO> BusIO for TracingBus<'t, T> {
fn read(&self, addr: usize) -> Option<u8> {
// print!("bus[{addr:04x}] -> ");
// match out {
// Some(v) => println!("{v:02x}"),
// None => println!("None"),
// }
self.bus.read(addr)
}
fn write(&mut self, addr: usize, data: u8) -> Option<()> {
eprintln!("set [{addr:04x}], {data:02x}");
self.bus.write(addr, data)
}
}
impl<'t, T: BusIO> From<&'t mut T> for TracingBus<'t, T> {
fn from(value: &'t mut T) -> Self {
Self { bus: value }
}
}
impl<'t, T: BusIO + AsRef<[u8]>> AsRef<[u8]> for TracingBus<'t, T> {
fn as_ref(&self) -> &[u8] {
self.bus.as_ref()
}
}
impl<'t, T: BusIO + AsMut<[u8]>> AsMut<[u8]> for TracingBus<'t, T> {
fn as_mut(&mut self) -> &mut [u8] {
self.bus.as_mut()
}
}
/// Implements a hacky serial port for extracting data
pub struct AsciiSerial<'t, T: BusIO + ?Sized> {
data: u8,
buf: Vec<u8>,
bus: &'t mut T,
}
impl<'t, T: BusIO + ?Sized> AsciiSerial<'t, T> {
/// Gets the contents of the data buffer
pub fn string(&self) -> Cow<str> {
String::from_utf8_lossy(&self.buf)
}
}
impl<'t, T: BusIO + ?Sized> BusIO for AsciiSerial<'t, T> {
fn read(&self, addr: usize) -> Option<u8> {
match addr {
0xff01 => Some(self.data),
0xff02 => Some(0x7f),
_ => self.bus.read(addr),
}
}
fn write(&mut self, addr: usize, data: u8) -> Option<()> {
match addr {
0xff01 => {
// eprintln!("'{data:02x}'");
self.buf.push(data);
}
0xff02 => {
if data & 0x80 != 0 {
// eprintln!("SERIAL => {:02x}", self.data);
eprintln!("tx: {data:02x}, buf: {:02x?}", self.buf);
let interrupt = self.bus.read(0xff0f)? | (1 << 3);
self.bus.write(0xff0f, interrupt)?;
}
}
_ => {}
}
self.bus.write(addr, data)
}
fn diag(&mut self, param: usize) {
println!("debug: '{}'", self.string());
self.buf.clear();
self.bus.diag(param)
}
}

View File

@@ -0,0 +1,86 @@
//! Wraps the [boy] [Insn]-disassembler and extends it to disassemble entire instructions, rather than just single words
use boy::cpu::disasm::{Insn, Prefixed};
use std::iter::{Enumerate, Peekable};
pub trait Disassemble: Iterator<Item = u8> + Sized {
fn disassemble(self) -> Disassembler<Self>;
}
impl<T: Iterator<Item = u8> + Sized> Disassemble for T {
fn disassemble(self) -> Disassembler<Self> {
Disassembler::new(self)
}
}
/// An iterator over some bytes, which prints the disassembly to stdout
// TODO: parameterize this over a Writer
pub struct Disassembler<I: Iterator<Item = u8>> {
bytes: Peekable<Enumerate<I>>,
}
impl<I: Iterator<Item = u8>> Iterator for Disassembler<I> {
type Item = (usize, String);
fn next(&mut self) -> Option<Self::Item> {
let (index, insn) = self.bytes.next()?;
Some((index, self.print(insn.into())?))
}
}
impl<I: Iterator<Item = u8>> Disassembler<I> {
pub fn new(bytes: I) -> Self {
Disassembler {
bytes: bytes.enumerate().peekable(),
}
}
pub fn index(&mut self) -> Option<usize> {
self.bytes.peek().map(|v| v.0)
}
pub fn imm8(&mut self) -> Option<u8> {
self.bytes.next().map(|v| v.1)
}
pub fn smm8(&mut self) -> Option<i8> {
self.imm8().map(|b| b as i8)
}
pub fn imm16(&mut self) -> Option<u16> {
let low = self.imm8()? as u16;
let high = self.imm8()? as u16;
Some(high << 8 | low)
}
pub fn smm16(&mut self) -> Option<i16> {
self.imm16().map(|w| w as i16)
}
pub fn print(&mut self, insn: Insn) -> Option<String> {
Some(match insn {
Insn::LdImm(reg) => format!("ld\t{reg}, {:02x}", self.imm8()?),
Insn::LdImm16(reg) => format!("ld\t{reg}, {:04x}", self.imm16()?),
Insn::LdAbs => format!("ld\ta, [{:04x}]", self.imm16()?),
Insn::StAbs => format!("ld\t[{:04x}], a", self.imm16()?),
Insn::StSpAbs => format!("ld\t[{:04x}], sp", self.imm16()?),
Insn::LdHlSpRel => format!("ld\thl, sp + {:02x}", self.imm8()?),
Insn::Ldh => format!("ldh\ta, [ff{:02x}]", self.imm8()?),
Insn::Sth => format!("ldh\t[ff{:02x}], a", self.imm8()?),
Insn::Add16SpI => format!("add\tsp, {:02x}", self.imm8()?),
Insn::AddI => format!("add\ta, {:02x}", self.imm8()?),
Insn::AdcI => format!("adc\ta, {:02x}", self.imm8()?),
Insn::SubI => format!("sub\ta, {:02x}", self.imm8()?),
Insn::SbcI => format!("sbc\ta, {:02x}", self.imm8()?),
Insn::AndI => format!("and\ta, {:02x}", self.imm8()?),
Insn::XorI => format!("xor\ta, {:02x}", self.imm8()?),
Insn::OrI => format!("or\ta, {:02x}", self.imm8()?),
Insn::CpI => format!("cp\ta, {:02x}", self.imm8()?),
Insn::Jr => format!("jr\t{0:02x} ({0:02})", self.smm8()?),
Insn::Jrc(cond) => format!("jr\t{cond}, {:02x} ({0:02})", self.smm8()?),
Insn::Jp => format!("jp\t{:04x}", self.imm16()?),
Insn::Jpc(cond) => format!("jp\t{cond}, {:04x}", self.imm16()?),
Insn::Call => format!("call\t{:04x}", self.imm16()?),
Insn::Callc(cond) => format!("call\t{cond}, {:04x}", self.imm16()?),
Insn::PrefixCB => self.print_prefix_cb()?,
_ => format!("{insn}"),
})
}
pub fn print_prefix_cb(&mut self) -> Option<String> {
let prefixed: Prefixed = self.imm8()?.into();
Some(format!("{prefixed}"))
}
}

1127
boy-debug/src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

39
boy-debug/src/main.rs Normal file
View File

@@ -0,0 +1,39 @@
//! Debug interface for the Boy emulator
#![allow(unused_imports)]
use boy::memory::{Bus, Cart};
use boy_debug::{
bus::BusIOTools,
cli::{lexer::Lexible, parser::Parsible},
gameboy::Gameboy,
message::Response,
};
use rustyline::{config::Configurer, history::FileHistory, Editor};
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
// Set up the gameboy
// TODO: move off the main thread
let args: Vec<_> = std::env::args().collect();
let mut bus = match args.len() {
2 => vec![0; 0x10000].read_file(&args[1])?,
_ => return Err(format!("Usage: {} [rom.gb]", args[0]).into()),
};
// let mut bus = Bus::new(Cart::new(bus));
let mut gb = Gameboy::new(bus.ascii());
// Set up and run the REPL
let mut rl: Editor<(), FileHistory> = Editor::new()?;
rl.set_auto_add_history(true);
while let Ok(line) = rl.readline("\x1b[96mgb>\x1b[0m ") {
for request in line.chars().lex().parse() {
// TODO: Process requests in another thread, maybe
match gb.process(request) {
Response::Success => {}
Response::Failure => println!(":("),
Response::Data(data) => println!(" => 0x{data:02x} ({data})"),
}
}
}
Ok(())
}