Skip to content

Commit

Permalink
introduce progressive scanline rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanballs committed Sep 15, 2024
1 parent 177369e commit e5cbc7a
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 127 deletions.
10 changes: 5 additions & 5 deletions src/cpu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,11 @@ impl CPU {

// stat
if mmu.ie & 2 > 0 && mmu.ppu.stat_irq {
//mmu.ppu.stat_irq = false;
//self.ime = false;
//
//self.push(mmu, return_pc);
//self.registers.pc = 0x48;
mmu.ppu.stat_irq = false;
self.ime = false;

self.push(mmu, return_pc);
self.registers.pc = 0x48;
}

// timer
Expand Down
7 changes: 3 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ use std::thread;

use gameboy::GameBoy;
use minifb::Key;
use mmu::ppu::PPU;
use renderer::window_loop;

pub static DEBUG_MODE: AtomicBool = AtomicBool::new(false);
Expand Down Expand Up @@ -48,7 +47,7 @@ fn main() {
enable_gameboy_doctor();
}

let (tx, rx) = mpsc::channel::<PPU>();
let (tx, rx) = mpsc::channel::<Vec<u32>>();
let (tx_key, rx_key) = mpsc::channel::<(bool, Key)>();
let rom = read_file_to_bytes(rom_path.as_str()).unwrap();
let game_title = CartridgeHeader::new(&rom).unwrap().title();
Expand All @@ -57,7 +56,7 @@ fn main() {
window_loop(rx, tx_key, &game_title);
}

fn emulator_loop(rom: Vec<u8>, tx: Sender<PPU>, rx: Receiver<(bool, Key)>) {
fn emulator_loop(rom: Vec<u8>, tx: Sender<Vec<u32>>, rx: Receiver<(bool, Key)>) {
let mut gameboy = GameBoy::new(rom);

ctrlc::set_handler(move || {
Expand All @@ -84,7 +83,7 @@ fn emulator_loop(rom: Vec<u8>, tx: Sender<PPU>, rx: Receiver<(bool, Key)>) {

// Render window
if gameboy.mmu.ppu.get_and_reset_frame_available() {
let _ = tx.send(gameboy.mmu.ppu.clone());
let _ = tx.send(gameboy.mmu.ppu.frame_buffer.clone());
}

// Handle joypad input
Expand Down
115 changes: 99 additions & 16 deletions src/mmu/ppu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,27 @@ use crate::debugger::is_gameboy_doctor;
const VRAM_SIZE: usize = 0x2000;
const VOAM_SIZE: usize = 0xA0;

const SCREEN_WIDTH: usize = 160;
const SCREEN_HEIGHT: usize = 144;

const TARGET_FPS: f64 = 30.0;

pub type Tile = [[u8; 8]; 8];

fn palette(id: u8) -> u32 {
match id {
0x0 => 0xFFFFFFFF,
0x1 => 0xFF666666,
0x2 => 0xFFBBBBBB,
0x3 => 0xFF000000,
_ => unreachable!(),
}
}

#[derive(Clone)]
pub struct PPU {
pub frame_buffer: Vec<u32>,

frame_available: bool,
frame_number: u32,
last_frame_time: Instant,
Expand Down Expand Up @@ -41,6 +58,7 @@ pub struct PPU {
impl PPU {
pub fn new() -> PPU {
PPU {
frame_buffer: vec![0; SCREEN_WIDTH * SCREEN_HEIGHT],
frame_available: false,
frame_number: 1,
last_frame_time: Instant::now(),
Expand Down Expand Up @@ -89,11 +107,15 @@ impl PPU {
self.stat_irq = true;
}

if self.ly < SCREEN_HEIGHT as u8 {
self.render_scanline(self.ly);
}

// Frame finished - flush to screen
if self.ly == 0 {
// Calculate how long to sleep
let elapsed = self.last_frame_time.elapsed();
let frame_duration = Duration::from_secs_f64(1.0 / 60.0);
let frame_duration = Duration::from_secs_f64(1.0 / TARGET_FPS);

if elapsed < frame_duration {
thread::sleep(frame_duration - elapsed);
Expand Down Expand Up @@ -148,7 +170,7 @@ impl PPU {
0xFF48 => self.obj_palette_0,
0xFF49 => self.obj_palette_1,

0xFe00..=0xFE9F => self.voam[(addr - 0xFE00) as usize],
0xFE00..=0xFE9F => self.voam[(addr - 0xFE00) as usize],

_ => {
println!("tried to read {:#04x}", addr);
Expand Down Expand Up @@ -195,14 +217,31 @@ impl PPU {
return result;
}

pub fn get_tile(&self, tile_index: u8) -> Tile {
let start_address = if self.lcdc & 0x10 > 0 {
pub fn get_tile_pixel(
&self,
tile_index: u8,
line_index: u16,
col_index: u16,
use_8000: bool,
) -> u8 {
let start_address = if self.lcdc & 0x10 > 0 || use_8000 {
0x8000 + ((tile_index as u16) * 16) as u16
} else {
let offset = ((tile_index as i8) as i16) * 16;
0x9000_u16.wrapping_add(offset as u16)
};

let byte_a = self.get_byte(start_address + (2 * line_index));
let byte_b = self.get_byte(start_address + (2 * line_index) + 1);

let bit1 = (byte_a >> 7 - col_index) & 1;
let bit2 = (byte_b >> 7 - col_index) & 1;

return ((bit2 << 1) | bit1) as u8;
}

pub fn get_object(&self, tile_index: u8) -> Tile {
let start_address = 0x8000 + ((tile_index as u16) * 16) as u16;
let mut ret = [[0u8; 8]; 8];

for i in 0..8 {
Expand All @@ -220,23 +259,67 @@ impl PPU {
ret
}

pub fn get_object(&self, tile_index: u8) -> Tile {
let start_address = 0x8000 + ((tile_index as u16) * 16) as u16;
let mut ret = [[0u8; 8]; 8];
fn render_scanline(&mut self, line: u8) {
// calculate background scanline
let buffer_y_offset = (line as usize) * SCREEN_WIDTH;
let background_y = line.wrapping_add(self.scy);
let tile_map_row = background_y / 8;

for i in 0..8 {
let byte_a = self.get_byte(start_address + (2 * i));
let byte_b = self.get_byte(start_address + (2 * i) + 1);
for buffer_x_offset in 0..SCREEN_WIDTH {
let background_x = (buffer_x_offset + self.scx as usize) % 256;

for j in 0..8 {
let bit1 = (byte_a >> 7 - j) & 1;
let bit2 = (byte_b >> 7 - j) & 1;
let tile_map_index = (tile_map_row as usize * 32) + (background_x / 8);
let tile_index = self.get_byte(0x9800 + tile_map_index as u16);

ret[j as usize][i as usize] = ((bit2 << 1) | bit1) as u8;
}
let tile_line = background_y % 8;
let tile_col = background_x % 8;

let tile_pixel =
self.get_tile_pixel(tile_index, tile_line as u16, tile_col as u16, false);

self.frame_buffer[buffer_y_offset + buffer_x_offset] = palette(tile_pixel);
}

ret
// For each object we should
for i in 0..40 {
let start_position = 0xFE00 + i * 4;

let object_y = self.get_byte(start_position);
let object_line = line.wrapping_sub(object_y).wrapping_add(16);
if object_line >= 8 {
continue;
}
let x_position = self.get_byte(start_position + 1);
let tile_index = self.get_byte(start_position + 2);
let flags = self.get_byte(start_position + 3);

let x_flip = flags & 0x20 == 0x20;
let y_flip = flags & 0x40 == 0x40;

for x_offset in 0..8 {
let x_position = if x_flip {
(x_position as usize).wrapping_sub(x_offset as usize)
} else {
(x_position as usize)
.wrapping_add(x_offset as usize)
.wrapping_sub(8)
};

if x_position >= SCREEN_WIDTH {
continue;
}

let tile_line = if y_flip { 8 - object_line } else { object_line };

let tile_pixel = self.get_tile_pixel(tile_index, tile_line as u16, x_offset, true);

if tile_pixel == 0 {
continue;
}

self.frame_buffer[buffer_y_offset + x_position] = palette(tile_pixel);
}
}
}
}

Expand Down
113 changes: 11 additions & 102 deletions src/renderer.rs
Original file line number Diff line number Diff line change
@@ -1,62 +1,14 @@
use minifb::{Key, KeyRepeat, Scale, Window, WindowOptions};
use std::sync::mpsc::{Receiver, Sender, TryRecvError};

use crate::mmu::ppu::{Tile, PPU};
use std::{
collections::VecDeque,
sync::mpsc::{Receiver, Sender, TryRecvError},
time::Instant,
};

const SCREEN_WIDTH: usize = 160;
const SCREEN_HEIGHT: usize = 144;

fn palette(id: u8) -> u32 {
match id {
0x0 => 0xFFFFFFFF,
0x1 => 0xFF666666,
0x2 => 0xFFBBBBBB,
0x3 => 0xFF000000,
_ => unreachable!(),
}
}

fn render_tile(
buffer: &mut Vec<u32>,
tile: Tile,
screen_x: u8,
screen_y: u8,
flags: u8,
transparency: bool,
) {
let x_flip = (flags & 0x20) > 0;
let y_flip = (flags & 0x40) > 0;

for tile_y in 0..8 {
for tile_x in 0..8 {
let x_offset: usize = if x_flip {
screen_x.wrapping_add(8).wrapping_sub(tile_x) as usize
} else {
screen_x.wrapping_add(tile_x) as usize
};

let y_offset: usize = if y_flip {
let o = screen_y.wrapping_add(8).wrapping_sub(tile_y) as usize;
o * SCREEN_WIDTH
} else {
let o = screen_y.wrapping_add(tile_y) as usize;
o * SCREEN_WIDTH
};

if x_offset >= SCREEN_WIDTH || y_offset > (SCREEN_HEIGHT - 1) * SCREEN_WIDTH {
continue;
}

if transparency && tile[tile_x as usize][tile_y as usize] == 0 {
continue;
}

buffer[y_offset + x_offset] = palette(tile[tile_x as usize][tile_y as usize]);
}
}
}

fn latest_ppu(rx: &Receiver<PPU>) -> Option<PPU> {
fn most_recent_frame(rx: &Receiver<Vec<u32>>) -> Option<Vec<u32>> {
let mut latest_frame = None;

loop {
Expand All @@ -77,9 +29,7 @@ fn latest_ppu(rx: &Receiver<PPU>) -> Option<PPU> {
latest_frame
}

pub fn window_loop(rx: Receiver<PPU>, tx: Sender<(bool, Key)>, game_title: &String) {
let mut buffer: Vec<u32> = vec![0; SCREEN_WIDTH as usize * SCREEN_HEIGHT as usize];

pub fn window_loop(rx: Receiver<Vec<u32>>, tx: Sender<(bool, Key)>, game_title: &String) {
let mut window = Window::new(
format!("Cowboy Emulator - {}", game_title).as_str(),
SCREEN_WIDTH as usize,
Expand All @@ -92,51 +42,10 @@ pub fn window_loop(rx: Receiver<PPU>, tx: Sender<(bool, Key)>, game_title: &Stri
.unwrap();

while window.is_open() && !window.is_key_down(Key::Escape) {
// We unwrap here as we want this code to exit if it fails. Real applications may want to
// handle this in a different way
window
.update_with_buffer(&buffer, SCREEN_WIDTH as usize, SCREEN_HEIGHT as usize)
.unwrap();

// Receive frame buffer from the emulator
if let Some(ppu) = latest_ppu(&rx) {
// Draw tiles
for i in 0..1024 {
let tile_index = ppu.get_byte(0x9800 + i);

// Calculate location on the background map
let tile_x = ((i % 32) * 8) as u8;
let tile_y = ((i / 32) * 8) as u8;

render_tile(
&mut buffer,
ppu.get_tile(tile_index),
tile_x.wrapping_sub(ppu.scx), // ensure wrapping
tile_y.wrapping_sub(ppu.scy),
0x0,
false,
)
}

// Draw objects
for i in 0..40 {
let start_position = 0xFE00 + i * 4;

let y_position = ppu.get_byte(start_position);

let x_position = ppu.get_byte(start_position + 1);
let tile_index = ppu.get_byte(start_position + 2);
let flags = ppu.get_byte(start_position + 3);

render_tile(
&mut buffer,
ppu.get_object(tile_index),
x_position.wrapping_sub(8),
y_position.wrapping_sub(16),
flags,
true,
);
}
if let Some(frame_buffer) = most_recent_frame(&rx) {
window
.update_with_buffer(&frame_buffer, SCREEN_WIDTH as usize, SCREEN_HEIGHT as usize)
.unwrap();
}

// dispatch unreleased keys
Expand Down

0 comments on commit e5cbc7a

Please sign in to comment.