Chip-8: From Rect to Textures

The main contribution of this commit is changing the display rendering method. Before, I was asking the canvas to render rectangles, which represented scaled pixesl. Now I am using a texture, that is automatically stretched to the whole canvas by SDL.

The display coding is running faster, but there is still some ways to improve rendering. The main one is to change the cpu display buffer from a multi-dimensional array of booleans (indincating pixels toggling) to a single dimension pixel-color array. In the pixel-color array, 4u8 samples represent a single pixel on screen (R: u8, G: u8, B: u8, A:u8).

This refactor will force the CPU to control have knowledge of which color will be printed on the display, letting the display being agnostic to implementation details.

This refactor will be added as an issue to the github project kanban and further analyzed.

Besides that, other changes to the codebase were mainly in terms of code organization.
This commit is contained in:
Marcus Vinicius de Carvalho 2021-07-30 15:02:08 +08:00
parent d6430f0a2e
commit 03b943eeca
6 changed files with 77 additions and 79 deletions

View File

@ -1,43 +1,48 @@
use super::cpu::cpu::CPU; use super::cpu::cpu::CPU;
use super::display::Display; use super::display::Display;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use std::io::Read; use std::io::Read;
pub fn run() { pub fn run() {
let mut cpu = CPU::new(); let mut cpu = CPU::new();
load_rom("src/chip_8/roms/CAVE.ch8", &mut cpu); load_rom(
"src/chip_8/roms/Trip8 Demo (2008) [Revival Studios].ch8",
&mut cpu,
);
let sdl_context = sdl2::init().unwrap(); let sdl_context = sdl2::init().unwrap();
let mut display = Display::init(&sdl_context, cpu.get_display_scale()); let mut display = Display::init(&sdl_context, cpu.get_display_scale());
let mut event_listener = sdl_context.event_pump().unwrap(); let mut event_pump = sdl_context.event_pump().unwrap();
'runner: loop { 'runner: loop {
for event in event_listener.poll_iter() { for event in event_pump.poll_iter() {
match event { match event {
sdl2::event::Event::Quit { .. } => break 'runner, Event::Quit { .. } => break 'runner,
sdl2::event::Event::KeyDown { Event::KeyDown {
keycode: Some(sdl2::keyboard::Keycode::Escape), keycode: Some(Keycode::Escape),
.. ..
} => break 'runner, } => break 'runner,
sdl2::event::Event::KeyDown { Event::KeyDown {
keycode: Some(sdl2::keyboard::Keycode::RightBracket), keycode: Some(Keycode::RightBracket),
.. ..
} => { } => {
cpu.increase_clock(true); cpu.increase_clock(true);
} }
sdl2::event::Event::KeyDown { Event::KeyDown {
keycode: Some(sdl2::keyboard::Keycode::LeftBracket), keycode: Some(Keycode::LeftBracket),
.. ..
} => { } => {
cpu.decrease_clock(true); cpu.decrease_clock(true);
} }
sdl2::event::Event::KeyDown { Event::KeyDown {
keycode: Some(sdl2::keyboard::Keycode::Backspace), keycode: Some(Keycode::Backspace),
.. ..
} => { } => {
cpu.reset_rom(); cpu.reset_rom();
} }
sdl2::event::Event::KeyDown { Event::KeyDown {
keycode: Some(keycode), keycode: Some(keycode),
.. ..
} => { } => {
@ -45,7 +50,7 @@ pub fn run() {
cpu.press_key(key_index); cpu.press_key(key_index);
} }
} }
sdl2::event::Event::KeyUp { Event::KeyUp {
keycode: Some(keycode), keycode: Some(keycode),
.. ..
} => { } => {

View File

@ -16,22 +16,16 @@ impl Clock {
} }
pub fn tick(&mut self) -> bool { pub fn tick(&mut self) -> bool {
let mut res: bool = false; if let Ok(elapsed) = self.elapsed.elapsed() {
match self.elapsed.elapsed() { if elapsed.as_secs_f64() >= self.clock_hz_as_secs_f64() {
Ok(elapsed) => { if self.tick > 0 {
if elapsed.as_secs_f64() >= self.clock_hz_as_secs_f64() { self.tick -= 1;
if self.tick > 0 {
self.tick -= 1;
}
self.reset_elapsed();
res = true;
} }
} self.reset_elapsed();
Err(e) => { return true;
println!("Error: {:?}", e);
} }
} }
res false
} }
fn reset_elapsed(&mut self) { fn reset_elapsed(&mut self) {

View File

@ -10,16 +10,16 @@ use sdl2::keyboard::Keycode;
use rand::Rng; use rand::Rng;
pub struct CPU { pub struct CPU {
stack: Vec<usize>, // Function Stack stack: Vec<usize>, // Function Stack
dt: Clock, // Delay Timer dt: Clock, // Delay Timer
st: Clock, // Sound Timer st: Clock, // Sound Timer
clock: Clock, // CPU Clock clock: Clock, // CPU Clock
regs: Registers, // Registers regs: Registers, // Registers
ram: RAM, // RAM ram: RAM, // RAM
keypad: Keypad, // Keypad keypad: Keypad, // Keypad
db: DisplayBuffer, // Display Buffer db: DisplayBuffer, // Display Buffer
op: OpCodes, // Operation Code, op: OpCodes, // Operation Code,
pub should_redraw: bool // Boolean indicating Display Buffer update pub should_redraw: bool, // Boolean indicating Display Buffer update
} }
impl CPU { impl CPU {
@ -37,7 +37,7 @@ impl CPU {
keypad: Keypad::new(), keypad: Keypad::new(),
db: DisplayBuffer::new(10), db: DisplayBuffer::new(10),
op: OpCodes::new(0000), op: OpCodes::new(0000),
should_redraw: false should_redraw: false,
} }
} }
@ -189,7 +189,7 @@ impl CPU {
pub fn set_delay_timer(&mut self, tick: u8) { pub fn set_delay_timer(&mut self, tick: u8) {
self.dt.tick = tick; self.dt.tick = tick;
} }
pub fn set_sound_timer(&mut self, tick: u8) { pub fn set_sound_timer(&mut self, tick: u8) {
self.st.tick = tick; self.st.tick = tick;
@ -343,8 +343,7 @@ fn op_bnnn(cpu: &mut CPU) {
fn op_cxnn(cpu: &mut CPU) { fn op_cxnn(cpu: &mut CPU) {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
cpu.regs cpu.regs.set(cpu.op.x, rng.gen_range(0x0..0xFF) & cpu.op.nn);
.set(cpu.op.x, rng.gen_range(0x0..0xFF) & cpu.op.nn);
} }
fn op_dxyn(cpu: &mut CPU) { fn op_dxyn(cpu: &mut CPU) {
@ -438,4 +437,4 @@ fn op_fx65(cpu: &mut CPU) {
for regs in 0x0..(cpu.op.x + 1) { for regs in 0x0..(cpu.op.x + 1) {
cpu.regs.set(regs, cpu.ram.read8(i + regs)); cpu.regs.set(regs, cpu.ram.read8(i + regs));
} }
} }

View File

@ -35,10 +35,10 @@ impl Keypad {
} }
pub fn compute_keycode(&self, keycode: Keycode) -> Option<usize> { pub fn compute_keycode(&self, keycode: Keycode) -> Option<usize> {
match self.keys.get(&keycode) { if let Some(&chip8_key) = self.keys.get(&keycode) {
Some(chip8_key) => Some(*chip8_key), return Some(chip8_key);
None => None,
} }
None
} }
pub fn get_status(&mut self, pos: usize) -> bool { pub fn get_status(&mut self, pos: usize) -> bool {

View File

@ -16,9 +16,9 @@ pub struct Registers {
x_c: u8, x_c: u8,
x_d: u8, x_d: u8,
x_e: u8, x_e: u8,
x_f: u8, // Flag register x_f: u8, // Flag register
pub pc: usize, // Program Counter pub pc: usize, // Program Counter
pub i: usize // Index register pub i: usize, // Index register
} }
impl Registers { impl Registers {
@ -64,7 +64,7 @@ impl Registers {
0xD => self.x_d, 0xD => self.x_d,
0xE => self.x_e, 0xE => self.x_e,
0xF => self.x_f, 0xF => self.x_f,
_ => 0, _ => panic!("Trying to access non-existing register"),
} }
} }
@ -87,7 +87,7 @@ impl Registers {
0xD => self.x_d = value, 0xD => self.x_d = value,
0xE => self.x_e = value, 0xE => self.x_e = value,
0xF => self.x_f = value, 0xF => self.x_f = value,
_ => {} _ => panic!("Trying to access non-existing register"),
} }
} }

View File

@ -1,56 +1,56 @@
use sdl2::render::Canvas;
use sdl2::pixels::Color; use sdl2::pixels::Color;
use sdl2::rect::Rect; use sdl2::pixels::PixelFormatEnum;
use sdl2::render::Canvas;
use sdl2::video::Window;
const WIDTH: usize = 64; const WIDTH: usize = 64;
const HEIGHT: usize = 32; const HEIGHT: usize = 32;
const PITCH_BYTES: usize = std::mem::size_of::<u32>(); // 4 bytes: R G B A, from colors
const PITCH: usize = WIDTH * PITCH_BYTES;
pub struct Display { pub struct Display {
canvas: Canvas<sdl2::video::Window>, canvas: Canvas<Window>,
scale: u32, off_color: [u8; PITCH_BYTES],
off_color: Color, on_color: [u8; PITCH_BYTES],
on_color: Color,
} }
impl Display { impl Display {
pub fn init(sdl_context: &sdl2::Sdl, scale: u32) -> Display { pub fn init(sdl_context: &sdl2::Sdl, scale: u32) -> Display {
let video_subsystem = sdl_context.video().unwrap(); let canvas = sdl_context.video().unwrap()
let window = video_subsystem
.window("Chip-8", WIDTH as u32 * scale, HEIGHT as u32 * scale) .window("Chip-8", WIDTH as u32 * scale, HEIGHT as u32 * scale)
.position_centered() .resizable().position_centered().build().unwrap()
.build() .into_canvas().build().unwrap();
.unwrap();
let canvas = window.into_canvas().build().unwrap(); let off_color = Color::RGBA(0, 0, 0, 255);
let on_color = Color::RGBA(255, 255, 255, 255);
Display { Display {
canvas: canvas, canvas: canvas,
scale: scale, off_color: [off_color.r, off_color.g, off_color.b, off_color.a],
off_color: Color::RGB(0, 0, 0), on_color: [on_color.r, on_color.g, on_color.b, on_color.a],
on_color: Color::RGB(255, 255, 255),
} }
} }
pub fn draw(self: &mut Display, buffer: &[[bool; HEIGHT]; WIDTH]) { pub fn draw(self: &mut Display, buffer: &[[bool; HEIGHT]; WIDTH]) {
self.canvas.set_draw_color(self.off_color); self.apply_texture(buffer);
self.canvas.clear(); self.canvas.present();
}
self.canvas.set_draw_color(self.on_color); fn apply_texture(&mut self, buffer: &[[bool; HEIGHT]; WIDTH]) {
let mut framebuffer: [u8; HEIGHT * PITCH] = [0; HEIGHT * PITCH];
for (x, col) in buffer.iter().enumerate() { for (x, col) in buffer.iter().enumerate() {
for (y, pixel) in col.iter().enumerate() { for (y, pixel) in col.iter().enumerate() {
if *pixel { let offset = x * PITCH_BYTES + y * PITCH;
let x = (x as u32 * self.scale) as i32; let color: &[u8; PITCH_BYTES] = if *pixel { &self.on_color } else { &self.off_color };
let y = (y as u32 * self.scale) as i32; for i in offset..(offset + PITCH_BYTES) { framebuffer[i] = color[i - offset]; }
let width = self.scale;
let height = self.scale;
self.canvas
.fill_rect(Rect::new(x, y, width, height))
.expect("Failed to draw pixel");
}
} }
} }
self.canvas.present(); let texture_creator = self.canvas.texture_creator();
let mut texture = texture_creator
.create_texture_streaming(PixelFormatEnum::RGBA32, WIDTH as u32, HEIGHT as u32)
.unwrap();
texture.update(None, &framebuffer, PITCH).unwrap();
self.canvas.copy(&texture, None, None).unwrap();
} }
} }