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