Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

All work required for v0.2 milestone complete #16

Merged
merged 2 commits into from
Feb 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/display.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::error::Error;
use crate::error::ErrorDetail;
use std::cmp;

/// The default CHIP-8 display size (64 x 32 pixels).
Expand All @@ -11,7 +11,7 @@ const DISPLAY_COLUMN_SIZE_PIXELS: usize = 32;
/// publically for read access by hosting applications so the display can be graphically rendered,
/// via a [StateSnapshot](crate::StateSnapshot) obtained from a call to
/// [Processor::export_state_snapshot()](crate::Processor::export_state_snapshot).
#[derive(Clone)]
#[derive(Clone, Debug, PartialEq)]
pub struct Display {
/// A two-dimensional array to hold the state of the display pixels (1 means on, 0 means off).
///
Expand All @@ -32,7 +32,7 @@ impl Display {
}

/// Clears the display by recreating the pixel array with default size and all pixels set to off.
pub fn clear(&mut self) {
pub(crate) fn clear(&mut self) {
self.pixels = [[0x0; DISPLAY_ROW_SIZE_PIXELS / 8]; DISPLAY_COLUMN_SIZE_PIXELS];
}

Expand All @@ -43,12 +43,12 @@ impl Display {
/// * `x_start_pixel` - An zero-based integer giving the starting x coordinate of the sprite
/// * `y_start_pixel` - An zero-based integer giving the starting y coordinate of the sprite
/// * `sprite` - An array slice holding the bytes that make up the sprite
pub fn draw_sprite(
pub(crate) fn draw_sprite(
&mut self,
x_start_pixel: usize,
y_start_pixel: usize,
sprite: &[u8],
) -> Result<bool, Error> {
) -> Result<bool, ErrorDetail> {
// Sprites are one byte wide, so the height (in pixels) is the length of the sprite byte array,
// however cap this if necessary so the sprite does not draw off the bottom of the display
let pixel_rows: usize = cmp::min(
Expand Down
88 changes: 66 additions & 22 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,56 +1,100 @@
use crate::StateSnapshot;
use std::collections::HashMap;
use std::error;
use std::fmt;

/// An Error enum used throughout the Chipolata crate to communicate runtime errors
/// An Error enum used throughout the Chipolata crate to communicate details of runtime errors
/// that have occurred.
///
/// Instances of [Error] are bubbled-up to the hosting application through the public
/// Instances of [ErrorDetail] are bubbled-up to the hosting application through the public
/// API methods.
#[derive(Debug, PartialEq)]
pub enum Error {
pub enum ErrorDetail {
/// An unrecognised opcode was read from memory
UnknownInstruction,
UnknownInstruction { opcode: u16 },
/// A valid opcode was read from memory but which is not implemented by Chipolata
UnimplementedInstruction,
UnimplementedInstruction { opcode: u16 },
/// One or more operands fall outside expected ranges and cannot be safely used
OperandsOutOfBounds,
/// The HashMap field holds the name of each potential faulty operand and its value
OperandsOutOfBounds { operands: HashMap<String, usize> },
/// An attempt was made to pop an item off the Chipolata stack while it is empty
PopEmptyStack,
/// An attempt was made to push an item on to the Chipolata stack while it is full
PushFullStack,
/// An attempt was made to read/write from an address outside the addressable range
MemoryAddressOutOfBounds,
MemoryAddressOutOfBounds { address: u16 },
/// A key ordinal was referenced that is outside the valid CHIP-8 keypad range (0x0 to 0xF)
InvalidKey,
InvalidKey { key: u8 },
/// General bucket for any unknown issues (to return *something* rather than panicking)
UnknownError,
}

impl error::Error for Error {}
impl error::Error for ErrorDetail {}

impl fmt::Display for Error {
impl fmt::Display for ErrorDetail {
/// Returns a textual description of each enum variant for display purposes.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::UnknownInstruction => {
write!(f, "an unrecognised opcode was decoded")
ErrorDetail::UnknownInstruction { opcode } => {
write!(f, "an unrecognised opcode {:#X} was decoded", opcode)
}
Error::UnimplementedInstruction => {
write!(f, "an unimplemented opcode was executed")
ErrorDetail::UnimplementedInstruction { opcode } => {
write!(f, "an unimplemented opcode {} was executed", opcode)
}
Error::OperandsOutOfBounds => {
write!(f, "an opcode contains invalid operands")
ErrorDetail::OperandsOutOfBounds { operands } => {
write!(f, "an opcode contains invalid operands: {:?}", operands)
}
Error::PopEmptyStack => {
ErrorDetail::PopEmptyStack => {
write!(f, "an attempt was made to pop the stack while empty")
}
Error::PushFullStack => {
ErrorDetail::PushFullStack => {
write!(f, "an attempt was made to push to the stack while full")
}
Error::MemoryAddressOutOfBounds => {
write!(f, "memory was accessed out of bounds")
ErrorDetail::MemoryAddressOutOfBounds { address } => {
write!(f, "invalid memory address {} was accessed", address)
}
Error::InvalidKey => {
write!(f, "an invalid key was specified")
ErrorDetail::InvalidKey { key } => {
write!(f, "invalid key {} was specified", key)
}
ErrorDetail::UnknownError => {
write!(f, "an unknown error occurred")
}
}
}
}

/// An Error struct used to bubble up Chipolata errors to the hosting application. This wraps
/// the more specific [ErrorDetail] error enum, and provides overall processor state context
/// at the point of the failure
#[derive(Debug, PartialEq)]
pub struct ChipolataError {
pub state_snapshot_dump: StateSnapshot,
pub inner_error: ErrorDetail,
}

impl error::Error for ChipolataError {}

impl fmt::Display for ChipolataError {
/// Returns a textual description of the error
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let StateSnapshot::ExtendedSnapshot {
frame_buffer: _,
stack: _,
memory: _,
program_counter,
index_register: _,
variable_registers: _,
delay_timer: _,
sound_timer: _,
cycles,
} = &self.state_snapshot_dump
{
write!(
f,
"an error occurred on cycle {}, with program_counter {}",
cycles, program_counter
)?;
}
self.inner_error.fmt(f)
}
}
21 changes: 16 additions & 5 deletions src/instruction.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::error::Error;
use crate::error::ErrorDetail;

/// An enum with a variant for each instruction within the CHIP-8 instruction set.
#[derive(Debug, PartialEq)]
pub(crate) enum Instruction {
Op004B, // Turn on COSMAC VIP display
Op00E0, // Clear screen
Op00EE, // Subroutine (call)
Op0NNN { nnn: u16 }, // Execute machine language routine
Expand Down Expand Up @@ -42,13 +43,13 @@ pub(crate) enum Instruction {

impl Instruction {
/// Constructor/builder method that parses the supplied two-byte opcode and returns the
/// corresponding [Instruction] enum variant. Returns [Error::UnknownInstruction] if
/// corresponding [Instruction] enum variant. Returns [ErrorDetail::UnknownInstruction] if
/// the opcode cannot be parsed or recognised.
///
/// # Arguments
///
/// * `opcode` - a (big-endian) two-byte representation of the opcode to be parsed
pub(crate) fn decode_from(opcode: u16) -> Result<Instruction, Error> {
pub(crate) fn decode_from(opcode: u16) -> Result<Instruction, ErrorDetail> {
// Divide the 16-bit opcode into four 4-bit nibbles, using bit shifting and masking
let first_nibble: u16 = opcode >> 12;
let second_nibble: u16 = (opcode & 0x0F00) >> 8;
Expand All @@ -57,6 +58,7 @@ impl Instruction {
// Pattern match on the nibbles as appropriate to identify the opcode and return
// the corresponding enum variant
match (first_nibble, second_nibble, third_nibble, fourth_nibble) {
(0x0, 0x0, 0x4, 0xB) => Ok(Instruction::Op004B),
(0x0, 0x0, 0xE, 0x0) => Ok(Instruction::Op00E0),
(0x0, 0x0, 0xE, 0xE) => Ok(Instruction::Op00EE),
(0x0, ..) => Ok(Instruction::Op0NNN {
Expand Down Expand Up @@ -178,14 +180,15 @@ impl Instruction {
}),
// If we have not matched by this point then we cannot identify the
// instruction; return an Error
_ => Err(Error::UnknownInstruction),
_ => Err(ErrorDetail::UnknownInstruction { opcode }),
}
}

/// Returns a textual representation of each enum variant.
#[allow(dead_code)]
pub(crate) fn name(&self) -> &str {
match self {
Instruction::Op004B => "004B",
Instruction::Op00E0 => "00E0",
Instruction::Op00EE => "00EE",
Instruction::Op0NNN { .. } => "0NNN",
Expand Down Expand Up @@ -230,6 +233,14 @@ mod tests {
#![allow(non_snake_case)]
use super::*;

#[test]
fn test_decode_004B() {
assert_eq!(
Instruction::decode_from(0x004B).unwrap(),
Instruction::Op004B
);
}

#[test]
fn test_decode_00E0() {
assert_eq!(
Expand Down Expand Up @@ -518,7 +529,7 @@ mod tests {
fn test_decode_unrecognised_opcode() {
assert_eq!(
Instruction::decode_from(0xFFFF).unwrap_err(),
Error::UnknownInstruction
ErrorDetail::UnknownInstruction { opcode: 0xFFFF }
);
}
}
22 changes: 13 additions & 9 deletions src/keystate.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![allow(non_snake_case)]

use crate::error::Error;
use crate::error::ErrorDetail;

/// The default number of keys in the CHIP-8 keypad.
const NUMBER_OF_KEYS: u8 = 16;
Expand All @@ -21,30 +21,30 @@ impl KeyState {
}

/// Returns true if the specified key is pressed, false if the specified key is not
/// pressed, and returns an [Error::InvalidKey](crate::error::Error::InvalidKey) if
/// pressed, and returns an [ErrorDetail::InvalidKey](crate::error::ErrorDetail::InvalidKey) if
/// the specified key is invalid.
///
/// # Arguments
///
/// * `key` - the hex ordinal of the key (valid range 0x0 to 0xF inclusive)
pub(crate) fn is_key_pressed(&self, key: u8) -> Result<bool, Error> {
pub(crate) fn is_key_pressed(&self, key: u8) -> Result<bool, ErrorDetail> {
match key {
n if n < NUMBER_OF_KEYS => Ok(self.keys_pressed[n as usize]),
_ => Err(Error::InvalidKey),
_ => Err(ErrorDetail::InvalidKey { key }),
}
}

/// Sets the state of the specified key; returns an [Error::InvalidKey] if the
/// Sets the state of the specified key; returns an [ErrorDetail::InvalidKey] if the
/// specified key is invalid.
///
/// # Arguments
///
/// * `key` - the hex ordinal of the key (valid range 0x0 to 0xF inclusive)
/// * `status` - boolean representing key state (true meaning pressed)
pub(crate) fn set_key_status(&mut self, key: u8, status: bool) -> Result<(), Error> {
pub(crate) fn set_key_status(&mut self, key: u8, status: bool) -> Result<(), ErrorDetail> {
match key {
n if n < NUMBER_OF_KEYS => Ok(self.keys_pressed[n as usize] = status),
_ => Err(Error::InvalidKey),
_ => Err(ErrorDetail::InvalidKey { key }),
}
}

Expand Down Expand Up @@ -89,7 +89,9 @@ mod tests {
let keys: KeyState = KeyState::new();
assert_eq!(
keys.is_key_pressed(NUMBER_OF_KEYS).unwrap_err(),
Error::InvalidKey
ErrorDetail::InvalidKey {
key: NUMBER_OF_KEYS
}
);
}

Expand All @@ -105,7 +107,9 @@ mod tests {
let mut keys: KeyState = KeyState::new();
assert_eq!(
keys.set_key_status(NUMBER_OF_KEYS, true).unwrap_err(),
Error::InvalidKey
ErrorDetail::InvalidKey {
key: NUMBER_OF_KEYS
}
);
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ mod stack;

// Re-exports
pub use crate::display::Display;
pub use crate::error::Error;
pub use crate::error::*;
pub use crate::memory::Memory;
pub use crate::options::Options;
pub use crate::options::COSMAC_VIP_PROCESSOR_SPEED_HERTZ;
pub use crate::processor::*;
pub use crate::program::Program;
pub use crate::stack::Stack;
15 changes: 9 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use chipolata::EmulationLevel;
use chipolata::COSMAC_VIP_PROCESSOR_SPEED_HERTZ;
// #![allow(unused)]
use chipolata::Options;
use chipolata::Processor;
Expand Down Expand Up @@ -29,11 +30,17 @@ struct ChipolataApp {

impl ChipolataApp {
pub fn new() -> Self {
let program_data = fs::read("roms\\tests\\bc_test.ch8").unwrap();
let program_data =
fs::read("roms\\demos\\Trip8 Demo (2008) [Revival Studios].ch8").unwrap();
let program: Program = Program::new(program_data);
let mut options: Options = Options::default();
options.processor_speed_hertz = 2500;
options.emulation_level = EmulationLevel::SuperChip11;
// options.processor_speed_hertz = COSMAC_VIP_PROCESSOR_SPEED_HERTZ;
// options.use_variable_cycle_timings = true;
// options.emulation_level = EmulationLevel::Chip8 {
// memory_limit_2k: false,
// };
let mut processor = Processor::initialise_and_load(program, options).unwrap();
let (proc_input_tx, proc_input_rx) = mpsc::channel();
let (proc_output_tx, proc_output_rx) = mpsc::channel();
Expand All @@ -54,11 +61,7 @@ impl ChipolataApp {
processor.execute_cycle().unwrap();
if proc_ready_rx.try_recv().is_ok() {
proc_output_tx
.send(
processor
.export_state_snapshot(StateSnapshotVerbosity::Minimal)
.unwrap(),
)
.send(processor.export_state_snapshot(StateSnapshotVerbosity::Minimal))
.unwrap();
}
//}
Expand Down
Loading