441 lines
14 KiB
Rust
441 lines
14 KiB
Rust
use self::{
|
|
tile_window::TileWindow,
|
|
types::{
|
|
Colour, DrawMode, ObjSize, Object, ObjectFlags, Palette, TiledataArea, TilemapArea, LCDC,
|
|
},
|
|
};
|
|
use crate::{
|
|
processor::{clear_bit, get_bit, set_bit, set_or_clear_bit, CPU},
|
|
FACTOR, HEIGHT, WIDTH,
|
|
};
|
|
use minifb::{Window, WindowOptions};
|
|
|
|
mod tile_window;
|
|
mod types;
|
|
|
|
const TILE_WINDOW_WIDTH: usize = 16 * 8;
|
|
const TILE_WINDOW_HEIGHT: usize = 24 * 8;
|
|
const TILE_WINDOW_WIDTH_SCALED: usize = TILE_WINDOW_WIDTH * FACTOR;
|
|
const TILE_WINDOW_HEIGHT_SCALED: usize = TILE_WINDOW_HEIGHT * FACTOR;
|
|
|
|
pub struct GPU {
|
|
pub buffer: Vec<u32>,
|
|
scaled_buffer: Vec<u32>,
|
|
mode: DrawMode,
|
|
mode_clock: usize,
|
|
scanline: u8,
|
|
tile_window: Option<TileWindow>,
|
|
window_lc: u8,
|
|
has_window_been_enabled: bool,
|
|
}
|
|
|
|
impl GPU {
|
|
pub(super) fn new(enable_tile_window: bool) -> Self {
|
|
let tile_window = if enable_tile_window {
|
|
let mut window = Window::new(
|
|
"Tiles",
|
|
TILE_WINDOW_WIDTH_SCALED,
|
|
TILE_WINDOW_HEIGHT_SCALED,
|
|
WindowOptions::default(),
|
|
)
|
|
.unwrap_or_else(|e| {
|
|
panic!("{}", e);
|
|
});
|
|
|
|
window.set_position((550 + (WIDTH * FACTOR)) as isize, 50);
|
|
window.topmost(true);
|
|
Some(TileWindow::new(window))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Self {
|
|
buffer: vec![0; WIDTH * HEIGHT],
|
|
scaled_buffer: vec![0; WIDTH * HEIGHT * 4],
|
|
mode: DrawMode::Mode2,
|
|
mode_clock: 0,
|
|
scanline: 0,
|
|
tile_window,
|
|
window_lc: 0,
|
|
has_window_been_enabled: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CPU {
|
|
pub fn advance_gpu_clock(&mut self, steps: u8) {
|
|
let lcdc = self.get_lcdc();
|
|
if lcdc.enable {
|
|
let real_steps = (steps as usize) * 4;
|
|
self.gpu.mode_clock += real_steps;
|
|
match self.gpu.mode {
|
|
DrawMode::HBlank => {
|
|
// mode 0: hblank
|
|
if self.gpu.mode_clock >= 204 {
|
|
self.gpu.mode_clock = 0;
|
|
self.gpu.scanline += 1;
|
|
if self.gpu.scanline == 143 {
|
|
self.enter_vblank();
|
|
} else {
|
|
self.gpu.mode = DrawMode::Mode2;
|
|
}
|
|
}
|
|
}
|
|
DrawMode::VBlank => {
|
|
// mode 1: vblank
|
|
if self.gpu.mode_clock >= 456 {
|
|
self.gpu.mode_clock = 0;
|
|
self.gpu.scanline += 1;
|
|
if self.gpu.scanline == 153 {
|
|
self.exit_vblank();
|
|
}
|
|
}
|
|
}
|
|
DrawMode::Mode2 => {
|
|
// search oam for sprites on this line
|
|
// we dont really have to emulate this
|
|
if self.gpu.mode_clock >= 80 {
|
|
self.gpu.mode_clock = 0;
|
|
self.gpu.mode = DrawMode::Mode3;
|
|
}
|
|
}
|
|
DrawMode::Mode3 => {
|
|
// generate scanline
|
|
if self.gpu.mode_clock >= 172 {
|
|
self.gpu.mode_clock = 0;
|
|
self.enter_hblank(&lcdc);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
self.gpu.mode_clock = 0;
|
|
self.gpu.mode = DrawMode::VBlank;
|
|
self.gpu.scanline = 0;
|
|
}
|
|
self.set_lcd_status();
|
|
}
|
|
|
|
fn get_lcdc(&self) -> LCDC {
|
|
let reg = self.memory.get(0xFF40);
|
|
LCDC {
|
|
enable: get_bit(reg, 7),
|
|
window_tilemap: if get_bit(reg, 6) {
|
|
TilemapArea::T9C00
|
|
} else {
|
|
TilemapArea::T9800
|
|
},
|
|
window_enable: get_bit(reg, 5),
|
|
tile_area: if get_bit(reg, 4) {
|
|
TiledataArea::D8000
|
|
} else {
|
|
TiledataArea::D9000
|
|
},
|
|
bg_tilemap: if get_bit(reg, 3) {
|
|
TilemapArea::T9C00
|
|
} else {
|
|
TilemapArea::T9800
|
|
},
|
|
obj_size: if get_bit(reg, 2) {
|
|
ObjSize::S8x16
|
|
} else {
|
|
ObjSize::S8x8
|
|
},
|
|
obj_enable: get_bit(reg, 1),
|
|
bg_window_enable: get_bit(reg, 0),
|
|
}
|
|
}
|
|
|
|
fn enter_hblank(&mut self, lcdc: &LCDC) {
|
|
self.gpu.mode = DrawMode::HBlank;
|
|
self.render_scanline(self.gpu.scanline, lcdc);
|
|
}
|
|
|
|
fn enter_vblank(&mut self) {
|
|
if self.memory.update_pressed_keys(self.window.get_keys()) {
|
|
self.memory.set(0xFF0F, set_bit(self.memory.get(0xFF0F), 4));
|
|
}
|
|
self.gpu.mode = DrawMode::VBlank;
|
|
self.render_window();
|
|
self.memory.set(0xFF0F, set_bit(self.memory.get(0xFF0F), 0));
|
|
}
|
|
|
|
fn exit_vblank(&mut self) {
|
|
self.gpu.mode = DrawMode::Mode2;
|
|
self.gpu.scanline = 0;
|
|
self.gpu.window_lc = 0;
|
|
self.gpu.has_window_been_enabled = false;
|
|
if let Some(tile_window) = &mut self.gpu.tile_window {
|
|
tile_window.draw_sprite_window(byte_to_palette(self.memory.get(0xFF47)), &self.memory);
|
|
}
|
|
}
|
|
|
|
fn set_lcd_status(&mut self) {
|
|
let mut stat = self.memory.get(0xFF41);
|
|
|
|
let lyc_eq_ly_enabled = get_bit(stat, 6);
|
|
let mode_2_enabled = get_bit(stat, 5);
|
|
let mode_1_vblank_enabled = get_bit(stat, 4);
|
|
let mode_0_hblank_enabled = get_bit(stat, 3);
|
|
|
|
let lyc_eq_ly = self.gpu.scanline == self.memory.get(0xFF45);
|
|
let mode_2 = self.gpu.mode == DrawMode::Mode2;
|
|
let mode_1_vblank = self.gpu.mode == DrawMode::VBlank;
|
|
let mode_0_hblank = self.gpu.mode == DrawMode::HBlank;
|
|
|
|
if (lyc_eq_ly_enabled && lyc_eq_ly)
|
|
|| (mode_2_enabled && mode_2)
|
|
|| (mode_1_vblank_enabled && mode_1_vblank)
|
|
|| (mode_0_hblank_enabled && mode_0_hblank)
|
|
{
|
|
self.memory.set(0xFF0F, set_bit(self.memory.get(0xFF0F), 1));
|
|
} else {
|
|
self.memory
|
|
.set(0xFF0F, clear_bit(self.memory.get(0xFF0F), 1));
|
|
}
|
|
|
|
stat = set_or_clear_bit(stat, 2, lyc_eq_ly);
|
|
stat = set_or_clear_bit(
|
|
stat,
|
|
1,
|
|
(self.gpu.mode == DrawMode::Mode2) || (self.gpu.mode == DrawMode::Mode3),
|
|
);
|
|
stat = set_or_clear_bit(
|
|
stat,
|
|
0,
|
|
(self.gpu.mode == DrawMode::VBlank) || (self.gpu.mode == DrawMode::Mode3),
|
|
);
|
|
self.memory.set(0xFF41, stat);
|
|
self.memory.set(0xFF44, self.gpu.scanline);
|
|
}
|
|
|
|
fn render_scanline(&mut self, scanline: u8, lcdc: &LCDC) {
|
|
for x in 0..WIDTH {
|
|
self.gpu.buffer[(scanline as usize * WIDTH) + x] = Colour::from_u8_rgb(255, 0, 255);
|
|
}
|
|
let bg_palette = byte_to_palette(self.memory.get(0xFF47));
|
|
if lcdc.bg_window_enable {
|
|
self.render_scanline_bg(scanline, lcdc, bg_palette);
|
|
if lcdc.window_enable {
|
|
if !self.gpu.has_window_been_enabled {
|
|
self.gpu.window_lc = scanline;
|
|
}
|
|
self.render_scanline_window(scanline, lcdc, bg_palette);
|
|
self.gpu.has_window_been_enabled = true;
|
|
}
|
|
} else {
|
|
for x in 0..WIDTH {
|
|
self.gpu.buffer[(scanline as usize * WIDTH) + x] =
|
|
Colour::from_u8_rgb(255, 255, 255);
|
|
}
|
|
}
|
|
if lcdc.obj_enable {
|
|
self.render_scanline_obj(scanline, lcdc, bg_palette);
|
|
}
|
|
}
|
|
|
|
fn render_scanline_bg(&mut self, scanline: u8, lcdc: &LCDC, palette: Palette) {
|
|
let scroll_y = 0_u8.wrapping_sub(self.memory.get(0xFF42));
|
|
let scroll_x = 0_u8.wrapping_sub(self.memory.get(0xFF43));
|
|
self.render_tiles(
|
|
scanline,
|
|
scanline,
|
|
&lcdc.bg_tilemap,
|
|
&lcdc.tile_area,
|
|
palette,
|
|
scroll_x,
|
|
scroll_y,
|
|
true,
|
|
);
|
|
}
|
|
|
|
fn render_scanline_window(&mut self, scanline: u8, lcdc: &LCDC, palette: Palette) {
|
|
let pos_y = self.memory.get(0xFF4A);
|
|
// subtracting 7 to get the Real Number...
|
|
let pos_x = self.memory.get(0xFF4B);
|
|
|
|
if pos_y < 143 && pos_x < 166 {
|
|
// window is on screen
|
|
self.render_tiles(
|
|
scanline,
|
|
self.gpu.window_lc,
|
|
&lcdc.window_tilemap,
|
|
&lcdc.tile_area,
|
|
palette,
|
|
pos_x.wrapping_sub(7),
|
|
pos_y,
|
|
false,
|
|
);
|
|
|
|
self.gpu.window_lc = self.gpu.window_lc.wrapping_add(1);
|
|
self.gpu.has_window_been_enabled = true;
|
|
}
|
|
}
|
|
|
|
fn render_scanline_obj(&mut self, scanline: u8, lcdc: &LCDC, bg_palette: Palette) {
|
|
let objs = self.parse_oam(scanline, &lcdc.obj_size);
|
|
for object in objs {
|
|
self.render_object(scanline, object, &lcdc.obj_size, bg_palette);
|
|
}
|
|
}
|
|
|
|
fn parse_oam(&mut self, scanline: u8, obj_size: &ObjSize) -> Vec<Object> {
|
|
let mut objs = vec![];
|
|
for i in (0xFE00..0xFE9F).step_by(4) {
|
|
let y_pos = self.memory.get(i).wrapping_sub(16);
|
|
if y_pos <= scanline && (y_pos + obj_size.get_height()) > scanline {
|
|
// sprite is on line
|
|
let x_pos = self.memory.get(i + 1);
|
|
let tile_index = self.memory.get(i + 2);
|
|
let flags = self.memory.get(i + 3);
|
|
let palette_addr = if get_bit(flags, 4) { 0xFF49 } else { 0xFF48 };
|
|
objs.push(Object {
|
|
x: x_pos,
|
|
y: y_pos,
|
|
tile_index,
|
|
flags: ObjectFlags {
|
|
behind_bg_and_window: get_bit(flags, 7),
|
|
y_flip: get_bit(flags, 6),
|
|
x_flip: get_bit(flags, 5),
|
|
palette: byte_to_palette(self.memory.get(palette_addr)),
|
|
},
|
|
});
|
|
if objs.len() >= 10 {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
objs
|
|
}
|
|
|
|
fn render_object(
|
|
&mut self,
|
|
scanline: u8,
|
|
object: Object,
|
|
obj_size: &ObjSize,
|
|
bg_palette: Palette,
|
|
) {
|
|
let mut object_row = scanline - object.y;
|
|
if object.flags.y_flip {
|
|
object_row = obj_size.get_height() - (object_row + 1);
|
|
}
|
|
let tile_row = object_row % 8;
|
|
let tile_addr = TiledataArea::D8000
|
|
.get_addr(object.tile_index + if object_row >= 8 { 1 } else { 0 })
|
|
+ (tile_row as u16 * 2);
|
|
let lsbs = self.memory.get(tile_addr);
|
|
let msbs = self.memory.get(tile_addr + 1);
|
|
for px_x in 0..8 {
|
|
let x_addr = if object.flags.x_flip { px_x } else { 7 - px_x };
|
|
let lsb = get_bit(lsbs, x_addr);
|
|
let msb = get_bit(msbs, x_addr);
|
|
let colour = bits_to_mapped_colour(lsb, msb, object.flags.palette);
|
|
if colour == object.flags.palette.zero {
|
|
continue;
|
|
}
|
|
let x_coord_uncorrected = (object.x as usize) + (px_x as usize);
|
|
if x_coord_uncorrected < 8 {
|
|
continue;
|
|
}
|
|
let x_coord = x_coord_uncorrected - 8;
|
|
if x_coord < WIDTH {
|
|
let buffer_index = (scanline as usize * WIDTH) + x_coord;
|
|
if !object.flags.behind_bg_and_window
|
|
|| self.gpu.buffer[buffer_index] == bg_palette.zero.to_rgb()
|
|
{
|
|
self.gpu.buffer[buffer_index] = colour.to_rgb();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_tiles(
|
|
&mut self,
|
|
scanline: u8,
|
|
draw_from: u8,
|
|
tilemap: &TilemapArea,
|
|
tiledata: &TiledataArea,
|
|
palette: Palette,
|
|
offset_x: u8,
|
|
offset_y: u8,
|
|
wrap: bool,
|
|
) {
|
|
let (tile_line_y, did_wrap_y) = draw_from.overflowing_sub(offset_y);
|
|
if did_wrap_y && !wrap {
|
|
return;
|
|
}
|
|
let tilemap_row = tile_line_y / 8;
|
|
let tile_px_y = (tile_line_y) % 8;
|
|
let tiledata_offset = tile_px_y * 2;
|
|
let row_addr = ((tilemap_row as usize * 32) % 0x400) as u16;
|
|
for x in 0..WIDTH {
|
|
let (tile_line_x, did_wrap_x) = (x as u8).overflowing_sub(offset_x);
|
|
if did_wrap_x && !wrap {
|
|
continue;
|
|
}
|
|
|
|
let tilemap_column = (tile_line_x / 8) as u16;
|
|
|
|
let tile_px_x = (tile_line_x % 8) as u8;
|
|
let tile_addr = tiledata.get_addr(
|
|
self.memory
|
|
.get(tilemap.get_addr(row_addr + (tilemap_column))),
|
|
) + tiledata_offset as u16;
|
|
|
|
let lsbs = self.memory.get(tile_addr);
|
|
let msbs = self.memory.get(tile_addr + 1);
|
|
let lsb = get_bit(lsbs, 7 - tile_px_x);
|
|
let msb = get_bit(msbs, 7 - tile_px_x);
|
|
let colour = bits_to_mapped_colour(lsb, msb, palette);
|
|
|
|
self.gpu.buffer[(scanline as usize * WIDTH) + x] = colour.to_rgb();
|
|
}
|
|
}
|
|
|
|
fn render_window(&mut self) {
|
|
self.gpu.scaled_buffer = scale_buffer(&self.gpu.buffer, WIDTH, HEIGHT, FACTOR);
|
|
self.window
|
|
.update_with_buffer(&self.gpu.scaled_buffer, WIDTH * FACTOR, HEIGHT * FACTOR)
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
fn scale_buffer(buffer: &Vec<u32>, width: usize, height: usize, factor: usize) -> Vec<u32> {
|
|
let mut v = vec![];
|
|
for y in 0..height {
|
|
for _ in 0..factor {
|
|
for x in 0..width {
|
|
for _ in 0..factor {
|
|
v.push(buffer[(y * width) + x]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
v
|
|
}
|
|
|
|
fn bits_to_mapped_colour(lsb: bool, msb: bool, palette: Palette) -> Colour {
|
|
match (lsb, msb) {
|
|
(true, true) => palette.three,
|
|
(true, false) => palette.one,
|
|
(false, true) => palette.two,
|
|
(false, false) => palette.zero,
|
|
}
|
|
}
|
|
|
|
fn byte_to_palette(byte: u8) -> Palette {
|
|
Palette {
|
|
zero: bits_to_colour(get_bit(byte, 0), get_bit(byte, 1)),
|
|
one: bits_to_colour(get_bit(byte, 2), get_bit(byte, 3)),
|
|
two: bits_to_colour(get_bit(byte, 4), get_bit(byte, 5)),
|
|
three: bits_to_colour(get_bit(byte, 6), get_bit(byte, 7)),
|
|
}
|
|
}
|
|
|
|
fn bits_to_colour(first: bool, second: bool) -> Colour {
|
|
match (first, second) {
|
|
(true, true) => Colour::Black,
|
|
(true, false) => Colour::LightGray,
|
|
(false, true) => Colour::DarkGray,
|
|
(false, false) => Colour::White,
|
|
}
|
|
}
|