Skip to content

Commit

Permalink
Implemented basic background rendering!
Browse files Browse the repository at this point in the history
This is the results of days of research, it's finally
working, I just have to implement the FIFO and stuff
before actually ticking "Background Rendering" on
the `README.md`, i have yet to research that
  • Loading branch information
velllu committed Dec 3, 2023
1 parent 2abeb2f commit 9d1f4b8
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 15 deletions.
81 changes: 81 additions & 0 deletions examples/screen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::{
sync::{Arc, Mutex},
thread,
};

use emulator::{gpu::Color, GameBoy};
use macroquad::{
color::{Color as MacroColor, WHITE},
math::vec2,
miniquad::FilterMode,
texture::{draw_texture_ex, DrawTextureParams, Image, Texture2D},
window::{clear_background, next_frame},
};

const WIDTH: u16 = 160;
const HEIGHT: u16 = 144;

#[macroquad::main("Main")]
async fn main() {
// ROM Loading
let args: Vec<String> = std::env::args().collect();

if args.len() != 2 {
println!("You need to specify the rom file");
std::process::exit(1);
}

let rom_path = args.last().unwrap();

// We need to make the emulator run on a separate thread, because macroquad's
// `next_frame()` uses vsync, so the emulator will run based on your monitor refresh
// rate, and it will be insanelyyy slow, my 60hz monitor took 10 minutes to render
// ~10,000 CPU instructions, this is so fast that the same runs in <0.2s.
// Two `Arc`s because one is for the "running" thread and one for the "rendering"
// thread
let gameboy = Arc::new(Mutex::new(GameBoy::new(&rom_path).unwrap()));
let gameboy_clone = gameboy.clone();

let mut image = Image::gen_image_color(WIDTH, HEIGHT, WHITE);
let texture = Texture2D::from_image(&image);
texture.set_filter(FilterMode::Nearest); // without this it will be blurry

// "Running" thread
thread::spawn(move || loop {
gameboy.lock().unwrap().step();
});

// "Rendering" thread
loop {
clear_background(MacroColor::from_rgba(0, 255, 0, 255));

for (y_coordinate, y) in gameboy_clone.lock().unwrap().gpu.screen.iter().enumerate() {
for (x_coordinate, x) in y.iter().enumerate() {
image.set_pixel(
x_coordinate as u32,
y_coordinate as u32,
match x {
Color::Dark => MacroColor::from_rgba(0, 0, 0, 255),
Color::MediumlyDark => MacroColor::from_rgba(55, 55, 55, 255),
Color::MediumlyLight => MacroColor::from_rgba(155, 155, 155, 255),
Color::Light => MacroColor::from_rgba(255, 255, 255, 255),
},
);
}
}

texture.update(&image);
draw_texture_ex(
&texture,
0.0,
0.0,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(400., 400.)),
..Default::default()
},
);

next_frame().await;
}
}
3 changes: 3 additions & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ pub(crate) mod bus {

pub(crate) mod gpu {
pub(crate) const LY: u16 = 0xFF44;
pub(crate) const SCY: u16 = 0xFF42;
pub(crate) const SCX: u16 = 0xFF43;
pub(crate) const LCDC: u16 = 0xFF40;
}

pub(crate) mod display {
Expand Down
3 changes: 1 addition & 2 deletions src/gpu/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
pub(crate) mod fifo;
pub(crate) mod states;
pub mod tile_parser;

#[derive(Clone, Copy, PartialEq)]
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Color {
Dark,
MediumlyDark,
Expand Down
48 changes: 43 additions & 5 deletions src/gpu/states.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use crate::{
common::Bit,
consts::{
display::{DISPLAY_SIZE_X, DISPLAY_SIZE_Y},
gpu::LY,
gpu::{LY, SCX, SCY},
},
GameBoy,
};

use super::Color;

#[derive(PartialEq)]
pub enum GPUState {
OAMSearch,
Expand All @@ -18,10 +21,12 @@ pub struct Gpu {
pub state: GPUState,

/// This keeps track of the pixel outputted in the current scanline
already_outputted_pixel: u32,
already_outputted_pixel: u16,

/// A tick is 1/4 of a CPU cycle
ticks: u32,

pub screen: [[Color; DISPLAY_SIZE_X]; DISPLAY_SIZE_Y],
}

impl Gpu {
Expand All @@ -30,6 +35,7 @@ impl Gpu {
state: GPUState::OAMSearch,
already_outputted_pixel: 0,
ticks: 0,
screen: [[Color::Light; DISPLAY_SIZE_X]; DISPLAY_SIZE_Y],
}
}
}
Expand All @@ -50,10 +56,42 @@ impl GameBoy {
}

fn pixel_transfer(&mut self) {
// TODO: Document and implement this
let tile_map_address: u16 = match self.bus[0xFF40].get_bit(3) {
false => 0x9800,
true => 0x9C00,
};

let mut i = 0;

// Y Scrolling
// The gameboy tilemap is 32x32 tiles, both `SCX` and `SCY` use pixels, not tiles
// so we have to divide them by 8, skipping 32 tiles just means to set the
// "cursor" on the line below
for _ in 0..(self.bus[SCY] / 8) {
i += 32;
}

for y in 0..18 {
for x in 0..20 {
// X Scrolling
// We add the number of tiles to skip to the adress
self.draw_tile(
self.bus[tile_map_address + i + (self.bus[SCX] as u16 / 8)],
y * 8,
x * 8,
);

i += 1;
}

// 12 is the number to skip to go from the end of the viewport, skip the
// the tiles that don't need to be rendered, and end up at the next line of
// the viewport (32 - 20)
i += 12;
}

self.gpu.already_outputted_pixel += 1;
if self.gpu.already_outputted_pixel == DISPLAY_SIZE_X as u32 {
self.gpu.already_outputted_pixel += 8;
if self.gpu.already_outputted_pixel == DISPLAY_SIZE_X as u16 {
self.gpu.state = GPUState::HBlank;
}
}
Expand Down
21 changes: 13 additions & 8 deletions src/gpu/tile_parser.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{common::Bit, consts::gpu::LCDC, GameBoy};

use super::{states::Gpu, Color};
use super::Color;

pub struct Tile {
pub colors: [[Color; 8]; 8],
Expand Down Expand Up @@ -48,24 +48,29 @@ impl Tile {
}

impl GameBoy {
pub(crate) fn draw_tile(&mut self, tile_number: u8) {
pub(crate) fn draw_tile(&mut self, tile_number: u8, x_offset: usize, y_offset: usize) {
let mut tile = Tile::new_blank();

// When the gameboy converts from u8 to u16, in this case, it adds 0s on the
// right instead of the left, so `0xF8` becomes `0xF800` instead of `0x00F8` as
// one might expect
let tile_number: u16 = (tile_number as u16) << 4;

let tile_data_address: u16 = match self.bus[LCDC].get_bit(4) {
false => 0x8800,
true => 0x8000,
};

for i in (0..16).step_by(2) {
let low = self.bus[tile_data_address + tile_number as u16 + i];
let high = self.bus[tile_data_address + tile_number as u16 + i + 1];
let low = self.bus[tile_data_address + tile_number + i];
let high = self.bus[tile_data_address + tile_number + i + 1];

tile.draw_line(low, high, (i / 2) as usize);
tile.draw_line(high, low, (i / 2) as usize);
}

for (y_coordinate, y) in tile.colors.iter().enumerate() {
for (x_coordinate, x) in y.iter().enumerate() {
self.gpu.screen[x_coordinate][y_coordinate] = *x;
for (x_coordinate, x) in tile.colors.iter().enumerate() {
for (y_coordinate, y) in x.iter().enumerate() {
self.gpu.screen[x_coordinate + x_offset][y_coordinate + y_offset] = *y;
}
}
}
Expand Down

0 comments on commit 9d1f4b8

Please sign in to comment.