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:
11
boy-debug/Cargo.toml
Normal file
11
boy-debug/Cargo.toml
Normal 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
119
boy-debug/src/bus.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
86
boy-debug/src/disassembler.rs
Normal file
86
boy-debug/src/disassembler.rs
Normal 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
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
39
boy-debug/src/main.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user