433 lines
15 KiB
Rust
433 lines
15 KiB
Rust
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));
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
}
|