493 lines
14 KiB
Rust
493 lines
14 KiB
Rust
use log::{debug, trace};
|
|
use crate::constants::{CHIP8_VIDEO_MEMORY, CHIP8_VIDEO_WIDTH};
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub struct Chip8Video {
|
|
memory: [bool; CHIP8_VIDEO_MEMORY],
|
|
pub has_frame_changed: bool,
|
|
}
|
|
|
|
impl Chip8Video {
|
|
pub fn reset(&mut self) {
|
|
self.cls();
|
|
self.start_frame();
|
|
}
|
|
|
|
pub fn cls(&mut self) {
|
|
for i in 0..CHIP8_VIDEO_MEMORY {
|
|
self.memory[i] = false;
|
|
}
|
|
}
|
|
|
|
pub fn start_frame(&mut self) {
|
|
self.has_frame_changed = false;
|
|
}
|
|
|
|
pub fn new(initial_configuration: [bool; CHIP8_VIDEO_MEMORY]) -> Self {
|
|
Self {
|
|
memory: initial_configuration,
|
|
has_frame_changed: false,
|
|
}
|
|
}
|
|
|
|
pub fn peek(self, address: u16) -> bool {
|
|
let effective_address = address % 2048;
|
|
self.memory[effective_address as usize]
|
|
}
|
|
|
|
pub fn poke(&mut self, address: u16, new_value: bool) {
|
|
// println!("OFFSET: {address} - POKING {new_value}");
|
|
|
|
// Loop the address
|
|
let effective_address = address % 2048;
|
|
|
|
let old_value = self.memory[effective_address as usize];
|
|
let xored_value = new_value ^ old_value; // XOR of the video
|
|
let value_changed = old_value != xored_value; // From True to False is a change.
|
|
|
|
if value_changed {
|
|
self.has_frame_changed = true
|
|
};
|
|
|
|
self.memory[effective_address as usize] = xored_value;
|
|
}
|
|
|
|
/*
|
|
CHATGPT
|
|
pub fn poke_byte(&mut self, first_address: u16, to_write: u8) {
|
|
let effective_address = first_address as usize % CHIP8_VIDEO_MEMORY;
|
|
|
|
// Loop through each bit of the byte
|
|
for i in 0..8 {
|
|
let is_set = (to_write & (0x80 >> i)) != 0;
|
|
let address = effective_address + i;
|
|
|
|
if address < CHIP8_VIDEO_MEMORY {
|
|
self.poke(address as u16, is_set);
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
|
|
pub fn poke_byte(&mut self, first_address: u16, to_write: u8) {
|
|
for i in (0..8).rev() {
|
|
let shifted = ((1 << i) & to_write) >> i;
|
|
//
|
|
let target_address = first_address + (7 - i);
|
|
let is_set = (to_write & (1 << i)) != 0;
|
|
self.poke(target_address, is_set);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
MINE
|
|
pub fn poke_byte(&mut self, first_address: u16, to_write: u8) {
|
|
for i in (0..8).rev() {
|
|
let shifted = ((1 << i) & to_write) >> i;
|
|
//
|
|
let target_address = first_address + (7 - i);
|
|
let is_set = shifted == 1;
|
|
self.poke(target_address, is_set);
|
|
}
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
pub fn poke_sprite(&mut self, first_address: u16, to_write: Vec<u8>) -> Self {
|
|
let sprite_length = to_write.len();
|
|
|
|
for (index, byte) in to_write.iter().enumerate() {
|
|
let real_address = index * 64;
|
|
self.poke_byte(real_address as u16, *byte);
|
|
};
|
|
self.to_owned()
|
|
}
|
|
|
|
pub fn format_as_string(self) -> String {
|
|
let mut output = String::new();
|
|
for row in 0..32 {
|
|
for column in 0..64 {
|
|
let data_offset = row * 64 + column;
|
|
debug!("Rendering {data_offset} with value {}", self.memory[data_offset]);
|
|
if self.memory[data_offset] {
|
|
output += "*"
|
|
} else {
|
|
output += " "
|
|
}
|
|
}
|
|
output += "\n";
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
pub fn tick(&mut self) {
|
|
self.has_frame_changed = false;
|
|
}
|
|
}
|
|
|
|
impl Default for Chip8Video {
|
|
fn default() -> Self {
|
|
debug!("DEFAULT VIDEO PREPARED");
|
|
|
|
let new_struct = Chip8Video { memory: [false; CHIP8_VIDEO_MEMORY], has_frame_changed: false };
|
|
println!("NEW DEFAULT MEMORY : {}", new_struct.format_as_string());
|
|
|
|
new_struct.clone()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use std::io::Read;
|
|
use crate::chip8::system_memory::{CHIP8FONT_0, CHIP8FONT_1, CHIP8FONT_2, CHIP8FONT_3, CHIP8FONT_4, CHIP8FONT_5, CHIP8FONT_6, CHIP8FONT_7};
|
|
use super::*;
|
|
|
|
const TEST_OUTPUT_SAMPLE_DIR: &str = "../resources/test/";
|
|
|
|
fn build_checkerboard() -> Chip8Video {
|
|
let mut r = Chip8Video::default();
|
|
|
|
for i in 0..CHIP8_VIDEO_MEMORY {
|
|
r.poke(i as u16, i % 2 == 0);
|
|
}
|
|
|
|
r
|
|
}
|
|
|
|
fn read_test_result(suffix: &str) -> String {
|
|
std::fs::read_to_string(TEST_OUTPUT_SAMPLE_DIR.to_owned() + suffix)
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn smoke() { assert!(true) }
|
|
|
|
#[test]
|
|
fn default_test() {
|
|
let mut x = Chip8Video::default();
|
|
|
|
for i in 0..CHIP8_VIDEO_MEMORY {
|
|
assert!(!x.clone().peek(i as u16));
|
|
// then flip the value and test again.
|
|
&x.poke(i as u16, true);
|
|
assert!(x.clone().peek(i as u16));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn string_test_1() {
|
|
let mut x = Chip8Video::default();
|
|
let mut working_string = String::new();
|
|
for i in 0..32 {
|
|
working_string += &*(" ".repeat(64) + "\n");
|
|
}
|
|
|
|
assert_eq!(working_string, x.format_as_string());
|
|
|
|
let mut working_string = String::new();
|
|
// set a checkerboard...
|
|
for cb_row in 0..32 {
|
|
for cb_col in 0..64 {
|
|
let data_offset = cb_row * 64 + cb_col;
|
|
if data_offset % 2 == 0 {
|
|
x.poke(data_offset, true);
|
|
working_string += "*";
|
|
} else {
|
|
x.poke(data_offset, false);
|
|
working_string += " ";
|
|
}
|
|
}
|
|
working_string += "\n";
|
|
}
|
|
|
|
assert_eq!(working_string, x.format_as_string());
|
|
}
|
|
|
|
#[test]
|
|
fn set_initial_memory() {
|
|
let mut x = Chip8Video::default();
|
|
// let mut initial_memory = [false; CHIP8_VIDEO_MEMORY];
|
|
let mut ws = String::new();
|
|
// set our checkerboard
|
|
for cbr in 0..32 {
|
|
for cbc in 0..64 {
|
|
let dof = cbr * 64 + cbc;
|
|
if (dof as i32 % 2) == 0 {
|
|
x.poke(dof, true);
|
|
ws += "*";
|
|
} else {
|
|
ws += " ";
|
|
}
|
|
}
|
|
ws += "\n";
|
|
}
|
|
assert_eq!(x.format_as_string(), ws);
|
|
}
|
|
|
|
#[test]
|
|
fn poke_byte() {
|
|
let to_poke = 0b11001111;
|
|
let mut x = Chip8Video::default();
|
|
x.poke_byte(0x05, to_poke);
|
|
let mut expected = String::new();
|
|
expected = " ** **** \n".to_string();
|
|
for i in 0..31 {
|
|
expected += &*(" ".repeat(64) + "\n");
|
|
}
|
|
assert_eq!(x.format_as_string(), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn cls() {
|
|
let mut initial_memory = [false; CHIP8_VIDEO_MEMORY];
|
|
let mut ws = String::new();
|
|
|
|
for cbr in 0..32 {
|
|
for cbc in 0..64 {
|
|
let dof = cbr * 64 + cbc;
|
|
if (dof as i32 % 2) == 0 {
|
|
initial_memory[dof] = true;
|
|
}
|
|
ws += " ";
|
|
}
|
|
ws += "\n";
|
|
}
|
|
let mut set_x = Chip8Video::new(initial_memory);
|
|
set_x.cls();
|
|
assert_eq!(set_x.format_as_string(), ws);
|
|
}
|
|
|
|
#[test]
|
|
fn poke_byte_test() {
|
|
let to_poke = 0b10101010;
|
|
let mut v = Chip8Video::default();
|
|
v.poke_byte(0x00, to_poke);
|
|
assert!(v.peek(0x00));
|
|
assert!(!v.peek(0x01));
|
|
assert!(v.peek(0x02));
|
|
assert!(!v.peek(0x03));
|
|
assert!(v.peek(0x04));
|
|
assert!(!v.peek(0x05));
|
|
assert!(v.peek(0x06));
|
|
assert!(!v.peek(0x07));
|
|
for i in 0x8..CHIP8_VIDEO_MEMORY {
|
|
assert!(!v.peek(i as u16));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn poke_multi_line_test() {
|
|
let mut v = Chip8Video::default();
|
|
let to_poke = [
|
|
0b00000000,
|
|
0b11111111,
|
|
0b10101010,
|
|
0b01010101
|
|
];
|
|
|
|
for (byte_in_set, byte_to_poke) in to_poke.iter().enumerate() {
|
|
let base_offset = byte_in_set * 64;
|
|
v.poke_byte(base_offset as u16, *byte_to_poke);
|
|
}
|
|
|
|
// row 2 column 1
|
|
{
|
|
assert!(v.clone().peek(0x40));
|
|
assert!(v.clone().peek(0x41));
|
|
assert!(v.clone().peek(0x42));
|
|
assert!(v.clone().peek(0x43));
|
|
assert!(v.clone().peek(0x44));
|
|
assert!(v.clone().peek(0x45));
|
|
assert!(v.clone().peek(0x46));
|
|
assert!(v.clone().peek(0x47));
|
|
|
|
// row 3 column 1
|
|
assert!(!v.peek(0xC0));
|
|
assert!(v.clone().peek(0xC1));
|
|
assert!(!v.clone().peek(0xC2));
|
|
assert!(v.clone().peek(0xC3));
|
|
assert!(!v.clone().peek(0xC4));
|
|
assert!(v.clone().peek(0xC5));
|
|
assert!(!v.clone().peek(0xC6));
|
|
assert!(v.clone().peek(0xC7));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn moved_poke_test() {
|
|
let mut v = Chip8Video::default();
|
|
let to_poke = [
|
|
0b00000000,
|
|
0b11111111,
|
|
0b10101010,
|
|
0b01010101
|
|
];
|
|
|
|
let x_offset = 20;
|
|
let y_offset = 5;
|
|
|
|
|
|
for (byte_in_set, byte_to_poke) in to_poke.iter().enumerate() {
|
|
let base_offset = (x_offset + byte_in_set) * 64 + y_offset;
|
|
v.poke_byte(base_offset as u16, *byte_to_poke);
|
|
}
|
|
|
|
let test_offset = (x_offset * 64 + y_offset) as u16;
|
|
assert!(!v.peek(test_offset));
|
|
assert!(!v.clone().peek(test_offset + 1));
|
|
assert!(!v.clone().peek(test_offset + 2));
|
|
assert!(!v.clone().peek(test_offset + 3));
|
|
assert!(!v.clone().peek(test_offset + 4));
|
|
assert!(!v.clone().peek(test_offset + 5));
|
|
assert!(!v.clone().peek(test_offset + 6));
|
|
assert!(!v.clone().peek(test_offset + 7));
|
|
|
|
let test_offset = test_offset + 0x40;
|
|
assert!(v.clone().peek(test_offset));
|
|
assert!(v.clone().peek(test_offset + 1));
|
|
assert!(v.clone().peek(test_offset + 2));
|
|
assert!(v.clone().peek(test_offset + 3));
|
|
assert!(v.clone().peek(test_offset + 4));
|
|
assert!(v.clone().peek(test_offset + 5));
|
|
assert!(v.clone().peek(test_offset + 6));
|
|
assert!(v.clone().peek(test_offset + 7));
|
|
}
|
|
|
|
#[test]
|
|
fn poke_sprite_test() {
|
|
let mut v = Chip8Video::default();
|
|
let to_poke = [
|
|
0b00000000,
|
|
0b11111111,
|
|
0b10101010,
|
|
0b01010101
|
|
];
|
|
|
|
v.poke_sprite(0x00, to_poke.into());
|
|
|
|
assert!(v.peek(0x40));
|
|
assert!(v.peek(0x41));
|
|
assert!(v.peek(0x42));
|
|
assert!(v.peek(0x43));
|
|
assert!(v.peek(0x44));
|
|
assert!(v.peek(0x45));
|
|
assert!(v.peek(0x46));
|
|
assert!(v.peek(0x47));
|
|
|
|
// row 3 column 1
|
|
assert!(!v.peek(0xC0));
|
|
assert!(v.peek(0xC1));
|
|
assert!(!v.peek(0xC2));
|
|
assert!(v.peek(0xC3));
|
|
assert!(!v.peek(0xC4));
|
|
assert!(v.peek(0xC5));
|
|
assert!(!v.peek(0xC6));
|
|
assert!(v.peek(0xC7));
|
|
}
|
|
|
|
#[test]
|
|
fn verify_change_registered() {
|
|
let mut v = Chip8Video::default();
|
|
v.poke(0x01, true);
|
|
v.poke(0x01, true);
|
|
assert!(v.has_frame_changed);
|
|
|
|
v.start_frame();
|
|
assert!(!v.has_frame_changed);
|
|
}
|
|
|
|
#[test]
|
|
fn write_checkboard() {
|
|
let mut v = build_checkerboard();
|
|
|
|
assert_eq!(v.format_as_string(), read_test_result("test_video_write_checkerboard.asc"));
|
|
}
|
|
|
|
#[test]
|
|
fn zero_test() {
|
|
let mut x = Chip8Video::default();
|
|
|
|
for (byte_index, data_offset) in (0..=0x100).step_by(0x40).enumerate() {
|
|
x.poke_byte(data_offset as u16, CHIP8FONT_0[byte_index]);
|
|
}
|
|
|
|
assert_eq!(read_test_result("test_video_zero.asc"), x.format_as_string());
|
|
}
|
|
|
|
#[test]
|
|
fn multi_sprite_test() {
|
|
let mut x = Chip8Video::default();
|
|
// draw a row of digits 01234567
|
|
let to_draw = [CHIP8FONT_0, CHIP8FONT_1, CHIP8FONT_2, CHIP8FONT_3, CHIP8FONT_4, CHIP8FONT_5, CHIP8FONT_6, CHIP8FONT_7];
|
|
for (index, sprite) in to_draw.iter().enumerate() {
|
|
let data_base_offset = index * 0x8;
|
|
for (index, offset) in (0..=0x100).step_by(0x40).enumerate() {
|
|
x.poke_byte((data_base_offset + offset) as u16, sprite[index]);
|
|
}
|
|
}
|
|
|
|
assert_eq!(read_test_result("test_multi_sprite.asc"), x.format_as_string());
|
|
}
|
|
|
|
#[test]
|
|
fn reset_test() {
|
|
let mut x = Chip8Video::default();
|
|
|
|
let to_draw = [CHIP8FONT_0, CHIP8FONT_1, CHIP8FONT_2, CHIP8FONT_3, CHIP8FONT_4, CHIP8FONT_5, CHIP8FONT_6, CHIP8FONT_7];
|
|
for (index, sprite) in to_draw.iter().enumerate() {
|
|
let data_base_offset = index * 0x8;
|
|
for (index, offset) in (0..=0x100).step_by(0x40).enumerate() {
|
|
x.poke_byte((data_base_offset + offset) as u16, sprite[index]);
|
|
}
|
|
}
|
|
|
|
x.reset();
|
|
assert_eq!(x.format_as_string(), read_test_result("test_reset_clears_video.asc"));
|
|
}
|
|
|
|
#[test]
|
|
fn collision_test() {
|
|
// Setup: Set 0xFF to 0x00 with a new frame ready
|
|
// Action: Run Poke to the same area
|
|
// Test: Verify the 'changed' flag is tripped
|
|
let mut x = Chip8Video::default();
|
|
x.poke_byte(0x00, 0xff);
|
|
x.tick();
|
|
// set the cell thats already set...
|
|
x.poke(0x00, true);
|
|
// it becomes unset and theres a frame changed
|
|
assert_eq!(false, x.peek(0x00));
|
|
|
|
assert_eq!(true, x.has_frame_changed);
|
|
}
|
|
|
|
#[test]
|
|
fn collision_test2() {
|
|
let mut x = Chip8Video::default();
|
|
x.poke_byte(0x00, 0b11110000);
|
|
assert_eq!(true, x.has_frame_changed);
|
|
x.tick();
|
|
assert_eq!(false, x.has_frame_changed);
|
|
// clear the 'has changed' flag
|
|
|
|
// now set a no-collision value
|
|
x.poke_byte(0x00, 0b00001111);
|
|
assert_eq!(true, x.has_frame_changed);
|
|
}
|
|
|
|
#[test]
|
|
fn peek_out_of_bounds_doesnt_panic() {
|
|
let x = Chip8Video::default();
|
|
|
|
let y = x.clone().peek(2049);
|
|
let y = x.clone().peek(0);
|
|
|
|
// if we got here we didn't panic
|
|
assert!(true);
|
|
}
|
|
}
|