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);
}
}