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