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:
John 2024-06-22 07:25:59 -05:00
commit acfbb8f1cf
16 changed files with 4646 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/target
# Test ROMs
/roms
**.gb
# VSCode
.vscode

10
Cargo.toml Normal file
View 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
View File

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

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

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

View File

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

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

File diff suppressed because it is too large Load Diff

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

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

9
boy-utils/Cargo.toml Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

654
src/cpu/disasm.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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());
}
}
}