initial commit

This commit is contained in:
45Tatami 2024-03-29 10:07:58 +01:00 committed by 45Tatami
commit 7a6e299da5
9 changed files with 801 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

223
Cargo.lock generated Normal file
View File

@ -0,0 +1,223 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "clap"
version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "jsgb"
version = "0.1.0"
dependencies = [
"clap",
]
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "2.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
[[package]]
name = "windows_i686_gnu"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
[[package]]
name = "windows_i686_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"

9
Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "jsgb"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "~4.4.0", features = ["derive"] }

6
shell.nix Normal file
View File

@ -0,0 +1,6 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = with pkgs; [ cargo rustc ];
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
}

79
src/breakpoint.rs Normal file
View File

@ -0,0 +1,79 @@
use std::collections::{HashMap, VecDeque};
// use crate::breakpoint_shell::{BreakPointShell, BreakPointShellCommand};
pub enum Command {
Ignore,
Step,
Cont,
Kill,
}
pub enum State {
Cont,
Step
}
#[derive(Debug)]
pub enum Trigger {
Address(usize),
WriteTo(usize),
Step(usize),
}
pub trait BreakPointHandler {
fn handler(&mut self, t: Trigger) -> Command;
}
pub struct BreakPointManager {
handlers: Vec<Box<dyn BreakPointHandler>>,
breakpoints: HashMap<usize, usize>,
steppers: Vec<usize>,
}
impl BreakPointManager {
pub fn new() -> Self {
BreakPointManager {
handlers: Vec::new(),
breakpoints: HashMap::new(),
steppers: Vec::new(),
}
}
// FIXME handler cannot use borrowed values adding more specific lifetimes could work but maybe
// it is better to not own it at all?
pub fn register_breakpoint(&mut self, adr: usize, bp: impl BreakPointHandler + 'static) {
self.handlers.insert(adr, Box::new(bp));
let i = self.handlers.len();
self.breakpoints.insert(adr, i);
}
pub fn inform(&mut self, adr: usize) -> Command {
let mut new_steppers = Vec::new();
new_steppers.reserve(self.steppers.len());
for hdl_i in self.steppers.drain(0..) {
let cmd = self.handlers[hdl_i].handler(Trigger::Step(adr));
match cmd {
Command::Step => new_steppers.push(hdl_i),
Command::Kill => return Command::Kill,
Command::Cont | Command::Ignore => (),
}
}
self.steppers = new_steppers;
if let Some(hdl_i) = self.breakpoints.get(&adr) {
println!("hit breakpoint {adr:#08x}");
let cmd = self.handlers[*hdl_i].handler(Trigger::Address(adr));
match cmd {
Command::Step => self.steppers.push(*hdl_i),
Command::Kill => return Command::Kill,
Command::Cont | Command::Ignore => (),
}
} else {
return Command::Ignore;
}
Command::Ignore // FIXME
}
}

63
src/breakpoint_shell.rs Normal file
View File

@ -0,0 +1,63 @@
use clap::{Parser, Subcommand};
use crate::breakpoint::{BreakPointHandler, Command, Trigger};
use std::io::{self, Write};
pub struct BreakPointShell {
}
#[derive(Parser, Debug)]
#[clap(disable_help_flag=true, disable_help_subcommand=false, disable_version_flag=true)]
struct BreakPointShellParser {
#[command(subcommand)]
cmd: ShellCommand,
}
#[derive(Subcommand, Debug)]
pub enum ShellCommand {
#[command(visible_alias="s")]
Step,
#[command(visible_alias="q")]
Quit,
}
impl BreakPointShell {
pub fn new() -> BreakPointShell {
BreakPointShell{}
}
}
impl BreakPointHandler for BreakPointShell {
fn handler(&mut self, _: Trigger) -> Command {
println!("welcome to the default debug shell; enter 'help' for a list of commands");
let stdin = io::stdin();
loop {
print!("$ ");
io::stdout().flush().unwrap();
let mut user_input = String::new();
stdin
.read_line(&mut user_input)
.expect("could not read command from stdin");
// FIXME needs to be able to handle quotes
let args = [""].into_iter().chain(user_input.trim().split(' '));
let parser = match BreakPointShellParser::try_parse_from(args) {
Ok(p) => p,
Err(e) => {
println!("{e}");
continue;
}
};
match parser.cmd {
ShellCommand::Step => break Command::Step,
ShellCommand::Quit => break Command::Kill,
}
}
}
}

251
src/emu.rs Normal file
View File

@ -0,0 +1,251 @@
use std::cmp;
use std::fs;
use std::io;
use std::io::{Read, Write};
use crate::breakpoint::{Command as BpCommand, BreakPointManager};
use crate::breakpoint_shell::BreakPointShell;
use crate::op::*;
const RAM_SIZE: usize = 0xFFFF + 1;
const BOOT_ROM_SIZE: usize = 256;
const REG_SIZE: usize = 0xB;
pub struct GameBoy {
boot_rom: [u8; BOOT_ROM_SIZE],
mem: [u8; RAM_SIZE],
reg: [u8; REG_SIZE],
pc: u16,
sp: u16,
bp_manager: BreakPointManager,
}
impl GameBoy {
pub fn new() -> GameBoy {
GameBoy {
boot_rom: [0; BOOT_ROM_SIZE],
mem: [0; RAM_SIZE],
reg: [0; REG_SIZE],
pc: 0,
sp: 0,
bp_manager: BreakPointManager::new(),
}
}
fn set(adr: u16, val: u8) {
// check for special registers
}
fn set_fast(adr: u16, val: u8) {
// no checks for special registers
}
pub fn load<T: io::Read>(&mut self, mut src: T) {
let mut buf = Vec::<u8>::new();
let _ = src.read_to_end(&mut buf);
// FIXME read only allowed amount
match (&mut self.mem[..]).write_all(&buf) {
Ok(_) => (),
Err(e) => panic!("{e}"),
};
let mut bios = fs::File::open("DMG_ROM.bin").expect("could not open bios");
bios.read_exact(&mut self.boot_rom)
.expect("fail to read rom");
}
pub fn register_breakpoint(&mut self, adr: &str) {
let adr = match adr.strip_prefix("0x") {
Some(adr) => adr,
None => adr,
};
let Ok(adr) = usize::from_str_radix(adr, 16) else {
panic!("FIXME error handling on given bad breakpoint address");
};
self.bp_manager.register_breakpoint(adr, BreakPointShell::new());
}
fn validate_header(&self) {
let mem = &self.mem;
let _nintendo_title = &mem[0x104..=0x133];
let _game_title = &mem[0x134..=0x13e];
let _game_code = &mem[0x13f..=0x142];
let _cgb_support_code = mem[0x143];
let _marker_code = ((mem[0x144] as u16) << 8) + (mem[0x145] as u16);
let _sgb_support_code = mem[0x146];
let _software_type = mem[0x147];
let _rom_size = mem[0x148];
let _ext_ram_size = mem[0x149];
let _dest_code = mem[0x14A];
let _mask_rom_ver = mem[0x14C];
let reg_data = &mem[0x134..=0x14C];
let sum = reg_data.iter().map(|x| *x as usize).sum::<usize>() + 0x19;
let comp = (-(sum as isize) as usize) & 0xFF;
let comp_exp = mem[0x14D] as usize;
if comp != comp_exp {
panic!("comp check failed: calc: {comp:#010b}\nexp: {comp_exp:#010b}");
} else {
println!("complement header check ok");
}
// let checksum = ((mem[0x14E] as u16) << 8) + mem[0x15F] as u16;
// println!("checksum: {checksum}");
}
pub fn run(&mut self) {
self.validate_header();
self.pc = 0x100;
self.sp = 0xFFFE;
loop {
self.step();
}
}
fn op_ld(&mut self, dst: Loc, src: Loc) {
let pc = self.pc as usize;
let (val, src_adr) = match src {
Loc::Reg(i) => (self.reg[i as usize], i as usize),
Loc::Val => (self.mem[pc + 1], 0),
Loc::Mem(_) => todo!(),
Loc::HMem(_) => todo!(),
};
let (dst_ref, dst_adr) = match dst {
Loc::Mem(ref src) => match src {
AddrLoc::Val => {
let adr = self.next16(pc) as usize;
(&mut self.mem[adr], adr)
}
_ => todo!(),
},
Loc::HMem(ref src) => match src {
AddrLoc::Val => {
let adr = self.mem[pc + 1] as usize + 0xFF00;
(&mut self.mem[adr], adr)
}
_ => todo!(),
},
Loc::Reg(r) => (&mut self.reg[r as usize], r as usize),
Loc::Val => panic!("val as destination should not be possible"),
};
let dst_str = dst.repr(dst_adr, *dst_ref);
let src_str = src.repr(src_adr, val);
println!("ld {dst_str} <- {src_str}");
*dst_ref = val;
}
fn op_ld16(&mut self, dst: Loc, src: Loc) {
let pc = self.pc as usize;
let (l, h) = match src {
Loc::Val => (self.mem[pc + 1], self.mem[pc + 2]),
_ => todo!(),
};
let dst_ref = match dst {
Loc::Reg(reg) => {
// FIXME assuming DD is a typo for DE...
let reg_loc = reg as usize;
&mut self.reg[reg_loc..=reg_loc + 1]
}
Loc::Val => panic!("value as dst is not allowed!"),
_ => todo!(),
};
match dst {
Loc::Reg(_) => println!("ld16 {dst:6?} <- 0x{h:02x}{l:02x}"),
_ => panic!("print address"),
}
// FIXME does this break when (if) copying from another register?
dst_ref[0] = l;
dst_ref[1] = h;
}
fn next16(&self, adr: usize) -> u16 {
let ladr = self.mem[adr + 1] as u16;
let hadr = self.mem[adr + 2] as u16;
(hadr << 8) + ladr
}
fn step(&mut self) {
use Instr::*;
let mut pc = self.pc as usize;
let instr = self.mem[pc];
match self.bp_manager.inform(pc) {
BpCommand::Ignore | BpCommand::Cont => (),
BpCommand::Step => todo!(),
BpCommand::Kill => panic!("kill from breakpoint"),
};
let win = &self.mem[pc..cmp::min(RAM_SIZE, pc + 3)];
let op = match Operation::try_from(instr) {
Ok(op) => op,
Err(err) => {
panic!("pc:{pc:#08x}:{win:02x?} error decoding operation {instr:#010b}: {err}");
}
};
println!("pc:{pc:#08x}:{win:02x?} {op:?}");
match op.inst {
Unimpl(instr) => {
let h = (instr & HD_MSK) >> 6;
let t1 = (instr & T1_MSK) >> 3;
let t2 = instr & T2_MSK;
panic!("unimplemented instruction {instr:#04x}: 0b{h:02b} {t1:03b} {t2:03b}")
}
Nop => (),
Jp => {
let new_pc = self.next16(pc);
self.pc = new_pc;
println!("jp {pc:#06x}->{new_pc:#06x}");
return;
}
Jr => {
let e = self.mem[pc + 1] as i8 + 2;
let old_pc = self.pc;
let new_pc = (old_pc as i16 + e as i16) as u16;
self.pc = new_pc;
println!("jr {new_pc:#06x} <- {old_pc:#06x} + {e}");
return;
}
Call => {
let sp = self.sp as usize;
self.mem[sp - 1] = (pc >> 8) as u8;
self.mem[sp - 2] = (pc & 0xFF) as u8;
let new_pc = self.next16(pc);
let old_pc = self.pc;
self.pc = new_pc;
self.sp -= 2;
println!("call {new_pc:#06x} <- {old_pc:#06x}");
return;
}
Ret => {
let new_pc = self.next16((self.sp as usize) - 1);
let old_pc = self.pc;
self.pc = new_pc;
self.sp += 2;
println!("ret {new_pc:#06x} <- {old_pc:#06x}");
}
Ld(dst, src) => self.op_ld(dst, src),
Ld16(dst, src) => self.op_ld16(dst, src),
Di => println!("FIXME Di instruction not implemented"),
}
pc += op.offs;
self.pc = pc as u16;
}
}

37
src/main.rs Normal file
View File

@ -0,0 +1,37 @@
mod breakpoint;
mod breakpoint_shell;
mod emu;
mod op;
use std::fs;
use std::path::PathBuf;
use clap::Parser;
use crate::emu::GameBoy;
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(short, long)]
breakpoint: Option<String>,
#[arg(long, default_value = "rom.gb")]
rom: PathBuf,
}
fn main() {
let cli = Cli::parse();
let mut gb = GameBoy::new();
let rom_path = cli.rom;
let rom = fs::File::open(rom_path).expect("could not open rom"); // FIXME better message
gb.load(rom);
if let Some(bp) = cli.breakpoint {
gb.register_breakpoint(&bp);
}
gb.run();
}

132
src/op.rs Normal file
View File

@ -0,0 +1,132 @@
pub const HD_MSK: u8 = 0b1100_0000;
pub const T1_MSK: u8 = 0b0011_1000;
pub const T2_MSK: u8 = 0b0000_0111;
#[derive(Debug)]
pub struct Operation {
pub inst: Instr,
pub cycl: usize,
pub offs: usize,
}
#[derive(Debug)]
pub enum Instr {
// Pseudo
Unimpl(u8),
// Misc
Nop,
// Load
Ld(Loc, Loc),
Ld16(Loc, Loc),
// CPU ctl
Di,
// Jump
Jp,
Jr,
Call,
Ret,
}
#[derive(Debug)]
pub enum AddrLoc {
Reg(Register),
Mem,
Val,
}
#[derive(Debug)]
pub enum Loc {
Reg(Register), // direct
Mem(AddrLoc),
HMem(AddrLoc),
Val,
}
#[derive(Debug, Copy, Clone)]
pub enum Register {
A = 0x07,
F = 0x08,
B = 0x00,
C = 0x01,
D = 0x02,
E = 0x03,
H = 0x04,
L = 0x05,
SP = 0x09,
}
impl Loc {
pub fn repr(&self, adr: usize, val: u8) -> String {
match self {
Loc::Reg(_) => format!("{self:6?}"),
Loc::Mem(_) | Loc::HMem(_) => format!("({adr:#06x})"),
Loc::Val => format!("{val:#04x}"),
}
}
}
impl TryFrom<u8> for Register {
type Error = String;
fn try_from(value: u8) -> Result<Self, Self::Error> {
use Register::*;
let v = match value {
0b111 => A,
0b000 => B,
0b001 => C,
0b010 => D,
0b011 => E,
0b100 => H,
0b101 => L,
val => return Err(format!("invalid register value {val:#b}")),
};
Ok(v)
}
}
impl TryFrom<u8> for Operation {
type Error = String;
fn try_from(value: u8) -> Result<Self, Self::Error> {
use Instr::*;
use Loc::*;
let (inst, cycl, offs) = match value {
0b00_000_000 => (Nop, 1, 1),
0b11_000_011 => (Jp, 4, 3),
0b00_011_000 => (Jr, 3, 2),
0b11_001_101 => (Call, 6, 3),
0b11_001_001 => (Ret, 4, 1),
0b11_110_011 => (Di, 1, 1),
0b11_101_010 => (Ld(Mem(AddrLoc::Val), Reg(Register::A)), 4, 3),
0b11_100_000 => (Ld(HMem(AddrLoc::Val), Reg(Register::A)), 3, 2),
_ if (value & (HD_MSK | T2_MSK)) == 0b00_000_110 => {
let reg = Register::try_from((value & T1_MSK) >> 3)?;
(Ld(Reg(reg), Val), 2, 2)
}
_ if (value & HD_MSK) == 0b01_000_000 => {
let dst = Register::try_from((value & T1_MSK) >> 3)?;
let src = Register::try_from(value & T2_MSK)?;
(Ld(Reg(dst), Reg(src)), 1, 1)
}
_ if (value & (HD_MSK | 0b1_000 | T2_MSK)) == 0b00_000_001 => {
let reg = match (value & 0b110_000) >> 4 {
0b00 => Register::B,
0b01 => Register::D,
0b10 => Register::H,
0b11 => Register::SP,
val => return Err(format!("invalid register {val}")),
};
(Ld16(Reg(reg), Val), 3, 3)
}
_ => (Unimpl(value), 0, 0),
};
Ok(Operation { inst, cycl, offs })
}
}