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:
commit
acfbb8f1cf
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/target
|
||||||
|
|
||||||
|
# Test ROMs
|
||||||
|
/roms
|
||||||
|
**.gb
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode
|
10
Cargo.toml
Normal file
10
Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
workspace = { members = ["boy-debug", "boy-utils"] }
|
||||||
|
[package]
|
||||||
|
name = "boy"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bitfield-struct = "0.7.0"
|
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(())
|
||||||
|
}
|
9
boy-utils/Cargo.toml
Normal file
9
boy-utils/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "boy-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
boy = { path = ".." }
|
69
boy-utils/src/lib.rs
Normal file
69
boy-utils/src/lib.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/// Utilities and common behavior for the [boy] emulator
|
||||||
|
use std::io::Write;
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use boy::memory::io::*;
|
||||||
|
|
||||||
|
/// Formats the data over a given range as a traditional hexdump
|
||||||
|
pub fn slice_hexdump(data: &[u8], range: Range<usize>) -> std::io::Result<()> {
|
||||||
|
const WIDTH: usize = 16;
|
||||||
|
const HEX: usize = 6;
|
||||||
|
const ASCII: usize = 3 * (WIDTH + 1) + HEX;
|
||||||
|
|
||||||
|
let base = range.start;
|
||||||
|
let mut out = std::io::stdout().lock();
|
||||||
|
|
||||||
|
let Some(data) = data.get(range) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
for (chunkno, chunk) in data.chunks(WIDTH).enumerate() {
|
||||||
|
write!(out, "{:04x}\x1b[{HEX}G", base + chunkno * WIDTH)?;
|
||||||
|
for (byteno, byte) in chunk.iter().enumerate() {
|
||||||
|
write!(out, "{}{byte:02x}", if byteno == 8 { " " } else { " " })?
|
||||||
|
}
|
||||||
|
write!(out, "\x1b[{ASCII}G|")?;
|
||||||
|
for byte in chunk.iter() {
|
||||||
|
match char::from_u32(*byte as u32) {
|
||||||
|
Some(c) if c.is_ascii_graphic() => write!(out, "{c}")?,
|
||||||
|
Some(_) | None => write!(out, "∙")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeln!(out, "|")?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bus_hexdump(bus: &impl BusIO, range: Range<usize>) -> std::io::Result<()> {
|
||||||
|
use boy::memory::io::BusAux;
|
||||||
|
const WIDTH: usize = 16;
|
||||||
|
|
||||||
|
let mut out = std::io::stdout();
|
||||||
|
|
||||||
|
let upper = range.end;
|
||||||
|
for lower in range.clone().step_by(WIDTH) {
|
||||||
|
let upper = (lower + WIDTH).min(upper);
|
||||||
|
// write!(out, "{:04x}\x1b[{HEX}G", lower)?;
|
||||||
|
let _data: [u8; WIDTH] = bus.read_arr(lower).unwrap_or_else(|e| e);
|
||||||
|
write!(out, "{:04x} ", lower)?;
|
||||||
|
// write hexadecimal repr
|
||||||
|
for (idx, addr) in (lower..upper).enumerate() {
|
||||||
|
let space = if idx == WIDTH / 2 { " " } else { " " };
|
||||||
|
match bus.read(addr) {
|
||||||
|
Some(data) => write!(out, "{space}{data:02x}"),
|
||||||
|
None => write!(out, "{space}__"),
|
||||||
|
}?;
|
||||||
|
}
|
||||||
|
let padding = WIDTH - (upper - lower);
|
||||||
|
let padding = padding * 3 + if padding >= (WIDTH / 2) { 1 } else { 0 };
|
||||||
|
write!(out, "{:padding$} |", "")?;
|
||||||
|
for data in (lower..upper).flat_map(|addr| bus.read(addr)) {
|
||||||
|
match char::from_u32(data as u32) {
|
||||||
|
Some(c) if !c.is_control() => write!(out, "{c}"),
|
||||||
|
_ => write!(out, "."),
|
||||||
|
}?;
|
||||||
|
}
|
||||||
|
writeln!(out, "|")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
1048
src/cpu.rs
Normal file
1048
src/cpu.rs
Normal file
File diff suppressed because it is too large
Load Diff
654
src/cpu/disasm.rs
Normal file
654
src/cpu/disasm.rs
Normal file
@ -0,0 +1,654 @@
|
|||||||
|
//! Disassembler for SM83 machine code
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum Insn {
|
||||||
|
#[default]
|
||||||
|
/// No operation
|
||||||
|
Nop,
|
||||||
|
Stop,
|
||||||
|
Halt,
|
||||||
|
/// [Register](R8) to [register](R8) transfer
|
||||||
|
Ld(R8, R8),
|
||||||
|
/// Load an [8-bit](R8) immediate: `ld r8, #u8`
|
||||||
|
LdImm(R8),
|
||||||
|
/// Load a [16-bit](R16) immediate: `ld r16, #u16`
|
||||||
|
LdImm16(R16),
|
||||||
|
/// Load from [R16Indirect] into register [`a`](R8::A)
|
||||||
|
LdInd(R16Indirect),
|
||||||
|
/// Store register [`a`](R8::A) into [R16Indirect]
|
||||||
|
StInd(R16Indirect),
|
||||||
|
/// Load [`a`](R8::A) from 0xff00+[`c`](R8::C): `ld a, [c]`
|
||||||
|
LdC,
|
||||||
|
/// Store [`a`](R8::A) into 0xff00+[`c`](R8::C): `ld [c], a`
|
||||||
|
StC,
|
||||||
|
/// Load [`a`](R8::A) from absolute address: `ld a, [addr]`
|
||||||
|
LdAbs,
|
||||||
|
/// Store [`a`](R8::A) to absolute address: `ld [addr], a`
|
||||||
|
StAbs,
|
||||||
|
/// Store [stack pointer](R16::SP) to absolute address: `ld [addr], sp`
|
||||||
|
StSpAbs,
|
||||||
|
/// Load [`HL`](R16::HL) into SP
|
||||||
|
LdSpHl,
|
||||||
|
/// Load [`HL`](R16::HL) with SP + [u8]
|
||||||
|
LdHlSpRel,
|
||||||
|
/// Load from himem at immediate offset: `ldh a, [0xff00 + #u8]`
|
||||||
|
Ldh,
|
||||||
|
/// Store to himem at immediate offset: `ldh [0xff00 + #u8], a`
|
||||||
|
Sth,
|
||||||
|
|
||||||
|
/// Increment the value in the [register](R8)
|
||||||
|
Inc(R8),
|
||||||
|
/// Increment the value in the [register](R16)
|
||||||
|
Inc16(R16),
|
||||||
|
/// Decrement the value in the [register](R8)
|
||||||
|
Dec(R8),
|
||||||
|
/// Decrement the value in the [register](R16)
|
||||||
|
Dec16(R16),
|
||||||
|
|
||||||
|
/// Rotate left [`a`](R8::A)
|
||||||
|
Rlnca,
|
||||||
|
/// Rotate right [`a`](R8::A)
|
||||||
|
Rrnca,
|
||||||
|
/// Rotate left [`a`](R8::A) [through carry flag](https://rgbds.gbdev.io/docs/v0.7.0/gbz80.7#RLA)
|
||||||
|
Rla,
|
||||||
|
/// Rotate Right [`a`](R8::A) [through carry flag](https://rgbds.gbdev.io/docs/v0.7.0/gbz80.7#RRA)
|
||||||
|
Rra,
|
||||||
|
/// daa ; Decimal-adjust accumulator using sub and half-carry flag
|
||||||
|
Daa,
|
||||||
|
/// cpl ; Complement [`a`](R8::A)
|
||||||
|
Cpl,
|
||||||
|
/// scf ; Set carry flag
|
||||||
|
Scf,
|
||||||
|
// ccf ; Complement carry flag
|
||||||
|
Ccf,
|
||||||
|
|
||||||
|
/// add [`a`](R8::A), [R8]
|
||||||
|
Add(R8),
|
||||||
|
/// add[`HL`](R16::HL), [R16]
|
||||||
|
Add16HL(R16),
|
||||||
|
/// add #[u8], [`a`](R8::A)
|
||||||
|
AddI,
|
||||||
|
/// add [`sp`](R16::SP), #[i8]
|
||||||
|
Add16SpI,
|
||||||
|
/// adc [R8], [`a`](R8::A)
|
||||||
|
Adc(R8),
|
||||||
|
/// adc #[u8], [`a`](R8::A)
|
||||||
|
AdcI,
|
||||||
|
/// sub [R8], [`a`](R8::A)
|
||||||
|
Sub(R8),
|
||||||
|
/// sub #[u8], [`a`](R8::A)
|
||||||
|
SubI,
|
||||||
|
/// sbc [R8], [`a`](R8::A)
|
||||||
|
Sbc(R8),
|
||||||
|
/// sbc #[u8], [`a`](R8::A)
|
||||||
|
SbcI,
|
||||||
|
/// and [R8], [`a`](R8::A)
|
||||||
|
And(R8),
|
||||||
|
/// and #[u8], [`a`](R8::A)
|
||||||
|
AndI,
|
||||||
|
/// xor [R8], [`a`](R8::A)
|
||||||
|
Xor(R8),
|
||||||
|
/// xor #[u8], [`a`](R8::A)
|
||||||
|
XorI,
|
||||||
|
/// or [R8], [`a`](R8::A)
|
||||||
|
Or(R8),
|
||||||
|
/// or #[u8], [`a`](R8::A)
|
||||||
|
OrI,
|
||||||
|
/// cp [R8], [`a`](R8::A)
|
||||||
|
Cp(R8),
|
||||||
|
/// cp #[u8], [`a`](R8::A)
|
||||||
|
CpI,
|
||||||
|
|
||||||
|
/// push [r16](R16Stack)
|
||||||
|
Push(R16Stack),
|
||||||
|
/// pop [r16](R16Stack)
|
||||||
|
Pop(R16Stack),
|
||||||
|
|
||||||
|
/// jr #[i8]
|
||||||
|
Jr,
|
||||||
|
/// jr [Cond], #[i8]
|
||||||
|
Jrc(Cond),
|
||||||
|
/// jp #[u16]
|
||||||
|
Jp,
|
||||||
|
/// jp [Cond], #[u16]
|
||||||
|
Jpc(Cond),
|
||||||
|
/// jp [HL](R16::HL)
|
||||||
|
JpHL,
|
||||||
|
/// call #[u16]
|
||||||
|
Call,
|
||||||
|
/// call [Cond], #[u16]
|
||||||
|
Callc(Cond),
|
||||||
|
/// ret
|
||||||
|
Ret,
|
||||||
|
/// ret [Cond] ;
|
||||||
|
Retc(Cond),
|
||||||
|
|
||||||
|
/// di ; Disable Interrupts
|
||||||
|
Di,
|
||||||
|
/// ei ; Enable Interrupts
|
||||||
|
Ei,
|
||||||
|
/// reti ; Return from interrupt, re-enabling interrupts
|
||||||
|
Reti,
|
||||||
|
/// rst #vec ; Call one of the reset vectors in the first page
|
||||||
|
Rst(u8),
|
||||||
|
/// (cb) NN ; Next instruction is a [Prefixed] instruction
|
||||||
|
PrefixCB,
|
||||||
|
Invalid(u8),
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum Prefixed {
|
||||||
|
Rlc(R8),
|
||||||
|
Rrc(R8),
|
||||||
|
Rl(R8),
|
||||||
|
Rr(R8),
|
||||||
|
Sla(R8),
|
||||||
|
Sra(R8),
|
||||||
|
Swap(R8),
|
||||||
|
Srl(R8),
|
||||||
|
Bit(R8, U3),
|
||||||
|
Res(R8, U3),
|
||||||
|
Set(R8, U3),
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum U3 {
|
||||||
|
_0,
|
||||||
|
_1,
|
||||||
|
_2,
|
||||||
|
_3,
|
||||||
|
_4,
|
||||||
|
_5,
|
||||||
|
_6,
|
||||||
|
_7,
|
||||||
|
}
|
||||||
|
impl From<u8> for Insn {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
// low, middle, and high nibbles of the instruction
|
||||||
|
let (low, mid, high) = (value.low(), value.mid(), value.high());
|
||||||
|
// R16 specifier bits, and alternate-instruction bit
|
||||||
|
let (r16, alt) = (mid >> 1, mid & 1 == 0);
|
||||||
|
match high {
|
||||||
|
0 => match (low, mid) {
|
||||||
|
(0, 0) => Insn::Nop,
|
||||||
|
(0, 1) => Insn::LdSpHl,
|
||||||
|
(0, 2) => Insn::Stop,
|
||||||
|
(0, 3) => Insn::Jr,
|
||||||
|
(0, _) => Insn::Jrc((mid & 3).into()),
|
||||||
|
(1, _) if alt => Insn::LdImm16(r16.into()),
|
||||||
|
(1, _) => Insn::Add16HL(r16.into()),
|
||||||
|
(2, _) if alt => Insn::StInd(r16.into()),
|
||||||
|
(2, _) => Insn::LdInd(r16.into()),
|
||||||
|
(3, _) if alt => Insn::Inc16(r16.into()),
|
||||||
|
(3, _) => Insn::Dec16(r16.into()),
|
||||||
|
(4, dst) => Insn::Inc(dst.into()),
|
||||||
|
(5, dst) => Insn::Dec(dst.into()),
|
||||||
|
(6, dst) => Insn::LdImm(dst.into()),
|
||||||
|
(7, 0) => Insn::Rlnca,
|
||||||
|
(7, 1) => Insn::Rrnca,
|
||||||
|
(7, 2) => Insn::Rla,
|
||||||
|
(7, 3) => Insn::Rra,
|
||||||
|
(7, 4) => Insn::Daa,
|
||||||
|
(7, 5) => Insn::Cpl,
|
||||||
|
(7, 6) => Insn::Scf,
|
||||||
|
(7, 7) => Insn::Ccf,
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
1 if value == 0b01110110 => Insn::Halt,
|
||||||
|
1 => Insn::Ld(mid.into(), low.into()),
|
||||||
|
2 => match mid {
|
||||||
|
0 => Insn::Add(low.into()),
|
||||||
|
1 => Insn::Adc(low.into()),
|
||||||
|
2 => Insn::Sub(low.into()),
|
||||||
|
3 => Insn::Sbc(low.into()),
|
||||||
|
4 => Insn::And(low.into()),
|
||||||
|
5 => Insn::Or(low.into()),
|
||||||
|
6 => Insn::Xor(low.into()),
|
||||||
|
7 => Insn::Cp(low.into()),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
3 => match (low, mid) {
|
||||||
|
(0, 0..=3) => Insn::Retc((mid & 3).into()),
|
||||||
|
(0, 4) => Insn::Sth,
|
||||||
|
(0, 5) => Insn::Add16SpI,
|
||||||
|
(0, 6) => Insn::Ldh,
|
||||||
|
(0, 7) => Insn::LdHlSpRel,
|
||||||
|
(1, _) if alt => Insn::Pop(r16.into()),
|
||||||
|
(1, 1) => Insn::Ret,
|
||||||
|
(1, 3) => Insn::Reti,
|
||||||
|
(1, 5) => Insn::JpHL,
|
||||||
|
(1, 7) => Insn::LdSpHl,
|
||||||
|
(2, 0..=3) => Insn::Jpc((mid & 3).into()),
|
||||||
|
(2, 4) => Insn::StC,
|
||||||
|
(2, 5) => Insn::StAbs,
|
||||||
|
(2, 6) => Insn::LdC,
|
||||||
|
(2, 7) => Insn::LdAbs,
|
||||||
|
(3, 0) => Insn::Jp,
|
||||||
|
(3, 1) => Insn::PrefixCB,
|
||||||
|
(3, 6) => Insn::Di,
|
||||||
|
(3, 7) => Insn::Ei,
|
||||||
|
(3, _) => Insn::Invalid(value),
|
||||||
|
(4, 0..=3) => Insn::Callc((mid & 3).into()),
|
||||||
|
(4, _) => Insn::Invalid(value),
|
||||||
|
(5, 1) => Insn::Call,
|
||||||
|
(5, _) if alt => Insn::Push(r16.into()),
|
||||||
|
(5, _) => Insn::Invalid(value),
|
||||||
|
(6, 0) => Insn::AddI,
|
||||||
|
(6, 1) => Insn::AdcI,
|
||||||
|
(6, 2) => Insn::SubI,
|
||||||
|
(6, 3) => Insn::SbcI,
|
||||||
|
(6, 4) => Insn::AndI,
|
||||||
|
(6, 5) => Insn::XorI,
|
||||||
|
(6, 6) => Insn::OrI,
|
||||||
|
(6, 7) => Insn::CpI,
|
||||||
|
(7, rst) => Insn::Rst(rst),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<u8> for Prefixed {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
let r8 = value.low().into();
|
||||||
|
match (value.high(), value.mid()) {
|
||||||
|
(0, 0) => Self::Rlc(r8),
|
||||||
|
(0, 1) => Self::Rrc(r8),
|
||||||
|
(0, 2) => Self::Rl(r8),
|
||||||
|
(0, 3) => Self::Rr(r8),
|
||||||
|
(0, 4) => Self::Sla(r8),
|
||||||
|
(0, 5) => Self::Sra(r8),
|
||||||
|
(0, 6) => Self::Swap(r8),
|
||||||
|
(0, 7) => Self::Srl(r8),
|
||||||
|
(1, b3) => Self::Bit(r8, b3.into()),
|
||||||
|
(2, b3) => Self::Res(r8, b3.into()),
|
||||||
|
(3, b3) => Self::Set(r8, b3.into()),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<u8> for U3 {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
match value & 7 {
|
||||||
|
0 => Self::_0,
|
||||||
|
1 => Self::_1,
|
||||||
|
2 => Self::_2,
|
||||||
|
3 => Self::_3,
|
||||||
|
4 => Self::_4,
|
||||||
|
5 => Self::_5,
|
||||||
|
6 => Self::_6,
|
||||||
|
7 => Self::_7,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<U3> for u8 {
|
||||||
|
fn from(value: U3) -> Self {
|
||||||
|
value as _
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Note: Meant only for implementation on numeric types
|
||||||
|
pub trait U8ins: Sized {
|
||||||
|
/// Gets the Nth octal digit of Self
|
||||||
|
fn oct<const N: u8>(self) -> u8;
|
||||||
|
/// Gets the low octal digit of self
|
||||||
|
/// ```binary
|
||||||
|
/// 00000111 -> 111
|
||||||
|
/// ```
|
||||||
|
fn low(self) -> u8 {
|
||||||
|
self.oct::<0>()
|
||||||
|
}
|
||||||
|
/// Gets the middle octal digit of self, also known as B3/TGT3
|
||||||
|
/// ```binary
|
||||||
|
/// 00111000 -> 111
|
||||||
|
/// ```
|
||||||
|
fn mid(self) -> u8 {
|
||||||
|
self.oct::<1>()
|
||||||
|
}
|
||||||
|
/// Gets the high octal digit of self
|
||||||
|
/// ```binary
|
||||||
|
/// 11000000 -> 011
|
||||||
|
/// ```
|
||||||
|
fn high(self) -> u8 {
|
||||||
|
self.oct::<2>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl U8ins for u8 {
|
||||||
|
fn oct<const N: u8>(self) -> u8 {
|
||||||
|
self >> (N * 3) & 0b111
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 8 bit register or RAM access
|
||||||
|
/// 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
|
||||||
|
/// --|---|---|---|---|---|---|--
|
||||||
|
/// B | C | D | E | H | L |*HL| A
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum R8 {
|
||||||
|
B,
|
||||||
|
C,
|
||||||
|
D,
|
||||||
|
E,
|
||||||
|
H,
|
||||||
|
L,
|
||||||
|
HLmem,
|
||||||
|
A,
|
||||||
|
}
|
||||||
|
/// 16 bit register
|
||||||
|
/// 0 | 1 | 2 | 3
|
||||||
|
/// ----|----|----|----
|
||||||
|
/// BC | DE | HL | SP
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum R16 {
|
||||||
|
BC,
|
||||||
|
DE,
|
||||||
|
HL,
|
||||||
|
SP,
|
||||||
|
}
|
||||||
|
/// 16 bit operand to push and pop
|
||||||
|
/// 0 | 1 | 2 | 3
|
||||||
|
/// ----|----|----|----
|
||||||
|
/// BC | DE | HL | AF
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum R16Stack {
|
||||||
|
BC,
|
||||||
|
DE,
|
||||||
|
HL,
|
||||||
|
AF,
|
||||||
|
}
|
||||||
|
/// 16 bit memory access through pointer
|
||||||
|
/// 0 | 1 | 2 | 3
|
||||||
|
/// ----|----|----|----
|
||||||
|
/// BC | DE | HL+| HL-
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum R16Indirect {
|
||||||
|
/// Address in [R16::BC]
|
||||||
|
BC,
|
||||||
|
/// Address in [R16::DE]
|
||||||
|
DE,
|
||||||
|
/// Address in [R16::HL]. Post-increment HL.
|
||||||
|
HLI,
|
||||||
|
/// Address in [R16::HL]. Post-decrement HL.
|
||||||
|
HLD,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 0 | 1 | 2 | 3
|
||||||
|
/// ----|----|----|----
|
||||||
|
/// NZ | Z | NC | C
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum Cond {
|
||||||
|
NZ,
|
||||||
|
Z,
|
||||||
|
NC,
|
||||||
|
C,
|
||||||
|
}
|
||||||
|
|
||||||
|
mod conv {
|
||||||
|
use super::*;
|
||||||
|
impl From<u8> for R8 {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => Self::B,
|
||||||
|
1 => Self::C,
|
||||||
|
2 => Self::D,
|
||||||
|
3 => Self::E,
|
||||||
|
4 => Self::H,
|
||||||
|
5 => Self::L,
|
||||||
|
6 => Self::HLmem,
|
||||||
|
7 => Self::A,
|
||||||
|
n => panic!("Index out of bounds: {n} > 7"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<u8> for R16 {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => Self::BC,
|
||||||
|
1 => Self::DE,
|
||||||
|
2 => Self::HL,
|
||||||
|
3 => Self::SP,
|
||||||
|
n => panic!("Index out of bounds: {n} > 3"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<u8> for R16Stack {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => Self::BC,
|
||||||
|
1 => Self::DE,
|
||||||
|
2 => Self::HL,
|
||||||
|
3 => Self::AF,
|
||||||
|
n => panic!("Index out of bounds: {n} > 3"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<u8> for R16Indirect {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => Self::BC,
|
||||||
|
1 => Self::DE,
|
||||||
|
2 => Self::HLI,
|
||||||
|
3 => Self::HLD,
|
||||||
|
n => panic!("Index out of bounds: {n} > 3"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<u8> for Cond {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => Self::NZ,
|
||||||
|
1 => Self::Z,
|
||||||
|
2 => Self::NC,
|
||||||
|
3 => Self::C,
|
||||||
|
n => panic!("Index out of bounds: {n} > 3"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod disp {
|
||||||
|
use super::*;
|
||||||
|
use std::fmt::Display;
|
||||||
|
impl Display for Insn {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Insn::Nop => write!(f, "nop"),
|
||||||
|
Insn::Stop => write!(f, "stop"),
|
||||||
|
Insn::Halt => write!(f, "halt"),
|
||||||
|
|
||||||
|
Insn::Ld(dst, src) => write!(f, "ld\t{dst}, {src}"),
|
||||||
|
Insn::LdImm(r8) => write!(f, "ld\t{r8}, #u8"),
|
||||||
|
Insn::LdImm16(r16) => write!(f, "ld\t{r16}, #u16"),
|
||||||
|
Insn::LdInd(src) => write!(f, "ld\ta, {src}"),
|
||||||
|
Insn::StInd(dst) => write!(f, "ld\t{dst}, a"),
|
||||||
|
Insn::LdC => write!(f, "ld\ta, [c]"),
|
||||||
|
Insn::StC => write!(f, "ld\t[c], a"),
|
||||||
|
Insn::LdAbs => write!(f, "ld\ta, [#u16]"),
|
||||||
|
Insn::StAbs => write!(f, "ld\t[#u16], a"),
|
||||||
|
Insn::StSpAbs => write!(f, "ld\t[#u16], sp"),
|
||||||
|
Insn::LdSpHl => write!(f, "ld\tsp, hl"),
|
||||||
|
Insn::LdHlSpRel => write!(f, "ldhl\tsp, #u8"),
|
||||||
|
|
||||||
|
Insn::Ldh => write!(f, "ldh\ta, [0xff00 + #u8]"),
|
||||||
|
Insn::Sth => write!(f, "ldh\t[0xff00 + #u8], a"),
|
||||||
|
|
||||||
|
Insn::Inc(dst) => write!(f, "inc\t{dst}"),
|
||||||
|
Insn::Inc16(dst) => write!(f, "inc\t{dst}"),
|
||||||
|
Insn::Dec(dst) => write!(f, "dec\t{dst}"),
|
||||||
|
Insn::Dec16(dst) => write!(f, "dec\t{dst}"),
|
||||||
|
|
||||||
|
Insn::Rlnca => write!(f, "rlca"),
|
||||||
|
Insn::Rrnca => write!(f, "rrca"),
|
||||||
|
Insn::Rla => write!(f, "rla"),
|
||||||
|
Insn::Rra => write!(f, "rra"),
|
||||||
|
Insn::Daa => write!(f, "daa"),
|
||||||
|
Insn::Cpl => write!(f, "cpl"),
|
||||||
|
Insn::Scf => write!(f, "scf"),
|
||||||
|
Insn::Ccf => write!(f, "ccf"),
|
||||||
|
|
||||||
|
Insn::Add(src) => write!(f, "add\ta, {src}"),
|
||||||
|
Insn::AddI => write!(f, "add\ta, #u8"),
|
||||||
|
Insn::Add16HL(src) => write!(f, "add\tHL, {src}"),
|
||||||
|
Insn::Add16SpI => write!(f, "add\tSP, #i8"),
|
||||||
|
Insn::Adc(src) => write!(f, "adc\ta, {src}"),
|
||||||
|
Insn::AdcI => write!(f, "adc\ta, #u8"),
|
||||||
|
Insn::Sub(src) => write!(f, "sub\ta, {src}"),
|
||||||
|
Insn::SubI => write!(f, "sub\ta, #u8"),
|
||||||
|
Insn::Sbc(src) => write!(f, "sbc\ta, {src}"),
|
||||||
|
Insn::SbcI => write!(f, "sbc\ta, #u8"),
|
||||||
|
Insn::And(src) => write!(f, "and\ta, {src}"),
|
||||||
|
Insn::AndI => write!(f, "and\ta, #u8"),
|
||||||
|
Insn::Xor(src) => write!(f, "xor\ta, {src}"),
|
||||||
|
Insn::XorI => write!(f, "xor\ta, #u8"),
|
||||||
|
Insn::Or(src) => write!(f, "or\ta, {src}"),
|
||||||
|
Insn::OrI => write!(f, "or\ta, #u8"),
|
||||||
|
Insn::Cp(src) => write!(f, "cp\ta, {src}"),
|
||||||
|
Insn::CpI => write!(f, "cp\ta, #u8"),
|
||||||
|
|
||||||
|
Insn::Push(src) => write!(f, "push\t{src}"),
|
||||||
|
Insn::Pop(dst) => write!(f, "pop\t{dst}"),
|
||||||
|
|
||||||
|
Insn::Jr => write!(f, "jr\t#i8"),
|
||||||
|
Insn::Jrc(cond) => write!(f, "jr\t{cond}, #i8"),
|
||||||
|
Insn::Jp => write!(f, "jp\t#u16"),
|
||||||
|
Insn::Jpc(cond) => write!(f, "jp\t{cond}, #u16"),
|
||||||
|
Insn::JpHL => write!(f, "jp\tHL"),
|
||||||
|
Insn::Call => write!(f, "call\t#u16"),
|
||||||
|
Insn::Callc(cond) => write!(f, "call\t{cond}, #u16"),
|
||||||
|
Insn::Di => write!(f, "di"),
|
||||||
|
Insn::Ei => write!(f, "ei"),
|
||||||
|
Insn::Ret => write!(f, "ret"),
|
||||||
|
Insn::Retc(cond) => write!(f, "ret\t{cond}"),
|
||||||
|
Insn::Reti => write!(f, "reti"),
|
||||||
|
Insn::Rst(vector) => write!(f, "rst\t${:02x}", vector * 8),
|
||||||
|
Insn::PrefixCB => write!(f, "; 0xCB"),
|
||||||
|
Insn::Invalid(op) => write!(f, "_{op:02x}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for Prefixed {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Prefixed::Rlc(r8) => write!(f, "rlc\t{r8}"),
|
||||||
|
Prefixed::Rrc(r8) => write!(f, "rrc\t{r8}"),
|
||||||
|
Prefixed::Rl(r8) => write!(f, "rl\t{r8}"),
|
||||||
|
Prefixed::Rr(r8) => write!(f, "rr\t{r8}"),
|
||||||
|
Prefixed::Sla(r8) => write!(f, "sla\t{r8}"),
|
||||||
|
Prefixed::Sra(r8) => write!(f, "sra\t{r8}"),
|
||||||
|
Prefixed::Swap(r8) => write!(f, "swap\t{r8}"),
|
||||||
|
Prefixed::Srl(r8) => write!(f, "srl\t{r8}"),
|
||||||
|
Prefixed::Bit(r8, u3) => write!(f, "bit\t{r8}, {:x}", *u3 as u8),
|
||||||
|
Prefixed::Res(r8, u3) => write!(f, "res\t{r8}, {:x}", *u3 as u8),
|
||||||
|
Prefixed::Set(r8, u3) => write!(f, "set\t{r8}, {:x}", *u3 as u8),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for R8 {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
R8::B => write!(f, "b"),
|
||||||
|
R8::C => write!(f, "c"),
|
||||||
|
R8::D => write!(f, "d"),
|
||||||
|
R8::E => write!(f, "e"),
|
||||||
|
R8::H => write!(f, "h"),
|
||||||
|
R8::L => write!(f, "l"),
|
||||||
|
R8::HLmem => write!(f, "[hl]"),
|
||||||
|
R8::A => write!(f, "a"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for R16 {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
R16::BC => write!(f, "bc"),
|
||||||
|
R16::DE => write!(f, "de"),
|
||||||
|
R16::HL => write!(f, "hl"),
|
||||||
|
R16::SP => write!(f, "sp"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for R16Stack {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
R16Stack::BC => write!(f, "bc"),
|
||||||
|
R16Stack::DE => write!(f, "de"),
|
||||||
|
R16Stack::HL => write!(f, "hl"),
|
||||||
|
R16Stack::AF => write!(f, "af"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for R16Indirect {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
R16Indirect::BC => write!(f, "[bc]"),
|
||||||
|
R16Indirect::DE => write!(f, "[de]"),
|
||||||
|
R16Indirect::HLI => write!(f, "[hl+]"),
|
||||||
|
R16Indirect::HLD => write!(f, "[hl-]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for Cond {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Cond::NZ => write!(f, "nz"),
|
||||||
|
Cond::Z => write!(f, "z"),
|
||||||
|
Cond::NC => write!(f, "nc"),
|
||||||
|
Cond::C => write!(f, "c"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mod info {
|
||||||
|
use super::Insn;
|
||||||
|
|
||||||
|
impl Insn {
|
||||||
|
/// Gets the length of the instruction in bytes
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Insn::Nop | Insn::Stop | Insn::Halt => 1,
|
||||||
|
Insn::Ld(_, _) => 1,
|
||||||
|
Insn::LdImm(_) => 2,
|
||||||
|
Insn::LdImm16(_) => 3,
|
||||||
|
Insn::LdInd(_) | Insn::StInd(_) => 1,
|
||||||
|
Insn::LdC | Insn::StC => 1,
|
||||||
|
Insn::LdAbs | Insn::StAbs | Insn::StSpAbs => 3,
|
||||||
|
Insn::LdSpHl => 1,
|
||||||
|
Insn::LdHlSpRel => 2,
|
||||||
|
Insn::Ldh | Insn::Sth => 2,
|
||||||
|
Insn::Inc(_) | Insn::Inc16(_) => 1,
|
||||||
|
Insn::Dec(_) | Insn::Dec16(_) => 1,
|
||||||
|
Insn::Rlnca | Insn::Rrnca | Insn::Rla | Insn::Rra => 1,
|
||||||
|
Insn::Daa | Insn::Cpl | Insn::Scf | Insn::Ccf => 1,
|
||||||
|
Insn::Add(_) | Insn::Add16HL(_) => 1,
|
||||||
|
Insn::AddI | Insn::Add16SpI => 2,
|
||||||
|
Insn::Adc(_)
|
||||||
|
| Insn::Sub(_)
|
||||||
|
| Insn::Sbc(_)
|
||||||
|
| Insn::And(_)
|
||||||
|
| Insn::Xor(_)
|
||||||
|
| Insn::Or(_)
|
||||||
|
| Insn::Cp(_) => 1,
|
||||||
|
Insn::AdcI
|
||||||
|
| Insn::SubI
|
||||||
|
| Insn::SbcI
|
||||||
|
| Insn::AndI
|
||||||
|
| Insn::XorI
|
||||||
|
| Insn::OrI
|
||||||
|
| Insn::CpI => 2,
|
||||||
|
Insn::Push(_) | Insn::Pop(_) => 1,
|
||||||
|
Insn::Jr | Insn::Jrc(_) => 2,
|
||||||
|
Insn::Jp | Insn::Jpc(_) => 3,
|
||||||
|
Insn::JpHL => 1,
|
||||||
|
Insn::Call | Insn::Callc(_) => 3,
|
||||||
|
Insn::Ret | Insn::Retc(_) => 1,
|
||||||
|
Insn::Di | Insn::Ei => 1,
|
||||||
|
Insn::Reti => 1,
|
||||||
|
Insn::Rst(_) => 1,
|
||||||
|
Insn::PrefixCB => 1,
|
||||||
|
Insn::Invalid(_) => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
matches!(self, Insn::Invalid(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
194
src/cpu/split_register.rs
Normal file
194
src/cpu/split_register.rs
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
//! Represents two 8-bit registers which may be conjoined into one 16-bit register
|
||||||
|
//!
|
||||||
|
//! The [SplitRegister] is represented by a native-endian [`Wrapping<u16>`]
|
||||||
|
//! or two [`Wrapping<u8>`]s, respectively
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
|
//! The [SplitRegister] can be initialized
|
||||||
|
//! ```rust
|
||||||
|
//! # use boy::cpu::split_register::SplitRegister;
|
||||||
|
//! use std::num::Wrapping;
|
||||||
|
//!
|
||||||
|
//! let reg = SplitRegister::from(0x1000);
|
||||||
|
//! assert_eq!(*reg.lo(), Wrapping(0x00));
|
||||||
|
//! assert_eq!(*reg.hi(), Wrapping(0x10));
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::{fmt::Debug, hash::Hash, num::Wrapping, ops::*};
|
||||||
|
|
||||||
|
use super::Flags;
|
||||||
|
|
||||||
|
/// The low-byte index constant
|
||||||
|
const LO: usize = if 1u16.to_be() != 1u16 { 0 } else { 1 };
|
||||||
|
/// The high-byte index constant
|
||||||
|
const HI: usize = 1 - LO;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub union SplitRegister {
|
||||||
|
lh: [Wrapping<u8>; 2],
|
||||||
|
af: [Flags; 2],
|
||||||
|
r16: Wrapping<u16>,
|
||||||
|
}
|
||||||
|
impl SplitRegister {
|
||||||
|
/// Read the low byte of the register
|
||||||
|
pub fn lo(&self) -> &Wrapping<u8> {
|
||||||
|
// SAFETY: The GB's registers store POD types, so it's safe to transmute
|
||||||
|
unsafe { &self.lh[LO] }
|
||||||
|
}
|
||||||
|
/// Read the high byte of the register
|
||||||
|
pub fn hi(&self) -> &Wrapping<u8> {
|
||||||
|
// SAFETY: See [SplitRegister::lo]
|
||||||
|
unsafe { &self.lh[HI] }
|
||||||
|
}
|
||||||
|
/// Read the [flags byte](Flags) of the register
|
||||||
|
pub fn flags(&self) -> &Flags {
|
||||||
|
// SAFETY: See [SplitRegister::lo]
|
||||||
|
unsafe { &self.af[LO] }
|
||||||
|
}
|
||||||
|
/// Read the contents of the combined register
|
||||||
|
pub fn wide(&self) -> &Wrapping<u16> {
|
||||||
|
// SAFETY: See [SplitRegister::lo]
|
||||||
|
unsafe { &self.r16 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to the low byte of the register
|
||||||
|
pub fn lo_mut(&mut self) -> &mut Wrapping<u8> {
|
||||||
|
// SAFETY: See [SplitRegister::lo]
|
||||||
|
unsafe { &mut self.lh[LO] }
|
||||||
|
}
|
||||||
|
/// Gets a mutable reference to the high byte of the register
|
||||||
|
pub fn hi_mut(&mut self) -> &mut Wrapping<u8> {
|
||||||
|
// SAFETY: See [SplitRegister::lo]
|
||||||
|
unsafe { &mut self.lh[HI] }
|
||||||
|
}
|
||||||
|
/// Gets a mutable reference to the [flags byte](Flags) of the register
|
||||||
|
pub fn flags_mut(&mut self) -> &mut Flags {
|
||||||
|
// SAFETY: See [SplitRegister::lo]
|
||||||
|
unsafe { &mut self.af[LO] }
|
||||||
|
}
|
||||||
|
/// Gets a mutable reference to the combined register
|
||||||
|
pub fn wide_mut(&mut self) -> &mut Wrapping<u16> {
|
||||||
|
// SAFETY: See [SplitRegister::lo]
|
||||||
|
unsafe { &mut self.r16 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a SplitRegister from an array of big-endian bytes.
|
||||||
|
pub fn from_be_bytes(value: [u8; 2]) -> Self {
|
||||||
|
Self {
|
||||||
|
lh: [Wrapping(value[HI]), Wrapping(value[LO])],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Constructs a SplitRegister from an array of little-endian bytes.
|
||||||
|
pub fn from_le_bytes(value: [u8; 2]) -> Self {
|
||||||
|
Self {
|
||||||
|
lh: [Wrapping(value[LO]), Wrapping(value[HI])],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Constructs a SplitRegister from an array of host-endian bytes.
|
||||||
|
pub fn from_ne_bytes(value: [u8; 2]) -> Self {
|
||||||
|
Self {
|
||||||
|
lh: [Wrapping(value[0]), Wrapping(value[1])],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Debug for SplitRegister {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "[{:02x}, {:02x}]", self.lo(), self.hi())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Default for SplitRegister {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { r16: Wrapping(0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl PartialEq for SplitRegister {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.wide().eq(other.wide())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Eq for SplitRegister {}
|
||||||
|
impl PartialOrd for SplitRegister {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Ord for SplitRegister {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.wide().cmp(other.wide())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Hash for SplitRegister {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.wide().hash(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<u16> for SplitRegister {
|
||||||
|
fn from(value: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
r16: Wrapping(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Wrapping<u16>> for SplitRegister {
|
||||||
|
fn from(value: Wrapping<u16>) -> Self {
|
||||||
|
Self { r16: value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro impl_ops(for $base:ident {$($trait:ident<$($ty:ty),*$(,)?> $func:ident()),*$(,)?}) {
|
||||||
|
$(
|
||||||
|
impl $trait<Wrapping<u16>> for $base {
|
||||||
|
type Output = Self;
|
||||||
|
fn $func(self, rhs: Wrapping<u16>) -> Self::Output {
|
||||||
|
Self { r16: self.wide().$func(rhs) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl $trait<Self> for $base {
|
||||||
|
type Output = Self;
|
||||||
|
fn $func(self, rhs: Self) -> Self::Output {
|
||||||
|
self.$func(*rhs.wide())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$(impl $trait<$ty> for $base {
|
||||||
|
type Output = Self;
|
||||||
|
fn $func(self, rhs: $ty) -> Self::Output {
|
||||||
|
self.$func(Wrapping(rhs as u16))
|
||||||
|
}
|
||||||
|
})*
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
impl_ops!(for SplitRegister {
|
||||||
|
Mul<u8, u16> mul(),
|
||||||
|
Div<u8, u16> div(),
|
||||||
|
Rem<u8, u16> rem(),
|
||||||
|
Add<u8, u16> add(),
|
||||||
|
Sub<u8, u16> sub(),
|
||||||
|
BitAnd<u8, u16> bitand(),
|
||||||
|
BitOr<u8, u16> bitor(),
|
||||||
|
BitXor<u8, u16> bitxor(),
|
||||||
|
});
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn low_is_low() {
|
||||||
|
let reg = SplitRegister {
|
||||||
|
r16: Wrapping(0x00ff),
|
||||||
|
};
|
||||||
|
assert_eq!(*reg.lo(), Wrapping(0xff))
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn hi_is_hi() {
|
||||||
|
let reg = SplitRegister {
|
||||||
|
r16: Wrapping(0xff00),
|
||||||
|
};
|
||||||
|
assert_eq!(*reg.hi(), Wrapping(0xff))
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn from_bytes() {
|
||||||
|
let be = SplitRegister::from_be_bytes([0xc5, 0x77]);
|
||||||
|
let le = SplitRegister::from_le_bytes([0xc5, 0x77]);
|
||||||
|
let ne = SplitRegister::from_ne_bytes(0x1234u16.to_ne_bytes());
|
||||||
|
assert_eq!(*be.wide(), Wrapping(0xc577));
|
||||||
|
assert_eq!(*le.wide(), Wrapping(0x77c5));
|
||||||
|
assert_eq!(*ne.wide(), Wrapping(0x1234));
|
||||||
|
}
|
420
src/lib.rs
Normal file
420
src/lib.rs
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
//! A very inaccurate gameboy emulator
|
||||||
|
#![feature(decl_macro)]
|
||||||
|
#![allow(clippy::unit_arg)]
|
||||||
|
|
||||||
|
/// A gameboy has:
|
||||||
|
/// - A 16-bit memory bus
|
||||||
|
/// - A CPU
|
||||||
|
/// - A picture processing unit
|
||||||
|
|
||||||
|
pub mod error {
|
||||||
|
use crate::cpu::disasm::Insn;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Error {
|
||||||
|
kind: ErrorKind,
|
||||||
|
// any other information about an error
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum ErrorKind {
|
||||||
|
InvalidInstruction(u8),
|
||||||
|
InvalidAddress(u16),
|
||||||
|
UnimplementedInsn(Insn),
|
||||||
|
Unimplemented(u8),
|
||||||
|
HitBreak(u16, u8),
|
||||||
|
Todo,
|
||||||
|
}
|
||||||
|
impl From<ErrorKind> for Error {
|
||||||
|
fn from(value: ErrorKind) -> Self {
|
||||||
|
Self { kind: value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> From<Error> for Result<T, Error> {
|
||||||
|
fn from(value: Error) -> Self {
|
||||||
|
Err(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.kind.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for ErrorKind {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ErrorKind::InvalidInstruction(i) => write!(f, "Invalid instruction: {i}"),
|
||||||
|
ErrorKind::InvalidAddress(addr) => write!(f, "Invalid address: {addr:04x}"),
|
||||||
|
ErrorKind::UnimplementedInsn(i) => write!(f, "Unimplemented instruction: {i}"),
|
||||||
|
ErrorKind::Unimplemented(i) => write!(f, "Unimplemented instruction: {i}"),
|
||||||
|
ErrorKind::HitBreak(addr, m) => {
|
||||||
|
write!(f, "Hit breakpoint {addr:x} (Took {m} cycles)")
|
||||||
|
}
|
||||||
|
ErrorKind::Todo => write!(f, "Not yet implemented"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod memory;
|
||||||
|
|
||||||
|
pub mod graphics {
|
||||||
|
use bitfield_struct::bitfield;
|
||||||
|
|
||||||
|
use crate::memory::io::BusIO;
|
||||||
|
use std::mem::size_of;
|
||||||
|
|
||||||
|
/// The number of OAM entries supported by the PPU (on real hardware, 40)
|
||||||
|
pub const NUM_OAM_ENTRIES: usize = 40;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum Mode {
|
||||||
|
HBlank,
|
||||||
|
VBlank,
|
||||||
|
OAMScan,
|
||||||
|
Drawing,
|
||||||
|
}
|
||||||
|
impl Mode {
|
||||||
|
const fn from_bits(value: u8) -> Self {
|
||||||
|
match value & 3 {
|
||||||
|
0b00 => Self::HBlank,
|
||||||
|
0b01 => Self::VBlank,
|
||||||
|
0b10 => Self::OAMScan,
|
||||||
|
0b11 => Self::Drawing,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fn into_bits(self) -> u8 {
|
||||||
|
self as _
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type OAMTable = [OAMEntry; NUM_OAM_ENTRIES];
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, PartialOrd)]
|
||||||
|
pub struct PPU {
|
||||||
|
lcdc: LcdC, // LCD Control
|
||||||
|
stat: Stat, // LCD Status
|
||||||
|
scy: u8, // viewport y coordinate
|
||||||
|
scx: u8, // viewport x coordinate
|
||||||
|
/// Current line. Read-only.
|
||||||
|
ly: u8, // holds values in range 0..=153 (144..=153 = vblank)
|
||||||
|
/// Line Y for Comparison. Triggers interrupt if enabled in `stat`
|
||||||
|
lyc: u8, // line y compare, triggers interrupt if enabled
|
||||||
|
|
||||||
|
oam: OAMTable,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PPU {
|
||||||
|
pub fn oam(&self) -> &[u8] {
|
||||||
|
let data = self.oam.as_slice().as_ptr().cast();
|
||||||
|
let len = size_of::<OAMTable>();
|
||||||
|
// SAFETY:
|
||||||
|
// [OAMEntry; NUM_OAM_ENTRIES] is sizeof<OAM_ENTRY> * NUM_OAM_ENTRIES long,
|
||||||
|
// and already aligned
|
||||||
|
unsafe { core::slice::from_raw_parts(data, len) }
|
||||||
|
}
|
||||||
|
pub fn oam_mut(&mut self) -> &mut [u8] {
|
||||||
|
let data = self.oam.as_mut_slice().as_mut_ptr().cast();
|
||||||
|
let len = size_of::<OAMTable>();
|
||||||
|
// SAFETY: see above
|
||||||
|
unsafe { core::slice::from_raw_parts_mut(data, len) }
|
||||||
|
}
|
||||||
|
/// Gets the current PPU operating mode
|
||||||
|
pub fn mode(&self) -> Mode {
|
||||||
|
self.stat.mode()
|
||||||
|
}
|
||||||
|
pub fn set_mode(&mut self, mode: Mode) {
|
||||||
|
self.stat.set_mode(mode);
|
||||||
|
}
|
||||||
|
/// Gets the STAT interrupt selection config.
|
||||||
|
///
|
||||||
|
/// | Bit | Name |
|
||||||
|
/// |:---:|:---------------|
|
||||||
|
/// | `0` | H Blank |
|
||||||
|
/// | `1` | V Blank |
|
||||||
|
/// | `2` | OAM Scan |
|
||||||
|
/// | `3` | LY Coincidence |
|
||||||
|
pub fn stat_interrupt_selection(&self) -> u8 {
|
||||||
|
(self.stat.into_bits() & 0b1111000) >> 3
|
||||||
|
}
|
||||||
|
/// Gets the LY coincidence flag. Should be true when LY = LYC
|
||||||
|
pub fn ly_coincidence(&self) -> bool {
|
||||||
|
self.stat.lyc_hit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PPU {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
lcdc: Default::default(),
|
||||||
|
stat: Default::default(),
|
||||||
|
scy: Default::default(),
|
||||||
|
scx: Default::default(),
|
||||||
|
ly: Default::default(),
|
||||||
|
lyc: Default::default(),
|
||||||
|
oam: [Default::default(); NUM_OAM_ENTRIES],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl BusIO for PPU {
|
||||||
|
fn read(&self, addr: usize) -> Option<u8> {
|
||||||
|
match addr {
|
||||||
|
0xfe00..=0xfe9f => self.oam().read(addr - 0xfe00),
|
||||||
|
0xff40 => Some(self.lcdc.into_bits()),
|
||||||
|
0xff41 => Some(self.stat.into_bits()),
|
||||||
|
0xff42 => Some(self.scy),
|
||||||
|
0xff43 => Some(self.scx),
|
||||||
|
0xff44 => Some(self.ly),
|
||||||
|
0xff45 => Some(self.lyc),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, addr: usize, data: u8) -> Option<()> {
|
||||||
|
match addr {
|
||||||
|
0xfe00..=0xfe9f => self.oam_mut().write(addr - 0xfe00, data),
|
||||||
|
0xff40 => Some(self.lcdc = LcdC::from_bits(data)),
|
||||||
|
0xff41 => Some(self.stat.set_interrupts(Stat::from_bits(data).interrupts())),
|
||||||
|
0xff42 => Some(self.scy = data),
|
||||||
|
0xff43 => Some(self.scx = data),
|
||||||
|
0xff44 => None,
|
||||||
|
0xff45 => Some(self.lyc = data),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct OAMEntry {
|
||||||
|
y: u8, // y coordinate of the bottom of a 16px sprite
|
||||||
|
x: u8, // x coordinate of the right of an 8px sprite
|
||||||
|
tid: u8, // tile identifier
|
||||||
|
attr: OAMAttr, // OAM Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bitfield(u8)]
|
||||||
|
/// OAM Attributes. Read-writable by CPU, read-only by PPU.
|
||||||
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct OAMAttr {
|
||||||
|
#[bits(3)]
|
||||||
|
/// Which palette (OBP 0-7) does this object use in CGB mode?
|
||||||
|
cgb_palette: u8,
|
||||||
|
#[bits(1)]
|
||||||
|
/// Which VRAM bank (0-1) contains this object's tile?
|
||||||
|
cgb_bank: u8,
|
||||||
|
#[bits(1)]
|
||||||
|
/// Which palette (OBP 0-1) does this object use in DMG mode?
|
||||||
|
dmg_palette: u8,
|
||||||
|
/// Is this sprite flipped horizontally?
|
||||||
|
x_flip: bool,
|
||||||
|
/// Is this sprite flipped vertically?
|
||||||
|
y_flip: bool,
|
||||||
|
/// Is this sprite drawn below the window and background layers?
|
||||||
|
priority: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bitfield(u8)]
|
||||||
|
/// LCD main control register
|
||||||
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct LcdC {
|
||||||
|
/// In DMG mode, disables Window and Background layers.
|
||||||
|
/// In CGB mode, causes objects to render on top of Window and Background layers
|
||||||
|
bg_window_enable_or_priority: bool,
|
||||||
|
/// Controls whether objects are rendered or not
|
||||||
|
obj_enable: bool,
|
||||||
|
/// Controls the height of objects: false = 8px tall, true = 16px tall
|
||||||
|
obj_size: bool,
|
||||||
|
/// Controls bit 10 of the backgroud tile map address (false, true) => (0x9800, 0x9c00)
|
||||||
|
bg_tile_map_area: bool,
|
||||||
|
/// Controls which addressing mode to use when picking tiles for the window and background layers
|
||||||
|
bg_window_tile_data_area: bool,
|
||||||
|
/// Enables or disables the window
|
||||||
|
window_enable: bool,
|
||||||
|
/// Controls bit 10 of the window tile map address (false, true) => (0x9800, 0x9c00)
|
||||||
|
window_tile_map_area: bool,
|
||||||
|
/// Controls whether the LCD/PPU recieves power
|
||||||
|
lcd_ppu_enable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bitfield(u8)]
|
||||||
|
/// LCD Status and Interrupt Control register
|
||||||
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Stat {
|
||||||
|
#[bits(2)]
|
||||||
|
/// The current PPU operating [Mode]
|
||||||
|
mode: Mode,
|
||||||
|
/// Set to true when LYC == LY. Read only.
|
||||||
|
lyc_hit: bool,
|
||||||
|
#[bits(4)]
|
||||||
|
/// Configures PPU to raise IRQ when a condition is met
|
||||||
|
interrupts: Interrupts,
|
||||||
|
#[bits(default = true)]
|
||||||
|
_reserved: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bitfield(u8)]
|
||||||
|
/// [Stat] register interrupt configuration
|
||||||
|
pub struct Interrupts {
|
||||||
|
/// Configures PPU to raise IRQ on VBlank start
|
||||||
|
hblank: bool,
|
||||||
|
/// Configures PPU to raise IRQ on VBlank start
|
||||||
|
vblank: bool,
|
||||||
|
/// Configures PPU to raise IRQ on OAM Scan start
|
||||||
|
oam: bool,
|
||||||
|
/// Configures PPU to raise IRQ on rising edge of [lyc_hit](Stat::lyc_hit)
|
||||||
|
lyc: bool,
|
||||||
|
#[bits(4)]
|
||||||
|
_unused: (),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum DMGColor {
|
||||||
|
White,
|
||||||
|
LGray,
|
||||||
|
DGray,
|
||||||
|
Black,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DMGColor {
|
||||||
|
pub const fn from_bits(value: u8) -> Self {
|
||||||
|
match value & 3 {
|
||||||
|
0 => DMGColor::White,
|
||||||
|
1 => DMGColor::LGray,
|
||||||
|
2 => DMGColor::DGray,
|
||||||
|
3 => DMGColor::Black,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub const fn to_bits(self) -> u8 {
|
||||||
|
self as _
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod cpu;
|
||||||
|
|
||||||
|
pub mod audio {
|
||||||
|
use crate::memory::io::BusIO;
|
||||||
|
use bitfield_struct::bitfield;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct APU {
|
||||||
|
wave: [u8; 0x10],
|
||||||
|
amc: AMC,
|
||||||
|
pan: SndPan,
|
||||||
|
vol: Vol,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BusIO for APU {
|
||||||
|
fn read(&self, addr: usize) -> Option<u8> {
|
||||||
|
match addr {
|
||||||
|
0xff10 => todo!("Channel 1 sweep"),
|
||||||
|
0xff11 => todo!("Channel 1 length timer & duty cycle"),
|
||||||
|
0xff12 => todo!("Channel 1 volume & envelope"),
|
||||||
|
0xff13 => todo!("Channel 1 period low [write-only]"),
|
||||||
|
0xff14 => todo!("Channel 1 period high & control [trigger, length enable, period]"),
|
||||||
|
0xff24 => Some(self.vol.into_bits()),
|
||||||
|
0xff25 => Some(self.pan.into_bits()),
|
||||||
|
0xff26 => Some(self.amc.into_bits()),
|
||||||
|
0xff30..=0xff3f => self.wave.read(addr - 0xff30),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, addr: usize, data: u8) -> Option<()> {
|
||||||
|
#[allow(clippy::unit_arg)]
|
||||||
|
match addr {
|
||||||
|
0xff10..=0xff14 => todo!("Channel 1"),
|
||||||
|
0xff16..=0xff19 => todo!("Channel 2"),
|
||||||
|
0xff1a..=0xff1e => todo!("Channel 3"),
|
||||||
|
0xff20..=0xff23 => todo!("Channel 4"),
|
||||||
|
0xff24 => Some(self.vol = Vol::from_bits(data)),
|
||||||
|
// Sound panning
|
||||||
|
0xff25 => Some(self.pan = SndPan::from_bits(data)),
|
||||||
|
// Audio Master Control
|
||||||
|
0xff26 => Some(self.amc.set_power(data & 0x80 != 0)),
|
||||||
|
// Wave table memory
|
||||||
|
0xff30..=0xff3f => self.wave.write(addr - 0xff30, data),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bitfield(u8)]
|
||||||
|
/// Audio Master Control register
|
||||||
|
pub struct AMC {
|
||||||
|
/// Set when CH1 is playing. Read-only.
|
||||||
|
ch1_on: bool,
|
||||||
|
/// Set when CH2 is playing. Read-only.
|
||||||
|
ch2_on: bool,
|
||||||
|
/// Set when CH3 is playing. Read-only.
|
||||||
|
ch3_on: bool,
|
||||||
|
/// Set when CH4 is playing. Read-only.
|
||||||
|
ch4_on: bool,
|
||||||
|
#[bits(3)] // 3 bits are unused
|
||||||
|
_reserved: (),
|
||||||
|
/// If false, all channels are muted. Read-writable.
|
||||||
|
power: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bitfield(u8)]
|
||||||
|
/// Sound Panning register. Setting `cNS` to true will send channel `N` to speaker `S`
|
||||||
|
pub struct SndPan {
|
||||||
|
c1r: bool,
|
||||||
|
c2r: bool,
|
||||||
|
c3r: bool,
|
||||||
|
c4r: bool,
|
||||||
|
c1l: bool,
|
||||||
|
c2l: bool,
|
||||||
|
c3l: bool,
|
||||||
|
c4l: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bitfield(u8)]
|
||||||
|
/// Volume control and VIN panning register
|
||||||
|
pub struct Vol {
|
||||||
|
#[bits(3)]
|
||||||
|
/// The volume of the right output
|
||||||
|
right: u8,
|
||||||
|
/// Whether VIN is sent to the right output
|
||||||
|
vin_right: bool,
|
||||||
|
#[bits(3)]
|
||||||
|
/// The volume of the left output
|
||||||
|
left: u8,
|
||||||
|
/// Whether VIN is sent to the left output
|
||||||
|
vin_left: bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::memory::{io::*, Cart};
|
||||||
|
const TEST_ROMS: &[&str] = &[
|
||||||
|
// "roms/Legend of Zelda, The - Link's Awakening DX (USA, Europe) (Rev B) (SGB Enhanced).gbc",
|
||||||
|
// "roms/Pokemon - Crystal Version (USA, Europe) (Rev A).gbc",
|
||||||
|
// "roms/Pokemon Pinball (USA) (Rumble Version) (SGB Enhanced).gbc",
|
||||||
|
// "roms/Pokemon - Red Version (USA, Europe) (SGB Enhanced).gb",
|
||||||
|
"roms/Super Mario Land 2 - 6 Golden Coins (USA, Europe) (Rev B).gb",
|
||||||
|
"roms/Super Mario Land (World) (Rev A).gb",
|
||||||
|
"roms/Tetris (World) (Rev A).gb",
|
||||||
|
];
|
||||||
|
#[test]
|
||||||
|
/// Checksums the cart headers of every cart defined in [TEST_ROMS],
|
||||||
|
/// and compares them against the checksum stored in the header
|
||||||
|
fn cart_header_checksum() {
|
||||||
|
for rom in TEST_ROMS {
|
||||||
|
let rom = std::fs::read(rom).unwrap();
|
||||||
|
let cart = Cart::from(rom);
|
||||||
|
let mut cksum = 0u8;
|
||||||
|
for index in 0x134..0x14d {
|
||||||
|
cksum = cksum
|
||||||
|
.wrapping_sub(cart.read(index).unwrap())
|
||||||
|
.wrapping_sub(1)
|
||||||
|
}
|
||||||
|
assert_eq!(cksum, cart.headercksum().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
463
src/memory.rs
Normal file
463
src/memory.rs
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
//! RAM, CROM, and cartridge mappers
|
||||||
|
//!
|
||||||
|
//! # Gameboy Color memory map:
|
||||||
|
//! - `0000..4000` => ROM bank 0
|
||||||
|
//! - `4000..8000` => ROM banks 1-N (via [Mapper])
|
||||||
|
//! - `8000..A000` => VRAM Bank 0-1
|
||||||
|
//! - `A000..C000` => Cart RAM (via [Mapper])
|
||||||
|
//! - `C000..D000` => WRAM Bank 0
|
||||||
|
//! - `D000..E000` => WRAM Bank 1-7
|
||||||
|
//! - `E000..FE00` => ECHO RAM (WRAM Bank 0)
|
||||||
|
//! - `FE00..FEA0` => Object Attribute Memory (OAM)
|
||||||
|
//! - `FEA0..FF00` => Prohibited region (corrupts OAM on DMG)
|
||||||
|
//! - `FF00..FF80` => [Memory mapped I/O][MMIO]
|
||||||
|
//! - `FF80..FFFF` => HiRAM
|
||||||
|
//! - `FFFF` => Interrupt Enable Register
|
||||||
|
//!
|
||||||
|
//! [MMIO]: https://gbdev.io/pandocs/Hardware_Reg_List.html
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
banked::{Banked, UpperBanked},
|
||||||
|
io::BusIO,
|
||||||
|
mapper::Mapper,
|
||||||
|
};
|
||||||
|
pub mod banked;
|
||||||
|
pub mod io;
|
||||||
|
|
||||||
|
/// The bus operating mode
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Mode {
|
||||||
|
DMG,
|
||||||
|
CGB,
|
||||||
|
}
|
||||||
|
impl Mode {
|
||||||
|
pub fn from_bits(value: u8) -> Self {
|
||||||
|
match value & 0x40 {
|
||||||
|
0 => Self::DMG,
|
||||||
|
_ => Self::CGB,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn to_bits(self) -> u8 {
|
||||||
|
(self as u8) << 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Bus {
|
||||||
|
mode: Mode,
|
||||||
|
// TODO: define a way to have an arbitrary memory map
|
||||||
|
cart: Cart,
|
||||||
|
// VRAM is a 0x2000 B window from 0x8000..0xa000, with two banks
|
||||||
|
vram: Banked<0x8000, 0x2000, 2>,
|
||||||
|
// WRAM is a 0x2000 B window from 0xc000..0xe000. The upper half is banked.
|
||||||
|
wram: UpperBanked<0xc000, 0x1000, 8>,
|
||||||
|
// HRAM is a 0x80 B window from 0xff80..0xffff
|
||||||
|
hram: [u8; 0x80],
|
||||||
|
// Joypad driver
|
||||||
|
// Serial driver
|
||||||
|
// Timer and divider
|
||||||
|
// Interrupt controller
|
||||||
|
// Audio controller
|
||||||
|
// PPU/LCD controller
|
||||||
|
mmio: [u8; 0x80],
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BusIO for Bus {
|
||||||
|
fn read(&self, addr: usize) -> Option<u8> {
|
||||||
|
match addr {
|
||||||
|
// Cart ROM
|
||||||
|
0x0000..=0x7fff => self.cart.read(addr),
|
||||||
|
// VRAM
|
||||||
|
0x8000..=0x9fff => self.vram.read(addr),
|
||||||
|
// Cart RAM
|
||||||
|
0xa000..=0xbfff => self.cart.read(addr),
|
||||||
|
// Work RAM
|
||||||
|
0xc000..=0xdfff => self.wram.read(addr),
|
||||||
|
// Wram mirror
|
||||||
|
0xe000..=0xfdff => self.wram.read(addr - 0x2000),
|
||||||
|
// Graphics OAM
|
||||||
|
0xfe00..=0xfe9f => Some(0x0a),
|
||||||
|
// Illegal
|
||||||
|
0xfea0..=0xfeff => None,
|
||||||
|
// Memory mapped IO
|
||||||
|
0xff00..=0xff7f => self.mmio.read(addr & 0x7f),
|
||||||
|
// HiRAM
|
||||||
|
0xff80..=0xffff => self.hram.read(addr & 0x7f),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, addr: usize, data: u8) -> Option<()> {
|
||||||
|
match addr {
|
||||||
|
// Cart ROM
|
||||||
|
0x0000..=0x7fff => self.cart.write(addr, data),
|
||||||
|
// VRAM
|
||||||
|
0x8000..=0x9fff => self.vram.write(addr, data),
|
||||||
|
// Cart RAM
|
||||||
|
0xa000..=0xbfff => self.cart.write(addr, data),
|
||||||
|
// Work RAM
|
||||||
|
0xc000..=0xdfff => self.wram.write(addr, data),
|
||||||
|
// Wram mirror
|
||||||
|
0xe000..=0xfdff => self.wram.write(addr - 0x2000, data),
|
||||||
|
// Graphics OAM
|
||||||
|
0xfe00..=0xfe9f => Some(()),
|
||||||
|
// Illegal
|
||||||
|
0xfea0..=0xfeff => None,
|
||||||
|
// Memory mapped IO
|
||||||
|
0xff00..=0xff7f => self.mmio.write(addr & 0x7f, data),
|
||||||
|
// HiRAM
|
||||||
|
0xff80..=0xffff => self.hram.write(addr & 0x7f, data),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bus {
|
||||||
|
pub fn new(cart: Cart) -> Self {
|
||||||
|
Self {
|
||||||
|
cart,
|
||||||
|
mode: Mode::CGB,
|
||||||
|
vram: Default::default(),
|
||||||
|
wram: Default::default(),
|
||||||
|
mmio: [0; 128],
|
||||||
|
hram: [0; 128],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn mmio_read(&self, addr: usize) -> Option<u8> {
|
||||||
|
match addr {
|
||||||
|
// Joypad I/O
|
||||||
|
0xff00 => eprintln!("DMG Joypad output"),
|
||||||
|
// DMG/CGB Serial transfer
|
||||||
|
0xff01..=0xff02 => eprintln!("DMG Serial transfer"),
|
||||||
|
// DMG Timer and divider
|
||||||
|
0xff04..=0xff07 => eprintln!("DMG Timer and divider"),
|
||||||
|
// Interrupt controller (IF)
|
||||||
|
0xff0f => eprintln!("Interrupt controller"),
|
||||||
|
// DMG Audio
|
||||||
|
0xff10..=0xff26 => eprintln!("DMG Audio"),
|
||||||
|
// DMG Wave pattern memory
|
||||||
|
0xff30..=0xff3f => eprintln!("DMG Wave pattern memory"),
|
||||||
|
// DMG LCD control, status, position, scrolling, and palettes
|
||||||
|
0xff40..=0xff4b => {
|
||||||
|
eprintln!("DMG LCD control, status, position, scrolling, and palettes")
|
||||||
|
}
|
||||||
|
// Disable bootrom
|
||||||
|
0xff50 => eprintln!("Set to non-zero to disable bootrom"),
|
||||||
|
_ => match self.mode {
|
||||||
|
Mode::CGB => {
|
||||||
|
if let Some(data) = self.mmio_read_cgb(addr) {
|
||||||
|
return Some(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Mode::DMG => {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.mmio.get(addr % 0x80).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mmio_read_cgb(&self, addr: usize) -> Option<u8> {
|
||||||
|
match addr {
|
||||||
|
// CGB Key1 speed switch
|
||||||
|
0xff4d => eprintln!("CGB Key1"),
|
||||||
|
// CGB VRam bank select
|
||||||
|
0xff4f => return Some(self.vram.selected() as u8 | 0xfc),
|
||||||
|
// CGB Vram DMA controller
|
||||||
|
0xff51..=0xff55 => eprintln!("CGB Vram DMA"),
|
||||||
|
// CGB Infrared
|
||||||
|
0xff56 => eprintln!("Infrared port"),
|
||||||
|
// CGB BG/OBJ palettes
|
||||||
|
0xff68..=0xff6b => eprintln!("CGB BG/Obj palettes"),
|
||||||
|
// CGB WRam bank select
|
||||||
|
0xff70 => return Some(self.wram.selected() as u8 | 0xfe),
|
||||||
|
// CGB Undocumented registers
|
||||||
|
0xff72..=0xff75 => return Some(00),
|
||||||
|
// CGB PCM Read registers
|
||||||
|
0xff76..=0xff77 => eprintln!("CGB PCM Read Registers"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mmio_write(&mut self, addr: usize, data: u8) -> Option<()> {
|
||||||
|
match addr {
|
||||||
|
// Joypad I/O
|
||||||
|
0xff00 => eprintln!("DMG Joypad output"),
|
||||||
|
// DMG/CGB Serial transfer
|
||||||
|
0xff01..=0xff02 => eprintln!("DMG Serial transfer"),
|
||||||
|
// DMG Timer and divider
|
||||||
|
0xff04..=0xff07 => eprintln!("DMG Timer and divider"),
|
||||||
|
// Interrupt controller (IF)
|
||||||
|
0xff0f => eprintln!("Interrupt controller"),
|
||||||
|
// DMG Audio
|
||||||
|
0xff10..=0xff26 => eprintln!("DMG Audio"),
|
||||||
|
// DMG Wave pattern memory
|
||||||
|
0xff30..=0xff3f => eprintln!("DMG Wave pattern memory"),
|
||||||
|
// DMG LCD control, status, position, scrolling, and palettes
|
||||||
|
0xff40..=0xff4b => {
|
||||||
|
eprintln!("DMG LCD control, status, position, scrolling, and palettes")
|
||||||
|
}
|
||||||
|
// Disable bootrom
|
||||||
|
0xff50 => todo!("Set to non-zero to disable bootrom"),
|
||||||
|
_ => match self.mode {
|
||||||
|
Mode::CGB => {
|
||||||
|
self.mmio_write_cgb(addr, data);
|
||||||
|
}
|
||||||
|
Mode::DMG => {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.mmio.write(addr % 0x80, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mmio_write_cgb(&mut self, addr: usize, data: u8) -> Option<()> {
|
||||||
|
match addr {
|
||||||
|
// CGB Key1 speed switch
|
||||||
|
0xff4d => eprintln!("CGB Key1"),
|
||||||
|
// CGB VRam bank select
|
||||||
|
0xff4f => {
|
||||||
|
self.vram.select(data as usize);
|
||||||
|
return Some(());
|
||||||
|
}
|
||||||
|
// CGB Vram DMA controller
|
||||||
|
0xff51..=0xff55 => eprintln!("CGB Vram DMA"),
|
||||||
|
// CGB Infrared
|
||||||
|
0xff56 => eprintln!("Infrared port"),
|
||||||
|
// CGB BG/OBJ palettes
|
||||||
|
0xff68..=0xff6b => eprintln!("CGB BG/Obj palettes"),
|
||||||
|
// CGB WRam bank select
|
||||||
|
0xff70 => {
|
||||||
|
self.wram.select(data as usize);
|
||||||
|
return Some(());
|
||||||
|
}
|
||||||
|
// CGB Undocumented registers
|
||||||
|
0xff72..=0xff75 => return Some(()),
|
||||||
|
// CGB PCM Read registers
|
||||||
|
0xff76..=0xff77 => eprintln!("CGB PCM Read Registers"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Cart {
|
||||||
|
pub rom: Vec<u8>,
|
||||||
|
pub ram: Vec<u8>,
|
||||||
|
mapper: Box<dyn Mapper>,
|
||||||
|
}
|
||||||
|
|
||||||
|
mod cart {
|
||||||
|
use crate::memory::io::BusAux;
|
||||||
|
|
||||||
|
use super::{mapper::*, BusIO, Cart};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
impl BusIO for Cart {
|
||||||
|
fn read(&self, addr: usize) -> Option<u8> {
|
||||||
|
match self.mapper.read(addr) {
|
||||||
|
Response::None => None,
|
||||||
|
Response::Data(data) => Some(data),
|
||||||
|
Response::Ram(addr) => self.ram.get(addr).copied(),
|
||||||
|
Response::Rom(addr) => self.rom.get(addr).copied(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, addr: usize, data: u8) -> Option<()> {
|
||||||
|
match self.mapper.write(addr, data) {
|
||||||
|
Response::None => None,
|
||||||
|
Response::Data(_) => Some(()),
|
||||||
|
Response::Ram(addr) => self.ram.get_mut(addr).map(|rambyte| *rambyte = data),
|
||||||
|
Response::Rom(addr) => {
|
||||||
|
panic!("Attempted to write to ROM address :{addr:x}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Read cartridge header, as defined in the [Pan docs](https://gbdev.io/pandocs/The_Cartridge_Header.html)
|
||||||
|
impl Cart {
|
||||||
|
pub fn new(rom: Vec<u8>) -> Self {
|
||||||
|
let rom_size = rom.romsize().expect("ROM should have cart header!");
|
||||||
|
let ram_size = rom.ramsize().expect("ROM should have cart header!");
|
||||||
|
let cart_type = rom.carttype().expect("ROM should have cart header!");
|
||||||
|
Self {
|
||||||
|
mapper: match cart_type {
|
||||||
|
0 => Box::<no_mbc::NoMBC>::default(),
|
||||||
|
1 => Box::new(mbc1::MBC1::new(rom_size, 0)), // ROM
|
||||||
|
2 => Box::new(mbc1::MBC1::new(rom_size, ram_size)), // ROM + RAM
|
||||||
|
3 => Box::new(mbc1::MBC1::new(rom_size, ram_size)), // ROM + RAM + Battery
|
||||||
|
// _ => Box::<NoMBC>::default(),
|
||||||
|
n => todo!("Mapper 0x{n:02x}"),
|
||||||
|
},
|
||||||
|
rom,
|
||||||
|
ram: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Cart {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Cart")
|
||||||
|
.field("rom", &self.rom)
|
||||||
|
.field("ram", &self.ram)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<u8>> for Cart {
|
||||||
|
fn from(value: Vec<u8>) -> Self {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<&[u8]> for Cart {
|
||||||
|
fn from(value: &[u8]) -> Self {
|
||||||
|
Self::new(Vec::from(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Item: AsRef<u8>> FromIterator<Item> for Cart {
|
||||||
|
fn from_iter<T: IntoIterator<Item = Item>>(iter: T) -> Self {
|
||||||
|
Self::new(iter.into_iter().map(|item| *(item.as_ref())).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Cart {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Entry: {:02x?}", self.entry())?;
|
||||||
|
// if let Some(logo) = self.logo() {
|
||||||
|
// write!(f, "Logo: ")?;
|
||||||
|
// for byte in logo {
|
||||||
|
// write!(f, "{byte:02x}")?;
|
||||||
|
// }
|
||||||
|
// writeln!(f)?;
|
||||||
|
// }
|
||||||
|
write!(f, "Title: {:02x?}", self.title())?;
|
||||||
|
write!(f, "MFCode: {:02x?}", self.mfcode())?;
|
||||||
|
write!(f, "CGB Flag: {:02x?}", self.cgbflag())?;
|
||||||
|
write!(f, "New Lic: {:02x?}", self.newlicensee())?;
|
||||||
|
write!(f, "SGB Flag: {:02x?}", self.sgbflag())?;
|
||||||
|
write!(f, "Cart Type: {:02x?}", self.carttype())?;
|
||||||
|
write!(f, "ROM Size: {:02x?}", self.romsize())?;
|
||||||
|
write!(f, "RAM Size: {:02x?}", self.ramsize())?;
|
||||||
|
write!(f, "Dest Code: {:02x?}", self.destcode())?;
|
||||||
|
write!(f, "Old Lic: {:02x?}", self.oldlicencee())?;
|
||||||
|
write!(f, "ROM Ver: {:02x?}", self.maskromver())?;
|
||||||
|
write!(f, "H Cksum: {:02x?}", self.headercksum())?;
|
||||||
|
write!(f, "G Cksum: {:04x?}", self.globalcksum())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod mapper {
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum Response {
|
||||||
|
/// No data to return
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
/// [Mapper] returned some data directly
|
||||||
|
Data(u8),
|
||||||
|
/// [Mapper] mapped incoming address to cart RAM
|
||||||
|
Ram(usize),
|
||||||
|
/// [Mapper] mapped incoming address to cart ROM
|
||||||
|
Rom(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Mapper {
|
||||||
|
fn read(&self, addr: usize) -> Response;
|
||||||
|
fn write(&mut self, addr: usize, val: u8) -> Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod no_mbc {
|
||||||
|
use super::*;
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct NoMBC;
|
||||||
|
impl Mapper for NoMBC {
|
||||||
|
fn read(&self, addr: usize) -> Response {
|
||||||
|
match addr {
|
||||||
|
0x0000..=0x7fff => Response::Rom(addr),
|
||||||
|
0xa000..=0xbfff => Response::Ram(addr - 0x8000),
|
||||||
|
_ => Response::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, addr: usize, _val: u8) -> Response {
|
||||||
|
match addr {
|
||||||
|
0xa000..=0xbfff => Response::Ram(addr - 0xA000),
|
||||||
|
_ => Response::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod mbc1 {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct MBC1 {
|
||||||
|
rom_mask: u8,
|
||||||
|
ram_mask: u8,
|
||||||
|
mode: u8,
|
||||||
|
rom_bank: u8,
|
||||||
|
ram_bank: u8,
|
||||||
|
ram_enable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MBC1 {
|
||||||
|
pub fn new(rom_size: usize, ram_size: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
rom_mask: (rom_size).wrapping_sub(1) as u8,
|
||||||
|
ram_mask: (ram_size).wrapping_sub(1) as u8,
|
||||||
|
rom_bank: 1,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn rom_bank_lower(&self) -> usize {
|
||||||
|
(((self.ram_bank << 5) & self.rom_mask) as usize) << 14
|
||||||
|
}
|
||||||
|
pub fn rom_bank_upper(&self) -> usize {
|
||||||
|
((self.rom_bank | (self.ram_bank << 5) & self.rom_mask) as usize) << 14
|
||||||
|
}
|
||||||
|
pub fn ram_bank_base(&self) -> usize {
|
||||||
|
((self.ram_bank & self.ram_mask) as usize) << 13
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for MBC1 {
|
||||||
|
fn read(&self, addr: usize) -> Response {
|
||||||
|
match (addr, self.mode) {
|
||||||
|
(0x0000..=0x3fff, 0) => Response::Rom(addr),
|
||||||
|
(0x0000..=0x3fff, _) => Response::Rom(self.rom_bank_lower() | (addr & 0x3fff)),
|
||||||
|
|
||||||
|
(0x4000..=0x7fff, _) => Response::Rom(self.rom_bank_upper() | (addr & 0x3fff)),
|
||||||
|
|
||||||
|
(0xa000..=0xbfff, 0) => Response::Ram(addr & 0x1fff),
|
||||||
|
(0xa000..=0xbfff, _) => Response::Ram(self.ram_bank_base() | (addr & 0x1fff)),
|
||||||
|
|
||||||
|
_ => Response::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, addr: usize, val: u8) -> Response {
|
||||||
|
match (addr, self.mode) {
|
||||||
|
(0x0000..=0x1fff, _) => {
|
||||||
|
self.ram_enable = val & 0xf == 0xa;
|
||||||
|
}
|
||||||
|
(0x2000..=0x3fff, _) => {
|
||||||
|
self.rom_bank = (val & 0x1f).max(1);
|
||||||
|
}
|
||||||
|
(0x4000..=0x5fff, _) => {
|
||||||
|
self.ram_bank = val & 0x3;
|
||||||
|
}
|
||||||
|
(0x6000..=0x7fff, _) => {
|
||||||
|
self.mode = val & 1;
|
||||||
|
}
|
||||||
|
(0xa000..=0xbfff, 0) if self.ram_enable => {
|
||||||
|
return Response::Ram(addr & 0x1fff);
|
||||||
|
}
|
||||||
|
(0xa000..=0xbfff, 1) if self.ram_enable => {
|
||||||
|
return Response::Ram((self.ram_bank as usize & 0b11 << 13) | addr & 0x1fff)
|
||||||
|
}
|
||||||
|
_ => return Response::None,
|
||||||
|
}
|
||||||
|
Response::Data(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
167
src/memory/banked.rs
Normal file
167
src/memory/banked.rs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
//! Shares the upper half of a memory region among a number of memory banks
|
||||||
|
use super::io::BusIO;
|
||||||
|
|
||||||
|
/// Shares an entire memory region from BASE to BASE + LEN among multiple banks
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Banked<const BASE: usize, const LEN: usize, const BANKS: usize> {
|
||||||
|
selected: usize,
|
||||||
|
mem: [[u8; LEN]; BANKS],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const BASE: usize, const LEN: usize, const BANKS: usize> Banked<BASE, LEN, BANKS> {
|
||||||
|
/// Creates a new [Banked<BASE, LEN, BANKS>](Banked)
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
/// Selects a bank between 0 and `LEN` - 1, saturating
|
||||||
|
pub fn select(&mut self, bank: usize) {
|
||||||
|
self.selected = bank.min(BANKS - 1)
|
||||||
|
}
|
||||||
|
pub fn selected(&self) -> usize {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const BASE: usize, const LEN: usize, const BANKS: usize> Default for Banked<BASE, LEN, BANKS> {
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { selected: 0, mem: [[0; LEN]; BANKS] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const BASE: usize, const LEN: usize, const BANKS: usize> BusIO for Banked<BASE, LEN, BANKS> {
|
||||||
|
fn read(&self, addr: usize) -> Option<u8> {
|
||||||
|
let addr = addr.checked_sub(BASE).filter(|v| *v < LEN)?;
|
||||||
|
self.mem[self.selected].read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, addr: usize, data: u8) -> Option<()> {
|
||||||
|
let addr = addr.checked_sub(BASE).filter(|v| *v < LEN)?;
|
||||||
|
self.mem[self.selected].write(addr, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const A: usize, const LEN: usize, const BANKS: usize> AsRef<[u8]> for Banked<A, LEN, BANKS> {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
//! Adapted from [core::slice::flatten](https://doc.rust-lang.org/std/primitive.slice.html#method.flatten)
|
||||||
|
let slice = self.mem.as_slice().as_ptr().cast();
|
||||||
|
// SAFETY:
|
||||||
|
// - `LEN * BANKS` cannot overflow because `self` is already in the address space.
|
||||||
|
// - `[T]` is layout-identical to `[T; N]`
|
||||||
|
unsafe { core::slice::from_raw_parts(slice, LEN * BANKS) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const A: usize, const LEN: usize, const BANKS: usize> AsMut<[u8]> for Banked<A, LEN, BANKS> {
|
||||||
|
fn as_mut(&mut self) -> &mut [u8] {
|
||||||
|
let slice = self.mem.as_mut_slice().as_mut_ptr().cast();
|
||||||
|
// SAFETY: see [Banked::as_ref]
|
||||||
|
unsafe { core::slice::from_raw_parts_mut(slice, LEN * BANKS) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------- //
|
||||||
|
|
||||||
|
/// Shares the region from `BASE` + `LEN` to `BASE` + 2 * `LEN` among `BANKS`-1 banks,
|
||||||
|
/// while keeping the lower bank constant
|
||||||
|
/// # Constants:
|
||||||
|
/// - `BASE`: The location where the [UpperBanked] begins
|
||||||
|
/// - `LEN`: The length of one bank. This is *half* of the in-memory length,
|
||||||
|
/// - `BANKS`: The number of banks
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct UpperBanked<const BASE: usize, const LEN: usize, const BANKS: usize> {
|
||||||
|
selected: usize,
|
||||||
|
mem: [[u8; LEN]; BANKS],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const BASE: usize, const LEN: usize, const BANKS: usize> UpperBanked<BASE, LEN, BANKS> {
|
||||||
|
/// Creates a new [`UpperBanked<BASE, LEN, BANKS>`](Banked)
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
/// Selects a bank between 1 and `LEN` - 1, saturating to `LEN` - 1
|
||||||
|
pub fn select(&mut self, bank: usize) {
|
||||||
|
self.selected = bank.clamp(1, BANKS - 1)
|
||||||
|
}
|
||||||
|
/// Gets the currently selected upper bank
|
||||||
|
pub fn selected(&self) -> usize {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const BASE: usize, const LEN: usize, const BANKS: usize> Default
|
||||||
|
for UpperBanked<BASE, LEN, BANKS>
|
||||||
|
{
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { selected: 1, mem: [[0; LEN]; BANKS] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const BASE: usize, const LEN: usize, const BANKS: usize> BusIO
|
||||||
|
for UpperBanked<BASE, LEN, BANKS>
|
||||||
|
{
|
||||||
|
fn read(&self, addr: usize) -> Option<u8> {
|
||||||
|
// Pray this gets optimized
|
||||||
|
let addr = addr.checked_sub(BASE).filter(|v| *v < LEN * 2)?;
|
||||||
|
let (bank, addr) = (addr / LEN, addr % LEN);
|
||||||
|
match bank {
|
||||||
|
0 => self.mem[0].read(addr),
|
||||||
|
_ => self.mem[self.selected].read(addr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, addr: usize, data: u8) -> Option<()> {
|
||||||
|
// Pray this gets optimized
|
||||||
|
let addr = addr.checked_sub(BASE).filter(|v| *v < LEN * 2)?;
|
||||||
|
let (bank, addr) = (addr / LEN, addr % LEN);
|
||||||
|
match bank {
|
||||||
|
0 => self.mem[0].write(addr, data),
|
||||||
|
_ => self.mem[self.selected].write(addr, data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const A: usize, const LEN: usize, const BANKS: usize> AsRef<[u8]>
|
||||||
|
for UpperBanked<A, LEN, BANKS>
|
||||||
|
{
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
let slice_ptr = self.mem.as_slice().as_ptr().cast();
|
||||||
|
// SAFETY: see [Banked::as_ref]
|
||||||
|
unsafe { core::slice::from_raw_parts(slice_ptr, LEN * BANKS) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const A: usize, const LEN: usize, const BANKS: usize> AsMut<[u8]>
|
||||||
|
for UpperBanked<A, LEN, BANKS>
|
||||||
|
{
|
||||||
|
fn as_mut(&mut self) -> &mut [u8] {
|
||||||
|
let slice_ptr = self.mem.as_mut_slice().as_mut_ptr().cast();
|
||||||
|
// SAFETY: see [UpperBanked::as_ref]
|
||||||
|
unsafe { core::slice::from_raw_parts_mut(slice_ptr, LEN * BANKS) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::UpperBanked;
|
||||||
|
#[test]
|
||||||
|
fn upper_banked_asref_slice() {
|
||||||
|
let mut ub = UpperBanked::<0, 10, 20>::new();
|
||||||
|
let mut counter = 0u8;
|
||||||
|
for bank in &mut ub.mem {
|
||||||
|
for addr in bank {
|
||||||
|
*addr = counter;
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (idx, data) in ub.as_ref().iter().enumerate() {
|
||||||
|
assert_eq!(idx, *data as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn upper_banked_bounds() {
|
||||||
|
let ub = UpperBanked::<0, 8, 32>::new();
|
||||||
|
assert_eq!(ub.as_ref().len(), 8 * 32);
|
||||||
|
}
|
||||||
|
}
|
222
src/memory/io.rs
Normal file
222
src/memory/io.rs
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
//! Trait for transferring data to and from a CPU
|
||||||
|
|
||||||
|
pub mod iter {
|
||||||
|
//! Iterates over the bytes in a [BusIO]
|
||||||
|
use super::BusIO;
|
||||||
|
|
||||||
|
pub struct Iter<'a> {
|
||||||
|
start: usize,
|
||||||
|
bus: &'a dyn BusIO,
|
||||||
|
}
|
||||||
|
impl<'a> Iter<'a> {
|
||||||
|
pub fn new(bus: &'a dyn BusIO) -> Self {
|
||||||
|
Self { start: 0, bus }
|
||||||
|
}
|
||||||
|
pub fn start_at(bus: &'a dyn BusIO, start: usize) -> Self {
|
||||||
|
Self { start, bus }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> Iterator for Iter<'a> {
|
||||||
|
type Item = u8;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let Self { start, bus } = self;
|
||||||
|
let out = bus.read(*start);
|
||||||
|
if out.is_some() {
|
||||||
|
*start += 1;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for transferring data to and from a CPU
|
||||||
|
pub trait BusIO {
|
||||||
|
// required functions
|
||||||
|
/// Attempts to read a byte from self, returning None if the operation failed.
|
||||||
|
fn read(&self, addr: usize) -> Option<u8>;
|
||||||
|
/// Attempts to write a byte to self, returning None if the operation failed.
|
||||||
|
fn write(&mut self, addr: usize, data: u8) -> Option<()>;
|
||||||
|
|
||||||
|
/// Does some implementation-specific debug function
|
||||||
|
fn diag(&mut self, _param: usize) {}
|
||||||
|
|
||||||
|
/// Gets an iterator which reads the bytes from self
|
||||||
|
fn read_iter(&self) -> iter::Iter
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
iter::Iter::new(self)
|
||||||
|
}
|
||||||
|
/// Gets an iterator which reads bytes in an exclusive range
|
||||||
|
fn read_iter_from(&self, start: usize) -> iter::Iter
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
iter::Iter::start_at(self, start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> BusIO for [u8; N] {
|
||||||
|
fn read(&self, addr: usize) -> Option<u8> {
|
||||||
|
self.get(addr).copied()
|
||||||
|
}
|
||||||
|
fn write(&mut self, addr: usize, data: u8) -> Option<()> {
|
||||||
|
self.get_mut(addr).map(|v| *v = data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BusIO for [u8] {
|
||||||
|
fn read(&self, addr: usize) -> Option<u8> {
|
||||||
|
self.get(addr).copied()
|
||||||
|
}
|
||||||
|
fn write(&mut self, addr: usize, data: u8) -> Option<()> {
|
||||||
|
self.get_mut(addr).map(|v| *v = data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BusIO for Vec<u8> {
|
||||||
|
fn read(&self, addr: usize) -> Option<u8> {
|
||||||
|
self.get(addr).copied()
|
||||||
|
}
|
||||||
|
fn write(&mut self, addr: usize, data: u8) -> Option<()> {
|
||||||
|
self.get_mut(addr).map(|v| *v = data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: BusIO> BusAux for T {}
|
||||||
|
pub trait BusAux: BusIO {
|
||||||
|
// derived functions
|
||||||
|
/// Copies a contiguous region from the bus. Returns an incomplete copy on error.
|
||||||
|
fn read_arr<const N: usize>(&self, addr: usize) -> Result<[u8; N], [u8; N]> {
|
||||||
|
let mut out = [0; N];
|
||||||
|
for (idx, byte) in out.iter_mut().enumerate() {
|
||||||
|
match self.read(addr + idx) {
|
||||||
|
Some(value) => *byte = value,
|
||||||
|
None => return Err(out),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
/// Gets the cart entrypoint bytes (machine code starting at 0x100)
|
||||||
|
fn entry(&self) -> Result<[u8; 4], [u8; 4]> {
|
||||||
|
self.read_arr(0x100)
|
||||||
|
}
|
||||||
|
/// Gets the logo bytes (0x104..0x134)
|
||||||
|
fn logo(&self) -> Result<[u8; 0x30], [u8; 0x30]> {
|
||||||
|
self.read_arr(0x104)
|
||||||
|
}
|
||||||
|
/// Gets the title bytes as a String
|
||||||
|
/// # Note
|
||||||
|
/// The title area was repartitioned for other purposes during the Game Boy's life.
|
||||||
|
/// See [mfcode](BusAux::mfcode) and [cgbflag](BusAux::cgbflag).
|
||||||
|
fn title(&self) -> Result<String, [u8; 0x10]> {
|
||||||
|
// Safety: Chars are checked to be valid ascii before cast to char
|
||||||
|
Ok(self
|
||||||
|
.read_arr(0x134)? // to 0x144
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.take_while(|c| *c != 0 && c.is_ascii())
|
||||||
|
.map(|c| unsafe { char::from_u32_unchecked(c as u32) })
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
/// Gets the manufacturer code bytes
|
||||||
|
fn mfcode(&self) -> Result<[u8; 4], [u8; 4]> {
|
||||||
|
self.read_arr(0x13f)
|
||||||
|
}
|
||||||
|
/// Gets the CGB Flag byte
|
||||||
|
fn cgbflag(&self) -> Option<u8> {
|
||||||
|
self.read(0x143)
|
||||||
|
}
|
||||||
|
/// Gets the New Licensee bytes
|
||||||
|
fn newlicensee(&self) -> Result<[u8; 2], [u8; 2]> {
|
||||||
|
self.read_arr(0x144)
|
||||||
|
}
|
||||||
|
/// Gets the SGB Flag byte
|
||||||
|
fn sgbflag(&self) -> Option<u8> {
|
||||||
|
self.read(0x146)
|
||||||
|
}
|
||||||
|
/// Gets the Cartridge Type byte
|
||||||
|
fn carttype(&self) -> Option<u8> {
|
||||||
|
self.read(0x147)
|
||||||
|
}
|
||||||
|
/// Gets the size of the cartridge ROM (in 0x8000 byte banks)
|
||||||
|
fn romsize(&self) -> Option<usize> {
|
||||||
|
self.read(0x148).map(|s| 1usize.wrapping_shl(s as u32 + 1))
|
||||||
|
}
|
||||||
|
/// Gets the size of the cartridge RAM (in 0x8000 byte banks)
|
||||||
|
fn ramsize(&self) -> Option<usize> {
|
||||||
|
self.read(0x149).map(|s| match s {
|
||||||
|
0x02 => 1,
|
||||||
|
0x03 => 4,
|
||||||
|
0x04 => 16,
|
||||||
|
0x05 => 8,
|
||||||
|
_ => 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Gets the destination region code
|
||||||
|
fn destcode(&self) -> Option<u8> {
|
||||||
|
self.read(0x14a)
|
||||||
|
}
|
||||||
|
/// Gets the old licensee byte
|
||||||
|
fn oldlicencee(&self) -> Option<u8> {
|
||||||
|
self.read(0x14b)
|
||||||
|
}
|
||||||
|
/// Gets the Maskrom Version byte. Usually 0
|
||||||
|
fn maskromver(&self) -> Option<u8> {
|
||||||
|
self.read(0x14c)
|
||||||
|
}
|
||||||
|
/// Gets the header checksum
|
||||||
|
fn headercksum(&self) -> Option<u8> {
|
||||||
|
self.read(0x14d)
|
||||||
|
}
|
||||||
|
/// Gets the checksum of all bytes in ROM
|
||||||
|
fn globalcksum(&self) -> Option<u16> {
|
||||||
|
self.read_arr(0x14e).ok().map(u16::from_be_bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod interrupt {
|
||||||
|
use super::BusIO;
|
||||||
|
use bitfield_struct::bitfield;
|
||||||
|
|
||||||
|
/// Interrupt Enable register
|
||||||
|
const IE: usize = 0xffff;
|
||||||
|
/// Interrupt Flags register
|
||||||
|
const IF: usize = 0xff0f;
|
||||||
|
|
||||||
|
#[bitfield(u8)]
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
pub struct Interrupt {
|
||||||
|
vblank: bool,
|
||||||
|
lcd: bool,
|
||||||
|
timer: bool,
|
||||||
|
serial: bool,
|
||||||
|
joypad: bool,
|
||||||
|
#[bits(3)]
|
||||||
|
_reserved: (),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: BusIO> Interrupts for B {}
|
||||||
|
/// Functionality for working with [Interrupt]s
|
||||||
|
pub trait Interrupts: BusIO {
|
||||||
|
/// Gets the set of enabled [Interrupt]s
|
||||||
|
fn enabled(&self) -> Option<Interrupt> {
|
||||||
|
self.read(IE).map(Interrupt::from_bits)
|
||||||
|
}
|
||||||
|
/// Gets the set of raised [Interrupt]s
|
||||||
|
#[inline]
|
||||||
|
fn raised(&self) -> Option<Interrupt> {
|
||||||
|
self.read(IF).map(Interrupt::from_bits)
|
||||||
|
}
|
||||||
|
/// Raises [Interrupt]s with the provided mask.
|
||||||
|
fn raise(&mut self, int: Interrupt) {
|
||||||
|
let flags = self.raised().unwrap_or_default();
|
||||||
|
let _ = self.write(IF, flags.into_bits() | int.into_bits());
|
||||||
|
}
|
||||||
|
/// Clears [Interrupt]s with the provided mask.
|
||||||
|
fn clear(&mut self, int: Interrupt) {
|
||||||
|
let flags = self.raised().unwrap_or_default();
|
||||||
|
let _ = self.write(IF, flags.into_bits() & !int.into_bits());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user