use self::tile_window::TileWindow; use crate::{ processor::{as_signed, get_bit, set_bit, set_or_clear_bit, CPU}, FACTOR, HEIGHT, WIDTH, }; use minifb::{Window, WindowOptions}; mod tile_window; #[derive(PartialEq)] enum DrawMode { HBlank, VBlank, Mode2, Mode3, } #[derive(Debug)] enum TilemapArea { T9800, T9C00, } impl TilemapArea { fn get_addr(&self, addr: u16) -> u16 { match self { TilemapArea::T9800 => 0x9800 + addr, TilemapArea::T9C00 => 0x9C00 + addr, } } } #[derive(Debug)] enum TiledataArea { D8000, D9000, } impl TiledataArea { fn get_addr(&self, addr: u8) -> u16 { match self { TiledataArea::D8000 => 0x8000 + ((addr as u16) * 16), TiledataArea::D9000 => 0x9000_u16.wrapping_add_signed((as_signed(addr) as i16) * 16), } } } #[derive(Debug)] enum ObjSize { S8x8, S8x16, } impl ObjSize { fn get_height(&self) -> u8 { match self { ObjSize::S8x8 => 8, ObjSize::S8x16 => 16, } } } #[derive(Debug)] struct LCDC { enable: bool, window_tilemap: TilemapArea, window_enable: bool, tile_area: TiledataArea, bg_tilemap: TilemapArea, obj_size: ObjSize, obj_enable: bool, bg_window_enable: bool, } #[derive(Clone, Copy, Debug)] enum Colour { White, LightGray, DarkGray, Black, } impl Colour { fn to_rgb(&self) -> u32 { match self { Colour::White => Self::from_u8_rgb(0xFF, 0xFF, 0xFF), Colour::LightGray => Self::from_u8_rgb(0xAA, 0xAA, 0xAA), Colour::DarkGray => Self::from_u8_rgb(0x55, 0x55, 0x55), Colour::Black => Self::from_u8_rgb(0x00, 0x00, 0x00), } } fn from_u8_rgb(r: u8, g: u8, b: u8) -> u32 { let (r, g, b) = (r as u32, g as u32, b as u32); (r << 16) | (g << 8) | b } } #[derive(Clone, Copy)] struct Palette { zero: Colour, one: Colour, two: Colour, three: Colour, } struct ObjectFlags { behind_bg_and_window: bool, y_flip: bool, x_flip: bool, palette: Palette, } struct Object { x: u8, y: u8, tile_index: u8, flags: ObjectFlags, } 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, scaled_buffer: Vec, mode: DrawMode, mode_clock: usize, scanline: u8, tile_window: Option, } 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, } } } impl CPU { pub fn advance_gpu_clock(&mut self, steps: u8) { let real_steps = (steps as usize) * 4; self.gpu.mode_clock += real_steps; let lcdc = self.get_lcdc(); 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(&lcdc); } 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); } } } 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, lcdc: &LCDC) { self.memory.update_pressed_keys(self.window.get_keys()); self.gpu.mode = DrawMode::VBlank; if lcdc.enable { 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; 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); stat = set_or_clear_bit(stat, 2, self.memory.get(0xFF44) == self.memory.get(0xFF45)); 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) { if lcdc.bg_window_enable { let palette = byte_to_palette(self.memory.get(0xFF47)); self.render_scanline_bg(scanline, lcdc, palette); if lcdc.window_enable { self.render_scanline_window(scanline, lcdc, palette); } } 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); } } fn render_scanline_bg(&mut self, scanline: u8, lcdc: &LCDC, palette: Palette) { let scroll_y = self.memory.get(0xFF42); let scroll_x = self.memory.get(0xFF43); self.render_tiles( scanline, &lcdc.bg_tilemap, &lcdc.tile_area, palette, scroll_x, scroll_y, ); } 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).wrapping_sub(7); if pos_y < 143 && pos_x < 166 { self.render_tiles( scanline, &lcdc.window_tilemap, &lcdc.tile_area, palette, pos_x, pos_y, ) } } fn render_scanline_obj(&mut self, scanline: u8, lcdc: &LCDC) { let objs = self.parse_oam(scanline, &lcdc.obj_size); for object in objs { self.render_object(scanline, object, &lcdc.obj_size); } } fn parse_oam(&mut self, scanline: u8, obj_size: &ObjSize) -> Vec { 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) { let mut object_row = scanline - object.y; if object.flags.y_flip { object_row = obj_size.get_height() - object_row; } 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); let x_coord = (object.x as usize) + (px_x as usize); if x_coord < WIDTH { self.gpu.buffer[(scanline as usize * WIDTH) + x_coord] = colour.to_rgb(); } } } fn render_tiles( &mut self, scanline: u8, tilemap: &TilemapArea, tiledata: &TiledataArea, palette: Palette, _offset_x: u8, offset_y: u8, ) { let tile_line = (scanline as usize) + (offset_y as usize); let tilemap_row = tile_line / 8; let tile_px = (tile_line) % 8; let tiledata_offset = tile_px * 2; let row_addr = ((tilemap_row * 32) % 0x400) as u16; for x in 0..32_u16 { let tile_addr = tiledata.get_addr(self.memory.get(tilemap.get_addr(row_addr + x))) + tiledata_offset as u16; let lsbs = self.memory.get(tile_addr); let msbs = self.memory.get(tile_addr + 1); for px_x in 0..8 { let lsb = get_bit(lsbs, 7 - px_x); let msb = get_bit(msbs, 7 - px_x); let colour = bits_to_mapped_colour(lsb, msb, palette); let x_coord = ((x * 8) + (px_x as u16)) as usize; if x_coord < WIDTH { self.gpu.buffer[(scanline as usize * WIDTH) + x_coord] = 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, width: usize, height: usize, factor: usize) -> Vec { 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, } }