From 7a6e299da5ed62d3a32ebf08e74ec555988823b8 Mon Sep 17 00:00:00 2001 From: 45Tatami Date: Fri, 29 Mar 2024 10:07:58 +0100 Subject: [PATCH] initial commit --- .gitignore | 1 + Cargo.lock | 223 +++++++++++++++++++++++++++++++++++ Cargo.toml | 9 ++ shell.nix | 6 + src/breakpoint.rs | 79 +++++++++++++ src/breakpoint_shell.rs | 63 ++++++++++ src/emu.rs | 251 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 37 ++++++ src/op.rs | 132 +++++++++++++++++++++ 9 files changed, 801 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 shell.nix create mode 100644 src/breakpoint.rs create mode 100644 src/breakpoint_shell.rs create mode 100644 src/emu.rs create mode 100644 src/main.rs create mode 100644 src/op.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..84b6b9b --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..529eafd --- /dev/null +++ b/Cargo.toml @@ -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"] } diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..4fb5456 --- /dev/null +++ b/shell.nix @@ -0,0 +1,6 @@ +{ pkgs ? import {} }: +pkgs.mkShell { + nativeBuildInputs = with pkgs; [ cargo rustc ]; + + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; +} diff --git a/src/breakpoint.rs b/src/breakpoint.rs new file mode 100644 index 0000000..0be1df3 --- /dev/null +++ b/src/breakpoint.rs @@ -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>, + breakpoints: HashMap, + steppers: Vec, +} + +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 + } +} diff --git a/src/breakpoint_shell.rs b/src/breakpoint_shell.rs new file mode 100644 index 0000000..f4a06f2 --- /dev/null +++ b/src/breakpoint_shell.rs @@ -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, + } + } + } +} diff --git a/src/emu.rs b/src/emu.rs new file mode 100644 index 0000000..fd9c5b7 --- /dev/null +++ b/src/emu.rs @@ -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(&mut self, mut src: T) { + let mut buf = Vec::::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::() + 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; + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..bd1d2bd --- /dev/null +++ b/src/main.rs @@ -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, + + #[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(); +} diff --git a/src/op.rs b/src/op.rs new file mode 100644 index 0000000..1427d70 --- /dev/null +++ b/src/op.rs @@ -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 for Register { + type Error = String; + + fn try_from(value: u8) -> Result { + 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 for Operation { + type Error = String; + + fn try_from(value: u8) -> Result { + 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 }) + } +}