extern crate sdl2; extern crate libc; // Internal ram size const VRAM_SIZE: usize = 0x2000; // OAM size const OAM_SIZE: usize = (0xFE9F - 0xFE00) + 1; // Timing values const TICKS_END_SCANLINE: u16 = 80; const TICKS_END_READMODE: u16 = 172; const TICKS_END_HBLANK: u16 = 204; const TICKS_END_VBLANK: u16 = 456; // Display size const GB_PIXELS_X: u16 = 160; const GB_PIXELS_Y: u16 = 144; const SCALE: u16 = 4; // Control flags const CTRL_LCD_DISPLAY_ENABLE: u8 = 1 << 7; const CTRL_TILE_MAP_SELECT: u8 = 1 << 6; const CTRL_WND_DISPLAY_ENABLE: u8 = 1 << 5; const CTRL_BG_WINDOW_TILE_DATA_SELECT: u8 = 1 << 4; const CTRL_BG_TILE_MAP_SELECT: u8 = 1 << 3; const CTRL_BG_SPRITE_SIZE: u8 = 1 << 2; const CTRL_BG_SPRITE_ENABLE: u8 = 1 << 1; const CTRL_BG_DISPLAY: u8 = 1 << 0; // Status flags const STAT_LYC_LC_COINCIDENCE_INT: u8 = 1 << 6; const STAT_MODE_OAM_INT: u8 = 1 << 5; const STAT_MODE_VBLANK_INT: u8 = 1 << 4; const STAT_MODE_HBLANK_INT: u8 = 1 << 3; #[derive(Debug)] enum DisplayMode { ReadOAMMemory, ReadFullMemory, HBlank, VBlank, } impl Default for DisplayMode { fn default() -> DisplayMode { DisplayMode::HBlank } } pub struct Display { control: u8, status: u8, background_palette: u8, object_palette_0: u8, object_palette_1: u8, scrollx: u8, scrolly: u8, windowx: u8, windowy: u8, curline: u8, lyc: u8, vram: Box<[u8]>, oam: Box<[u8]>, current_ticks: u16, current_mode: DisplayMode, // TODO renderer: sdl2::render::Renderer<'static>, pub event_pump: sdl2::EventPump, vblank_interrupt: bool, stat_interrupt: bool, } impl Display { pub fn new() -> Display { let sdl_ctx = sdl2::init().unwrap(); let video_ctx = sdl_ctx.video().unwrap(); let wnd = video_ctx.window("RustBoy", (GB_PIXELS_X * SCALE) as u32, (GB_PIXELS_Y * SCALE) as u32).position_centered().build().expect("Failed to create window :<"); let renderer = wnd.renderer().build().expect("Could not build renderer"); let event_pump = sdl_ctx.event_pump().expect("Getting event pump failed"); Display { control: 0, status: 0, background_palette: 0, object_palette_0: 0, object_palette_1: 0, scrollx: 0, scrolly: 0, windowx: 0, windowy: 0, curline: 0, lyc: 0, current_ticks: 0, current_mode: DisplayMode::default(), vram: vec![0; VRAM_SIZE].into_boxed_slice(), oam: vec![0; OAM_SIZE].into_boxed_slice(), renderer: renderer, event_pump: event_pump, vblank_interrupt: false, stat_interrupt: false, } } #[inline] fn set_pixel(&mut self, x: u8, y: u8, color: sdl2::pixels::Color) { self.renderer.set_draw_color(color); self.renderer.fill_rect(sdl2::rect::Rect::new((x as i32) * SCALE as i32, (y as i32) * SCALE as i32, SCALE as u32, SCALE as u32)); } #[inline] pub fn vblank_interrupt(&mut self) -> bool { // Returns whether or not a vblank interrupt should be done // Yes, this is polling, and yes, this sucks.\ if self.vblank_interrupt { self.vblank_interrupt = false; true } else { false } } #[inline] pub fn stat_interrupt(&mut self) -> bool { if self.stat_interrupt { self.stat_interrupt = false; true } else { false } } #[inline] pub fn write_byte(&mut self, addr: u16, val: u8) { match addr { 0x8000 ... 0x9FFF => { // println!("VRAM: Write {:02X} to {:04X}", val, addr); self.vram[(addr - 0x8000) as usize] = val; } 0xFE00 ... 0xFE9F => { // println!("OAM: Write {:02X} to {:04X}", val, addr); self.oam[(addr - 0xFE00) as usize] = val; } 0xFF40 => self.control = val, 0xFF41 => self.status = val, 0xFF42 => self.scrolly = val, 0xFF43 => self.scrollx = val, 0xFF44 => self.curline = 0, 0xFF45 => self.lyc = val, 0xFF47 => self.background_palette = val, 0xFF48 => self.object_palette_0 = val, 0xFF49 => self.object_palette_1 = val, 0xFF4A => self.windowy = val, 0xFF4B => self.windowx = val, _ => panic!("Display: Write {:02X} to {:04X} unsupported", val, addr), } } #[inline] pub fn read_byte(&self, addr: u16) -> u8 { match addr { 0x8000 ... 0x9FFF => self.vram[(addr - 0x8000) as usize], 0xFE00 ... 0xFE9F => self.oam[(addr - 0xFE00) as usize], 0xFF40 => self.control, 0xFF41 => self.status, 0xFF42 => self.scrolly, 0xFF43 => self.scrollx, 0xFF44 => self.curline, 0xFF45 => self.lyc, 0xFF47 => self.background_palette, 0xFF48 => self.object_palette_0, 0xFF49 => self.object_palette_1, 0xFF4A => self.windowy, 0xFF4B => self.windowx, _ => panic!("Display: Read from {:04X} unsupported", addr), } } #[inline] pub fn tick(&mut self, ticks: u16) { self.status &= 0xFC; if self.control & CTRL_LCD_DISPLAY_ENABLE == 0 { // Display is disabled self.current_ticks = 0; self.current_mode = DisplayMode::VBlank; self.curline = 0; return; } self.current_ticks += ticks; match self.current_mode { DisplayMode::ReadOAMMemory => { // Mode 2, Reading OAM memory, RAM may be accessed. if self.current_ticks > TICKS_END_SCANLINE { self.current_ticks = 0; self.current_mode = DisplayMode::ReadFullMemory; } self.status |= 2; }, DisplayMode::ReadFullMemory => { // Mode 3, reading OAM, VMEM and palette data. // Nothing may be accessed. if self.current_ticks > TICKS_END_READMODE { self.current_ticks = 0; self.current_mode = DisplayMode::HBlank; self.renderscan(); if self.status & STAT_MODE_HBLANK_INT > 0 { self.stat_interrupt = true; } } self.status |= 3; }, DisplayMode::HBlank => { // Mode 0, H-Blank, Memory (RAM, OAM) may be accessed. if self.current_ticks > TICKS_END_HBLANK { self.current_ticks = 0; self.curline += 1; // render scan? if self.curline == 143 { self.current_mode = DisplayMode::VBlank; // To Mode 1 if self.status & STAT_MODE_VBLANK_INT > 0 { self.stat_interrupt = true; } // render frame. self.renderer.present(); self.renderer.set_draw_color(sdl2::pixels::Color::RGB(0, 0, 0)); self.renderer.clear(); } else { self.current_mode = DisplayMode::ReadOAMMemory; // Mode 2 again if self.status & STAT_MODE_OAM_INT > 0 { self.stat_interrupt = true; } } } self.status &= 0xFC; self.status |= 0; }, DisplayMode::VBlank => { // Mode 1, V-Blank (or display disabled), Memory (RAM, OAM) // may be accessed if self.current_ticks > TICKS_END_VBLANK { self.current_ticks = 0; self.curline += 1; if self.curline == 144 { self.vblank_interrupt = true; } if self.curline > 153 { self.current_mode = DisplayMode::ReadOAMMemory; // Mode 2, scanline. self.curline = 0; if self.status & STAT_MODE_OAM_INT > 0 { self.stat_interrupt = true; } } } self.status |= 1; } } // Update the status register if self.curline == self.lyc { self.status |= 1 << 2; if self.status & STAT_LYC_LC_COINCIDENCE_INT > 0 { self.stat_interrupt = true; } } else { self.status &= !(1 << 2); } } #[inline] fn renderscan(&mut self) { // Points to the background map offset to use. let background_map: usize; // verify! if self.control & CTRL_BG_TILE_MAP_SELECT > 0 { background_map = 0x1C00; } else { background_map = 0x1800; } // Those are pixel units, not tile units. let map_offset_y: u8 = self.scrolly; let map_offset_x: u8 = self.scrollx; let render_y: u8 = self.curline; if self.control & CTRL_BG_SPRITE_ENABLE > 0 { // panic!("Sprites not supported"); } // Render background if self.control & CTRL_BG_DISPLAY > 0 { // Draw borders first. /* for t_x in 0 .. 160/8 { self.renderer.set_draw_color(sdl2::pixels::Color::RGB(0xFF, 10, 0xFF)); let RX: i32 = (t_x as i32)*8 + map_offset_x as i32; let RY: i32 = ((render_y as u32) & 0xFFFFFFF8) as i32 - map_offset_y as i32; let TS: u32 = 8u32 * (SCALE as u32); self.renderer.draw_rect( sdl2::rect::Rect::new(RX * SCALE as i32, RY * SCALE as i32, TS, TS) ); } */ // Render pixels (20 tiles) for render_x in 0 .. 160 { // Absolute render coordinates let render_abs_x = map_offset_x.wrapping_add(render_x); let render_abs_y = map_offset_y.wrapping_add(render_y); let tile_index_x: u8 = render_abs_x / 8; let tile_offset_x: u8 = render_abs_x % 8; let tile_index_y: u8 = render_abs_y / 8; let tile_offset_y: u8 = render_abs_y % 8; let vram_offset: usize = background_map + (tile_index_y as usize) * 32 + tile_index_x as usize; // Obtain tile ID in this area let mut tile_id = self.vram[vram_offset]; // Obtain tile information let tile_base_addr: usize; if self.control & CTRL_BG_WINDOW_TILE_DATA_SELECT > 0 { tile_base_addr = 0x0000; } else { tile_base_addr = 0x0800; // This set goes from -127 to 127. // TODO: Fix this. (?) let s_tid: i8 = tile_id as i8; tile_id = ((128u8 as i8).wrapping_add(s_tid)) as u8; // panic!("OH MY GOD, this wasn't tested yet"); } let tile_base_addr: usize = tile_base_addr + (tile_id as usize) * 16; let tile_line_1 = self.vram[tile_base_addr + (tile_offset_y as usize) * 2]; let tile_line_2 = self.vram[tile_base_addr + (tile_offset_y as usize) * 2 + 1]; // Get the correct bit let b1: bool = (tile_line_1 & 1 << (7 - tile_offset_x)) > 0; let b2: bool = (tile_line_2 & 1 << (7 - tile_offset_x)) > 0; let mut factor = 0; if b1 { factor += 64; } if b2 { factor += 128; } // Draw stuff. We're currently only in monochrome mode self.set_pixel(render_x, render_y, sdl2::pixels::Color::RGB(factor, factor, factor)); } if self.control & CTRL_BG_SPRITE_ENABLE > 0 { // Let's draw sprites. // TODO: Sprites with smaller X coordinate should // should be in front // TODO: Draw only up to 10 sprites per line if self.control & CTRL_BG_SPRITE_SIZE > 0 { println!("Wide sprites not tested!"); } for i in 0 .. 39 { let mut y: u8 = self.oam[i * 4 + 0]; let mut x: u8 = self.oam[i * 4 + 1]; let t_num: u8 = self.oam[i * 4 + 2]; let flags: u8 = self.oam[i * 4 + 3]; if x == 0 || y == 0 { // This sprite is hidden continue; } x = x.wrapping_sub(8); y = y.wrapping_sub(16); // Is this sprite on the current line? if y.wrapping_add(8) >= render_y && y <= render_y { let tile_offset_y: usize = render_y as usize - y as usize; let tile_base_addr: usize = 0 + t_num as usize * 16; let tile_line_1 = self.vram[tile_base_addr + tile_offset_y * 2]; let tile_line_2 = self.vram[tile_base_addr + tile_offset_y * 2 + 1]; let tile_line_3 = self.vram[tile_base_addr + tile_offset_y * 2 + 2]; let tile_line_4 = self.vram[tile_base_addr + tile_offset_y * 2 + 3]; // We need to draw this. let wide_mode = self.control & CTRL_BG_SPRITE_SIZE > 0; let limit = match wide_mode { true => 16, false => 8 }; for x_o in 0 .. limit { let b1: bool; let b2: bool; let mut factor = 0; if wide_mode && x_o > 7 { b1 = (tile_line_3 & 1 << (14 - x_o)) > 0; b2 = (tile_line_4 & 1 << (14 - x_o)) > 0; } else { b1 = (tile_line_1 & 1 << (7 - x_o)) > 0; b2 = (tile_line_2 & 1 << (7 - x_o)) > 0; } if b1 { factor += 64; } if b2 { factor += 128; } // Draw stuff. We're currently only in monochrome mode self.set_pixel(x.wrapping_add(x_o), render_y, sdl2::pixels::Color::RGB(factor, factor, factor)); } } } } } } }