v0.1.1 Partial Super Chip Support #11
11
justfile
11
justfile
@ -7,11 +7,14 @@ rat:
|
|||||||
test:
|
test:
|
||||||
cargo nextest run
|
cargo nextest run
|
||||||
|
|
||||||
chirp:
|
run rom:
|
||||||
cargo run --bin chirp-minifb -- chip8-test-suite/bin/chip8-test-suite.ch8
|
cargo run --bin chirp-minifb -- '{{rom}}'
|
||||||
|
|
||||||
|
debug rom:
|
||||||
|
cargo run --bin chirp-minifb -- -d '{{rom}}'
|
||||||
# Run at 2100000 instructions per frame, and output per-frame runtime statistics
|
# Run at 2100000 instructions per frame, and output per-frame runtime statistics
|
||||||
bench:
|
bench:
|
||||||
cargo run --bin chirp-minifb --release -- chip8Archive/roms/1dcell.ch8 -xP -s10 -S2100000
|
cargo run --bin chirp-minifb --release -- chip8Archive/roms/1dcell.ch8 -Ps10 -S2100000 -m xochip
|
||||||
|
|
||||||
flame rom:
|
flame rom:
|
||||||
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -F 15300 --open --bin chirp-minifb -- '{{rom}}' -s10
|
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -F 15300 --open --bin chirp-minifb -- '{{rom}}' -s10
|
||||||
@ -23,5 +26,5 @@ cover:
|
|||||||
cargo llvm-cov --open --doctests
|
cargo llvm-cov --open --doctests
|
||||||
|
|
||||||
tokei:
|
tokei:
|
||||||
tokei --exclude tests/chip8-test-suite
|
tokei --exclude chip8-test-suite --exclude chip8Archive
|
||||||
|
|
||||||
|
@ -36,33 +36,39 @@ struct Arguments {
|
|||||||
pub step: Option<usize>,
|
pub step: Option<usize>,
|
||||||
#[options(help = "Enable performance benchmarking on stderr (requires -S)")]
|
#[options(help = "Enable performance benchmarking on stderr (requires -S)")]
|
||||||
pub perf: bool,
|
pub perf: bool,
|
||||||
#[options(
|
|
||||||
short = "z",
|
|
||||||
help = "Disable setting vF to 0 after a bitwise operation."
|
|
||||||
)]
|
|
||||||
pub vfreset: bool,
|
|
||||||
#[options(
|
|
||||||
short = "x",
|
|
||||||
help = "Disable waiting for vblank after issuing a draw call."
|
|
||||||
)]
|
|
||||||
pub drawsync: bool,
|
|
||||||
|
|
||||||
#[options(
|
#[options(
|
||||||
short = "c",
|
help = "Run in (Chip8, SChip, XOChip) mode.",
|
||||||
help = "Use CHIP-48 style DMA instructions, which don't touch I."
|
//parse(from_str = "parse_mode")
|
||||||
)]
|
)]
|
||||||
pub memory: bool,
|
pub mode: Option<Mode>,
|
||||||
#[options(
|
|
||||||
short = "v",
|
|
||||||
help = "Use CHIP-48 style bit-shifts, which don't touch vY."
|
|
||||||
)]
|
|
||||||
pub shift: bool,
|
|
||||||
#[options(
|
|
||||||
short = "b",
|
|
||||||
help = "Use SUPER-CHIP style indexed jump, which is indexed relative to v[adr]."
|
|
||||||
)]
|
|
||||||
pub jumping: bool,
|
|
||||||
|
|
||||||
|
// #[options(
|
||||||
|
// short = "z",
|
||||||
|
// help = "Disable setting vF to 0 after a bitwise operation."
|
||||||
|
// )]
|
||||||
|
// pub vfreset: bool,
|
||||||
|
// #[options(
|
||||||
|
// short = "x",
|
||||||
|
// help = "Disable waiting for vblank after issuing a draw call."
|
||||||
|
// )]
|
||||||
|
// pub drawsync: bool,
|
||||||
|
|
||||||
|
// #[options(
|
||||||
|
// short = "c",
|
||||||
|
// help = "Use CHIP-48 style DMA instructions, which don't touch I."
|
||||||
|
// )]
|
||||||
|
// pub memory: bool,
|
||||||
|
// #[options(
|
||||||
|
// short = "v",
|
||||||
|
// help = "Use CHIP-48 style bit-shifts, which don't touch vY."
|
||||||
|
// )]
|
||||||
|
// pub shift: bool,
|
||||||
|
// #[options(
|
||||||
|
// short = "b",
|
||||||
|
// help = "Use SUPER-CHIP style indexed jump, which is indexed relative to v[adr]."
|
||||||
|
// )]
|
||||||
|
// pub jumping: bool,
|
||||||
#[options(
|
#[options(
|
||||||
long = "break",
|
long = "break",
|
||||||
help = "Set breakpoints for the emulator to stop at.",
|
help = "Set breakpoints for the emulator to stop at.",
|
||||||
@ -105,7 +111,7 @@ impl State {
|
|||||||
// Load the ROM file into RAM
|
// Load the ROM file into RAM
|
||||||
Program [0x0200..0x1000] = &read(&options.file)?,
|
Program [0x0200..0x1000] = &read(&options.file)?,
|
||||||
// Create a screen
|
// Create a screen
|
||||||
Screen [0x1000..0x1100],
|
Screen [0x1000..0x1400],
|
||||||
// Create a stack
|
// Create a stack
|
||||||
Stack [0x0EA0..0x0F00],
|
Stack [0x0EA0..0x0F00],
|
||||||
},
|
},
|
||||||
@ -117,13 +123,7 @@ impl State {
|
|||||||
Dis::default(),
|
Dis::default(),
|
||||||
options.breakpoints,
|
options.breakpoints,
|
||||||
ControlFlags {
|
ControlFlags {
|
||||||
quirks: chirp::cpu::Quirks {
|
quirks: options.mode.unwrap_or_default().into(),
|
||||||
bin_ops: options.vfreset,
|
|
||||||
shift: options.shift,
|
|
||||||
draw_wait: options.drawsync,
|
|
||||||
dma_inc: options.memory,
|
|
||||||
stupid_jumps: options.jumping,
|
|
||||||
},
|
|
||||||
debug: options.debug,
|
debug: options.debug,
|
||||||
pause: options.pause,
|
pause: options.pause,
|
||||||
monotonic: options.speed,
|
monotonic: options.speed,
|
||||||
@ -131,7 +131,7 @@ impl State {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
ui: UIBuilder::new(64, 32, &options.file).build()?,
|
ui: UIBuilder::new(128, 64, &options.file).build()?,
|
||||||
ft: Instant::now(),
|
ft: Instant::now(),
|
||||||
};
|
};
|
||||||
state.ch8.bus.write(0x1feu16, options.data);
|
state.ch8.bus.write(0x1feu16, options.data);
|
||||||
@ -202,7 +202,7 @@ impl Iterator for State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
pub fn main() -> Result<()> {
|
||||||
let options = Arguments::parse_args_default_or_exit();
|
let options = Arguments::parse_args_default_or_exit();
|
||||||
let state = State::new(options)?;
|
let state = State::new(options)?;
|
||||||
for result in state {
|
for result in state {
|
||||||
|
@ -56,14 +56,14 @@ impl UIBuilder {
|
|||||||
impl Default for UIBuilder {
|
impl Default for UIBuilder {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
UIBuilder {
|
UIBuilder {
|
||||||
width: 64,
|
width: 128,
|
||||||
height: 32,
|
height: 64,
|
||||||
name: Some("Chip-8 Interpreter"),
|
name: Some("Chip-8 Interpreter"),
|
||||||
rom: None,
|
rom: None,
|
||||||
window_options: WindowOptions {
|
window_options: WindowOptions {
|
||||||
title: true,
|
title: true,
|
||||||
resize: false,
|
resize: false,
|
||||||
scale: Scale::X16,
|
scale: Scale::X8,
|
||||||
scale_mode: ScaleMode::AspectRatioStretch,
|
scale_mode: ScaleMode::AspectRatioStretch,
|
||||||
none: true,
|
none: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@ -80,9 +80,13 @@ pub struct FrameBufferFormat {
|
|||||||
|
|
||||||
impl Default for FrameBufferFormat {
|
impl Default for FrameBufferFormat {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
// FrameBufferFormat {
|
||||||
|
// fg: 0x0011a434,
|
||||||
|
// bg: 0x001E2431,
|
||||||
|
// }
|
||||||
FrameBufferFormat {
|
FrameBufferFormat {
|
||||||
fg: 0x0011a434,
|
fg: 0xc4c4c4,
|
||||||
bg: 0x001E2431,
|
bg: 0x000000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,6 +116,8 @@ impl FrameBuffer {
|
|||||||
self.format.fg
|
self.format.fg
|
||||||
} else {
|
} else {
|
||||||
self.format.bg
|
self.format.bg
|
||||||
|
// .wrapping_add(0x001104 * (idx / self.width) as u32)
|
||||||
|
// .wrapping_add(0x141000 * (idx & 3) as u32)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,7 +129,7 @@ impl FrameBuffer {
|
|||||||
|
|
||||||
impl Default for FrameBuffer {
|
impl Default for FrameBuffer {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(64, 32)
|
Self::new(128, 64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
76
src/bus.rs
76
src/bus.rs
@ -308,14 +308,14 @@ impl Bus {
|
|||||||
const REGION: Region = Region::Screen;
|
const REGION: Region = Region::Screen;
|
||||||
if let Some(screen) = self.get_region(REGION) {
|
if let Some(screen) = self.get_region(REGION) {
|
||||||
for (index, byte) in screen.iter().enumerate() {
|
for (index, byte) in screen.iter().enumerate() {
|
||||||
if index % 8 == 0 {
|
if index % 16 == 0 {
|
||||||
print!("|");
|
print!("{index:03x}|");
|
||||||
}
|
}
|
||||||
print!(
|
print!(
|
||||||
"{}",
|
"{}",
|
||||||
format!("{byte:08b}").replace('0', " ").replace('1', "██")
|
format!("{byte:08b}").replace('0', " ").replace('1', "█")
|
||||||
);
|
);
|
||||||
if index % 8 == 7 {
|
if index % 16 == 15 {
|
||||||
println!("|");
|
println!("|");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -339,7 +339,43 @@ impl Read<u16> for Bus {
|
|||||||
fn read(&self, addr: impl Into<usize>) -> u16 {
|
fn read(&self, addr: impl Into<usize>) -> u16 {
|
||||||
let addr: usize = addr.into();
|
let addr: usize = addr.into();
|
||||||
if let Some(bytes) = self.memory.get(addr..addr + 2) {
|
if let Some(bytes) = self.memory.get(addr..addr + 2) {
|
||||||
u16::from_be_bytes(bytes.try_into().expect("asked for 2 bytes, got != 2 bytes"))
|
u16::from_be_bytes(bytes.try_into().expect("Should get 2 bytes"))
|
||||||
|
} else {
|
||||||
|
0xc5c5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read<u32> for Bus {
|
||||||
|
/// Read a u16 from address `addr`
|
||||||
|
fn read(&self, addr: impl Into<usize>) -> u32 {
|
||||||
|
let addr: usize = addr.into();
|
||||||
|
if let Some(bytes) = self.memory.get(addr..addr + 4) {
|
||||||
|
u32::from_be_bytes(bytes.try_into().expect("Should get 4 bytes"))
|
||||||
|
} else {
|
||||||
|
0xc5c5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read<u64> for Bus {
|
||||||
|
/// Read a u16 from address `addr`
|
||||||
|
fn read(&self, addr: impl Into<usize>) -> u64 {
|
||||||
|
let addr: usize = addr.into();
|
||||||
|
if let Some(bytes) = self.memory.get(addr..addr + 8) {
|
||||||
|
u64::from_be_bytes(bytes.try_into().expect("Should get 8 bytes"))
|
||||||
|
} else {
|
||||||
|
0xc5c5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read<u128> for Bus {
|
||||||
|
/// Read a u16 from address `addr`
|
||||||
|
fn read(&self, addr: impl Into<usize>) -> u128 {
|
||||||
|
let addr: usize = addr.into();
|
||||||
|
if let Some(bytes) = self.memory.get(addr..addr + 16) {
|
||||||
|
u128::from_be_bytes(bytes.try_into().expect("Should get 16 bytes"))
|
||||||
} else {
|
} else {
|
||||||
0xc5c5
|
0xc5c5
|
||||||
}
|
}
|
||||||
@ -366,6 +402,36 @@ impl Write<u16> for Bus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Write<u32> for Bus {
|
||||||
|
/// Write a u16 to address `addr`
|
||||||
|
fn write(&mut self, addr: impl Into<usize>, data: u32) {
|
||||||
|
let addr: usize = addr.into();
|
||||||
|
if let Some(slice) = self.get_mut(addr..addr + 4) {
|
||||||
|
data.to_be_bytes().as_mut().swap_with_slice(slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write<u64> for Bus {
|
||||||
|
/// Write a u16 to address `addr`
|
||||||
|
fn write(&mut self, addr: impl Into<usize>, data: u64) {
|
||||||
|
let addr: usize = addr.into();
|
||||||
|
if let Some(slice) = self.get_mut(addr..addr + 8) {
|
||||||
|
data.to_be_bytes().as_mut().swap_with_slice(slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write<u128> for Bus {
|
||||||
|
/// Write a u16 to address `addr`
|
||||||
|
fn write(&mut self, addr: impl Into<usize>, data: u128) {
|
||||||
|
let addr: usize = addr.into();
|
||||||
|
if let Some(slice) = self.get_mut(addr..addr + 16) {
|
||||||
|
data.to_be_bytes().as_mut().swap_with_slice(slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_feature = "rhexdump")]
|
#[cfg(target_feature = "rhexdump")]
|
||||||
impl Display for Bus {
|
impl Display for Bus {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
233
src/cpu.rs
233
src/cpu.rs
@ -22,12 +22,39 @@ use crate::{
|
|||||||
use imperative_rs::InstructionSet;
|
use imperative_rs::InstructionSet;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use rand::random;
|
use rand::random;
|
||||||
use std::time::Instant;
|
use std::{str::FromStr, time::Instant};
|
||||||
|
|
||||||
type Reg = usize;
|
type Reg = usize;
|
||||||
type Adr = u16;
|
type Adr = u16;
|
||||||
type Nib = u8;
|
type Nib = u8;
|
||||||
|
|
||||||
|
/// Selects the memory behavior of the interpreter
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum Mode {
|
||||||
|
/// VIP emulation mode
|
||||||
|
#[default]
|
||||||
|
Chip8,
|
||||||
|
/// Chip-48 emulation mode
|
||||||
|
SChip,
|
||||||
|
/// XO-Chip emulation mode
|
||||||
|
XOChip,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Mode {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"chip8" | "chip-8" => Ok(Mode::Chip8),
|
||||||
|
"schip" | "superchip" => Ok(Mode::SChip),
|
||||||
|
"xo-chip" | "xochip" => Ok(Mode::XOChip),
|
||||||
|
_ => Err(Error::InvalidMode {
|
||||||
|
mode: s.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Controls the authenticity behavior of the CPU on a granular level.
|
/// Controls the authenticity behavior of the CPU on a granular level.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct Quirks {
|
pub struct Quirks {
|
||||||
@ -51,7 +78,7 @@ impl From<bool> for Quirks {
|
|||||||
shift: true,
|
shift: true,
|
||||||
draw_wait: true,
|
draw_wait: true,
|
||||||
dma_inc: true,
|
dma_inc: true,
|
||||||
stupid_jumps: false,
|
stupid_jumps: true,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Quirks {
|
Quirks {
|
||||||
@ -65,6 +92,22 @@ impl From<bool> for Quirks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Mode> for Quirks {
|
||||||
|
fn from(value: Mode) -> Self {
|
||||||
|
match value {
|
||||||
|
Mode::Chip8 => false.into(),
|
||||||
|
Mode::SChip => true.into(),
|
||||||
|
Mode::XOChip => Self {
|
||||||
|
bin_ops: true,
|
||||||
|
shift: false,
|
||||||
|
draw_wait: true,
|
||||||
|
dma_inc: false,
|
||||||
|
stupid_jumps: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for Quirks {
|
impl Default for Quirks {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::from(false)
|
Self::from(false)
|
||||||
@ -82,9 +125,13 @@ pub struct ControlFlags {
|
|||||||
pub keypause: bool,
|
pub keypause: bool,
|
||||||
/// Set when the emulator is waiting for a frame to be drawn
|
/// Set when the emulator is waiting for a frame to be drawn
|
||||||
pub draw_wait: bool,
|
pub draw_wait: bool,
|
||||||
|
/// Set when the emulator is in high-res mode
|
||||||
|
pub draw_mode: bool,
|
||||||
/// Set to the last key that's been *released* after a keypause
|
/// Set to the last key that's been *released* after a keypause
|
||||||
pub lastkey: Option<usize>,
|
pub lastkey: Option<usize>,
|
||||||
/// Represents the set of emulator [Quirks] to enable
|
/// Represents the current emulator [Mode]
|
||||||
|
pub mode: Mode,
|
||||||
|
/// Represents the set of emulator [Quirks] to enable, independent of the [Mode]
|
||||||
pub quirks: Quirks,
|
pub quirks: Quirks,
|
||||||
/// Represents the number of instructions to run per tick of the internal timer
|
/// Represents the number of instructions to run per tick of the internal timer
|
||||||
pub monotonic: Option<usize>,
|
pub monotonic: Option<usize>,
|
||||||
@ -704,6 +751,7 @@ impl CPU {
|
|||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
fn execute(&mut self, bus: &mut Bus, instruction: Insn) {
|
fn execute(&mut self, bus: &mut Bus, instruction: Insn) {
|
||||||
match instruction {
|
match instruction {
|
||||||
|
// Core Chip-8 instructions
|
||||||
Insn::cls => self.clear_screen(bus),
|
Insn::cls => self.clear_screen(bus),
|
||||||
Insn::ret => self.ret(bus),
|
Insn::ret => self.ret(bus),
|
||||||
Insn::jmp { A } => self.jump(A),
|
Insn::jmp { A } => self.jump(A),
|
||||||
@ -713,7 +761,7 @@ impl CPU {
|
|||||||
Insn::se { y, x } => self.skip_equals(x, y),
|
Insn::se { y, x } => self.skip_equals(x, y),
|
||||||
Insn::movb { B, x } => self.load_immediate(x, B),
|
Insn::movb { B, x } => self.load_immediate(x, B),
|
||||||
Insn::addb { B, x } => self.add_immediate(x, B),
|
Insn::addb { B, x } => self.add_immediate(x, B),
|
||||||
Insn::mov { x, y } => self.load(x, y),
|
Insn::mov { y, x } => self.load(x, y),
|
||||||
Insn::or { y, x } => self.or(x, y),
|
Insn::or { y, x } => self.or(x, y),
|
||||||
Insn::and { y, x } => self.and(x, y),
|
Insn::and { y, x } => self.and(x, y),
|
||||||
Insn::xor { y, x } => self.xor(x, y),
|
Insn::xor { y, x } => self.xor(x, y),
|
||||||
@ -726,7 +774,7 @@ impl CPU {
|
|||||||
Insn::movI { A } => self.load_i_immediate(A),
|
Insn::movI { A } => self.load_i_immediate(A),
|
||||||
Insn::jmpr { A } => self.jump_indexed(A),
|
Insn::jmpr { A } => self.jump_indexed(A),
|
||||||
Insn::rand { B, x } => self.rand(x, B),
|
Insn::rand { B, x } => self.rand(x, B),
|
||||||
Insn::draw { x, y, n } => self.draw(x, y, n, bus),
|
Insn::draw { y, x, n } => self.draw(x, y, n, bus),
|
||||||
Insn::sek { x } => self.skip_key_equals(x),
|
Insn::sek { x } => self.skip_key_equals(x),
|
||||||
Insn::snek { x } => self.skip_key_not_equals(x),
|
Insn::snek { x } => self.skip_key_not_equals(x),
|
||||||
Insn::getdt { x } => self.load_delay_timer(x),
|
Insn::getdt { x } => self.load_delay_timer(x),
|
||||||
@ -738,6 +786,16 @@ impl CPU {
|
|||||||
Insn::bcd { x } => self.bcd_convert(x, bus),
|
Insn::bcd { x } => self.bcd_convert(x, bus),
|
||||||
Insn::dmao { x } => self.store_dma(x, bus),
|
Insn::dmao { x } => self.store_dma(x, bus),
|
||||||
Insn::dmai { x } => self.load_dma(x, bus),
|
Insn::dmai { x } => self.load_dma(x, bus),
|
||||||
|
// Super-Chip extensions
|
||||||
|
Insn::scd { n } => self.scroll_down(n, bus),
|
||||||
|
Insn::scr => self.scr(bus),
|
||||||
|
Insn::scl => self.scl(bus),
|
||||||
|
Insn::halt => self.flags.pause(),
|
||||||
|
Insn::lores => self.flags.draw_mode = false,
|
||||||
|
Insn::hires => self.flags.draw_mode = true,
|
||||||
|
Insn::hfont { x } => self.load_big_sprite(x),
|
||||||
|
Insn::flgo { x } => self.store_flags(x, bus),
|
||||||
|
Insn::flgi { x } => self.load_flags(x, bus),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -755,9 +813,7 @@ impl CPU {
|
|||||||
/// |`00e0`| Clears the screen memory to 0
|
/// |`00e0`| Clears the screen memory to 0
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn clear_screen(&mut self, bus: &mut Bus) {
|
fn clear_screen(&mut self, bus: &mut Bus) {
|
||||||
if let Some(screen) = bus.get_region_mut(Region::Screen) {
|
bus.clear_region(Region::Screen);
|
||||||
screen.fill(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/// |`00ee`| Returns from subroutine
|
/// |`00ee`| Returns from subroutine
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
@ -765,6 +821,37 @@ impl CPU {
|
|||||||
self.sp = self.sp.wrapping_add(2);
|
self.sp = self.sp.wrapping_add(2);
|
||||||
self.pc = bus.read(self.sp);
|
self.pc = bus.read(self.sp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// |`00cN`| Scroll the screen down N lines
|
||||||
|
#[inline(always)]
|
||||||
|
fn scroll_down(&mut self, n: Nib, bus: &mut Bus) {
|
||||||
|
// Get a line from the bus
|
||||||
|
for i in (16 * n as usize..16 * 15).step_by(16).rev() {
|
||||||
|
let i = i + self.screen as usize;
|
||||||
|
let line: u128 = bus.read(i);
|
||||||
|
bus.write(i - (n as usize * 16), line);
|
||||||
|
bus.write(i, 0u128);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`00fb`| Scroll the screen right
|
||||||
|
#[inline(always)]
|
||||||
|
fn scr(&mut self, bus: &mut (impl Read<u128> + Write<u128>)) {
|
||||||
|
// Get a line from the bus
|
||||||
|
for i in (0..16 * 64).step_by(16) {
|
||||||
|
//let line: u128 = bus.read(self.screen + i) >> 4;
|
||||||
|
bus.write(self.screen + i, bus.read(self.screen + i) >> 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// |`00fc`| Scroll the screen right
|
||||||
|
#[inline(always)]
|
||||||
|
fn scl(&mut self, bus: &mut (impl Read<u128> + Write<u128>)) {
|
||||||
|
// Get a line from the bus
|
||||||
|
for i in (0..16 * 64).step_by(16) {
|
||||||
|
let line: u128 = (bus.read(self.screen + i) & !(0xf << 124)) << 4;
|
||||||
|
bus.write(self.screen + i, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// |`1aaa`| Sets pc to an absolute address
|
// |`1aaa`| Sets pc to an absolute address
|
||||||
@ -994,6 +1081,15 @@ impl CPU {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TODO: Do this more idiomatically, using some iterator chain?
|
||||||
|
fn doublewide(value: u16) -> u32 {
|
||||||
|
let mut out: u32 = 0;
|
||||||
|
for i in 0..16 {
|
||||||
|
out |= ((value as u32 & 0x1 << i) * 3) << i;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY)
|
// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY)
|
||||||
impl CPU {
|
impl CPU {
|
||||||
/// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY)
|
/// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY)
|
||||||
@ -1002,31 +1098,86 @@ impl CPU {
|
|||||||
/// On the original chip-8 interpreter, this will wait for a VBI
|
/// On the original chip-8 interpreter, this will wait for a VBI
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn draw(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) {
|
fn draw(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) {
|
||||||
let (x, y) = (self.v[x] as u16 % 64, self.v[y] as u16 % 32);
|
// lmaotch
|
||||||
|
match self.flags.draw_mode {
|
||||||
|
true => self.draw_hires(x, y, n, bus),
|
||||||
|
false => self.draw_lores(x, y, n, bus),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[inline(always)]
|
||||||
|
fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) {
|
||||||
if !self.flags.quirks.draw_wait {
|
if !self.flags.quirks.draw_wait {
|
||||||
self.flags.draw_wait = true;
|
self.flags.draw_wait = true;
|
||||||
}
|
}
|
||||||
|
let (w, h) = (64, 32);
|
||||||
|
let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h);
|
||||||
self.v[0xf] = 0;
|
self.v[0xf] = 0;
|
||||||
for byte in 0..n as u16 {
|
if let Some(sprite) = bus.get(self.i as usize..(self.i + n as u16) as usize) {
|
||||||
if y + byte > 32 {
|
let sprite = sprite.to_vec();
|
||||||
return;
|
for (line, sprite) in sprite.iter().enumerate() {
|
||||||
|
let (line, sprite) = (line as u16, *sprite);
|
||||||
|
// clip
|
||||||
|
if y + line >= h {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// Calculate the lower bound address based on the X,Y position on the screen
|
// clip
|
||||||
let addr = (y + byte) * 8 + (x & 0x3f) / 8 + self.screen;
|
let sprite = (sprite as u16) << (8 - (x % 8))
|
||||||
// Read a byte of sprite data into a u16, and shift it x % 8 bits
|
& if (x % w) >= (w - 8) { 0xff00 } else { 0xffff };
|
||||||
let sprite: u8 = bus.read(self.i + byte);
|
// scale
|
||||||
let sprite =
|
let addr = |x, y| -> u16 { ((y + (2 * line)) * 8 + (x / 8)) * 2 + self.screen };
|
||||||
(sprite as u16) << (8 - (x & 7)) & if x % 64 > 56 { 0xff00 } else { 0xffff };
|
let y = y << 1;
|
||||||
// Read a u16 from the bus containing the two bytes which might need to be updated
|
let sprite = doublewide(sprite);
|
||||||
let mut screen: u16 = bus.read(addr);
|
for scale in 0..2 {
|
||||||
// Save the bits-toggled-off flag if necessary
|
let screen: u32 = bus.read(addr(x, y + scale));
|
||||||
|
bus.write(addr(x, y + scale), screen ^ sprite);
|
||||||
if screen & sprite != 0 {
|
if screen & sprite != 0 {
|
||||||
self.v[0xF] = 1
|
self.v[0xf] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Super-Chip extension high-resolution graphics mode
|
||||||
|
#[inline(always)]
|
||||||
|
fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) {
|
||||||
|
if !self.flags.quirks.draw_wait {
|
||||||
|
self.flags.draw_wait = true;
|
||||||
|
}
|
||||||
|
let (w, h) = (128, 64);
|
||||||
|
let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h);
|
||||||
|
let w_bytes = w / 8;
|
||||||
|
self.v[0xf] = 0;
|
||||||
|
if n == 0 {
|
||||||
|
if let Some(sprite) = bus.get(self.i as usize..(self.i + 32) as usize) {
|
||||||
|
let sprite = sprite.to_owned();
|
||||||
|
for (line, sprite) in sprite.chunks(2).enumerate() {
|
||||||
|
let sprite = u16::from_be_bytes(
|
||||||
|
sprite
|
||||||
|
.try_into()
|
||||||
|
.expect("Chunks should only return 2 bytes"),
|
||||||
|
);
|
||||||
|
let addr = (y + line as u16) * w_bytes + x / 8 + self.screen;
|
||||||
|
let sprite = (sprite as u32) << (16 - (x % 8));
|
||||||
|
let screen: u32 = bus.read(addr);
|
||||||
|
bus.write(addr, screen ^ sprite);
|
||||||
|
if screen & sprite != 0 {
|
||||||
|
self.v[0xf] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(sprite) = bus.get(self.i as usize..(self.i + n as u16) as usize) {
|
||||||
|
let sprite = sprite.to_vec();
|
||||||
|
for (line, sprite) in sprite.iter().enumerate() {
|
||||||
|
let addr = (y + line as u16) * w_bytes + x / 8 + self.screen;
|
||||||
|
let sprite = (*sprite as u16) << (8 - (x % 8));
|
||||||
|
let screen: u16 = bus.read(addr);
|
||||||
|
bus.write(addr, screen ^ sprite);
|
||||||
|
if screen & sprite != 0 {
|
||||||
|
self.v[0xf] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Update the screen word by XORing the sprite byte
|
|
||||||
screen ^= sprite;
|
|
||||||
// Save the result to the screen
|
|
||||||
bus.write(addr, screen);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1164,4 +1315,34 @@ impl CPU {
|
|||||||
self.i += x as Adr + 1;
|
self.i += x as Adr + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// |`Fx30`| (Super-Chip) 16x16 equivalent of Fx29
|
||||||
|
#[inline(always)]
|
||||||
|
fn load_big_sprite(&mut self, x: Reg) {
|
||||||
|
self.i = self.font + (5 * 8) + (16 * (self.v[x] as Adr % 0x10));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Fx75`| (Super-Chip) Save to "flag registers"
|
||||||
|
/// I just chuck it in 0x0..0xf. Screw it.
|
||||||
|
#[inline(always)]
|
||||||
|
fn store_flags(&mut self, x: Reg, bus: &mut Bus) {
|
||||||
|
// TODO: Save these, maybe
|
||||||
|
for (reg, value) in bus
|
||||||
|
.get_mut(0..=x)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
*value = self.v[reg]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Fx85`| (Super-Chip) Load from "flag registers"
|
||||||
|
/// I just chuck it in 0x0..0xf. Screw it.
|
||||||
|
#[inline(always)]
|
||||||
|
fn load_flags(&mut self, x: Reg, bus: &mut Bus) {
|
||||||
|
for (reg, value) in bus.get(0..=x).unwrap_or_default().iter().enumerate() {
|
||||||
|
self.v[reg] = *value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ use std::fmt::Display;
|
|||||||
#[derive(Clone, Copy, Debug, InstructionSet, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, InstructionSet, PartialEq, Eq)]
|
||||||
/// Implements a Disassembler using imperative_rs
|
/// Implements a Disassembler using imperative_rs
|
||||||
pub enum Insn {
|
pub enum Insn {
|
||||||
|
// Base instruction set
|
||||||
/// | 00e0 | Clear screen memory to 0s
|
/// | 00e0 | Clear screen memory to 0s
|
||||||
#[opcode = "0x00e0"]
|
#[opcode = "0x00e0"]
|
||||||
cls,
|
cls,
|
||||||
@ -111,12 +112,42 @@ pub enum Insn {
|
|||||||
// | fX65 | DMA Load from I to registers 0..X
|
// | fX65 | DMA Load from I to registers 0..X
|
||||||
#[opcode = "0xfx65"]
|
#[opcode = "0xfx65"]
|
||||||
dmai { x: usize },
|
dmai { x: usize },
|
||||||
|
|
||||||
|
// Super Chip extensions
|
||||||
|
/// | 00cN | Scroll the screen down
|
||||||
|
#[opcode = "0x00cn"]
|
||||||
|
scd { n: u8 },
|
||||||
|
/// | 00fb | Scroll the screen right
|
||||||
|
#[opcode = "0x00fb"]
|
||||||
|
scr,
|
||||||
|
/// | 00fc | Scroll the screen left
|
||||||
|
#[opcode = "0x00fc"]
|
||||||
|
scl,
|
||||||
|
/// | 00fd | Exit (halt and catch fire)
|
||||||
|
#[opcode = "0x00fd"]
|
||||||
|
halt,
|
||||||
|
/// | 00fe | Return to low-resolution mode
|
||||||
|
#[opcode = "0x00fe"]
|
||||||
|
lores,
|
||||||
|
/// | 00ff | Enter high-resolution mode
|
||||||
|
#[opcode = "0x00ff"]
|
||||||
|
hires,
|
||||||
|
/// | fx30 | Enter high-resolution mode
|
||||||
|
#[opcode = "0xfx30"]
|
||||||
|
hfont { x: usize },
|
||||||
|
/// | fx75 | Save to "flag registers"
|
||||||
|
#[opcode = "0xfx75"]
|
||||||
|
flgo { x: usize },
|
||||||
|
/// | fx85 | Load from "flag registers"
|
||||||
|
#[opcode = "0xfx85"]
|
||||||
|
flgi { x: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Insn {
|
impl Display for Insn {
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
// Base instruction set
|
||||||
Insn::cls => write!(f, "cls "),
|
Insn::cls => write!(f, "cls "),
|
||||||
Insn::ret => write!(f, "ret "),
|
Insn::ret => write!(f, "ret "),
|
||||||
Insn::jmp { A } => write!(f, "jmp {A:03x}"),
|
Insn::jmp { A } => write!(f, "jmp {A:03x}"),
|
||||||
@ -151,6 +182,16 @@ impl Display for Insn {
|
|||||||
Insn::bcd { x } => write!(f, "bcd v{x:X}, &I"),
|
Insn::bcd { x } => write!(f, "bcd v{x:X}, &I"),
|
||||||
Insn::dmao { x } => write!(f, "dmao v{x:X}"),
|
Insn::dmao { x } => write!(f, "dmao v{x:X}"),
|
||||||
Insn::dmai { x } => write!(f, "dmai v{x:X}"),
|
Insn::dmai { x } => write!(f, "dmai v{x:X}"),
|
||||||
|
// Super Chip extensions
|
||||||
|
Insn::scd { n } => write!(f, "scd #{n:x}"),
|
||||||
|
Insn::scr => write!(f, "scr "),
|
||||||
|
Insn::scl => write!(f, "scl "),
|
||||||
|
Insn::halt => write!(f, "halt "),
|
||||||
|
Insn::lores => write!(f, "lores "),
|
||||||
|
Insn::hires => write!(f, "hires "),
|
||||||
|
Insn::hfont { x } => write!(f, "hfont v{x:X}"),
|
||||||
|
Insn::flgo { x } => write!(f, "flgo v{x:X}"),
|
||||||
|
Insn::flgi { x } => write!(f, "flgi v{x:X}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,12 @@ pub enum Error {
|
|||||||
/// The offending register
|
/// The offending register
|
||||||
reg: usize,
|
reg: usize,
|
||||||
},
|
},
|
||||||
|
/// Tried to convert string into mode, but it did not match.
|
||||||
|
#[error("Invalid mode: {mode}")]
|
||||||
|
InvalidMode {
|
||||||
|
/// The string which failed to become a mode
|
||||||
|
mode: String,
|
||||||
|
},
|
||||||
/// Error originated in [std::io]
|
/// Error originated in [std::io]
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
IoError(#[from] std::io::Error),
|
IoError(#[from] std::io::Error),
|
||||||
|
@ -14,7 +14,7 @@ pub mod error;
|
|||||||
|
|
||||||
// Common imports for Chirp
|
// Common imports for Chirp
|
||||||
pub use bus::{Bus, Read, Region::*, Write};
|
pub use bus::{Bus, Read, Region::*, Write};
|
||||||
pub use cpu::{disassembler::Dis, ControlFlags, CPU};
|
pub use cpu::{disassembler::Dis, ControlFlags, Mode, CPU};
|
||||||
pub use error::Result;
|
pub use error::Result;
|
||||||
|
|
||||||
/// Holds the state of a Chip-8
|
/// Holds the state of a Chip-8
|
||||||
|
@ -17,7 +17,7 @@ fn setup_environment() -> (CPU, Bus) {
|
|||||||
Charset [0x0050..0x00A0] = include_bytes!("../src/mem/charset.bin"),
|
Charset [0x0050..0x00A0] = include_bytes!("../src/mem/charset.bin"),
|
||||||
// Load the ROM file into RAM
|
// Load the ROM file into RAM
|
||||||
Program [0x0200..0x1000] = include_bytes!("../chip8-test-suite/bin/chip8-test-suite.ch8"),
|
Program [0x0200..0x1000] = include_bytes!("../chip8-test-suite/bin/chip8-test-suite.ch8"),
|
||||||
// Create a screen, and fill it with garbage data
|
// Create a screen, and fill it with
|
||||||
Screen [0x0F00..0x1000] = include_bytes!("chip8_test_suite.rs"),
|
Screen [0x0F00..0x1000] = include_bytes!("chip8_test_suite.rs"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -132,8 +132,8 @@ mod cpu {
|
|||||||
keypause: false,
|
keypause: false,
|
||||||
draw_wait: false,
|
draw_wait: false,
|
||||||
lastkey: None,
|
lastkey: None,
|
||||||
quirks: Default::default(),
|
|
||||||
monotonic: None,
|
monotonic: None,
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
let cf2 = cf1.clone();
|
let cf2 = cf1.clone();
|
||||||
assert_eq!(cf1, cf2)
|
assert_eq!(cf1, cf2)
|
||||||
@ -151,9 +151,7 @@ mod cpu {
|
|||||||
pause: false,
|
pause: false,
|
||||||
keypause: false,
|
keypause: false,
|
||||||
draw_wait: false,
|
draw_wait: false,
|
||||||
lastkey: Default::default(),
|
..Default::default()
|
||||||
quirks: Default::default(),
|
|
||||||
monotonic: Default::default()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -165,9 +163,7 @@ mod cpu {
|
|||||||
pause: true,
|
pause: true,
|
||||||
keypause: true,
|
keypause: true,
|
||||||
draw_wait: true,
|
draw_wait: true,
|
||||||
lastkey: Default::default(),
|
..Default::default()
|
||||||
quirks: Default::default(),
|
|
||||||
monotonic: Default::default(),
|
|
||||||
};
|
};
|
||||||
assert_ne!(cf1, cf2);
|
assert_ne!(cf1, cf2);
|
||||||
}
|
}
|
||||||
@ -179,9 +175,7 @@ mod cpu {
|
|||||||
pause: true,
|
pause: true,
|
||||||
keypause: true,
|
keypause: true,
|
||||||
draw_wait: true,
|
draw_wait: true,
|
||||||
lastkey: Default::default(),
|
..Default::default()
|
||||||
quirks: Default::default(),
|
|
||||||
monotonic: Default::default(),
|
|
||||||
};
|
};
|
||||||
assert!(cf1 < cf2);
|
assert!(cf1 < cf2);
|
||||||
assert_eq!(ControlFlags::default(), cf1.min(cf2));
|
assert_eq!(ControlFlags::default(), cf1.min(cf2));
|
||||||
|
Loading…
Reference in New Issue
Block a user