920 lines
31 KiB
Rust
920 lines
31 KiB
Rust
extern crate libc;
|
|
extern crate sdl2;
|
|
|
|
use std::io::Write;
|
|
|
|
// 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: usize = 160;
|
|
const GB_PIXELS_Y: usize = 144;
|
|
const SCALE: usize = 4;
|
|
|
|
const WND_RES_X: usize = GB_PIXELS_X * SCALE;
|
|
const WND_RES_Y: usize = GB_PIXELS_Y * SCALE;
|
|
|
|
// Control flags
|
|
const CTRL_LCD_DISPLAY_ENABLE: u8 = 1 << 7;
|
|
const CTRL_WND_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;
|
|
|
|
// Sprite flags
|
|
const SPRITE_OBJ_BG_PRIORITY: u8 = 1 << 7;
|
|
const SPRITE_Y_FLIP: u8 = 1 << 6;
|
|
const SPRITE_X_FLIP: u8 = 1 << 5;
|
|
//const SPRITE_PALETTE_NO: u8 = 1 << 4; // NonCGB only
|
|
const SPRITE_TILE_VRAM_BANK: u8 = 1 << 3; // CGB only
|
|
|
|
// Display color
|
|
/*
|
|
const MONOCHROME_PALETTE: &'static [[f64; 3]; 4] = &[
|
|
[0.605, 0.734, 0.059],
|
|
[0.543, 0.672, 0.059],
|
|
[0.188, 0.383, 0.188],
|
|
[0.059, 0.219, 0.059],
|
|
];
|
|
const MONOCHROME_PALETTE: &'static [[u8; 3]; 4] = &[
|
|
[255, 255, 255],
|
|
[200, 200, 200],
|
|
[125, 125, 12],
|
|
[50, 50, 50],
|
|
];
|
|
*/
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct CgbPalette([u8; 8]);
|
|
impl CgbPalette {
|
|
fn get_color(self, n: usize) -> sdl2::pixels::Color {
|
|
if n == 0 {
|
|
return sdl2::pixels::Color::RGB(255, 255, 255);
|
|
}
|
|
let v = ((self.0[2 * n + 1] as u16) << 8) | (self.0[2 * n] as u16);
|
|
let r = (v & 0b1_1111) as u8;
|
|
let g = ((v >> 5) & 0b1_1111) as u8;
|
|
let b = ((v >> 10) & 0b1_1111) as u8;
|
|
// According to some code:
|
|
// Real colors:
|
|
let r = r as u16;
|
|
let g = g as u16;
|
|
let b = b as u16;
|
|
let mapped_r = ((r * 13 + g * 2 + b) >> 1) as u8;
|
|
let mapped_g = ((g * 3 + b) << 1) as u8;
|
|
let mapped_b = ((r * 3 + g * 2 + b * 11) >> 1) as u8;
|
|
sdl2::pixels::Color::RGB(mapped_r, mapped_g, mapped_b)
|
|
}
|
|
}
|
|
|
|
struct BgMapAttributes(u8);
|
|
impl BgMapAttributes {
|
|
fn palette_number(&self) -> usize {
|
|
(self.0 & 0b111) as usize
|
|
}
|
|
|
|
fn vram_bank_number(&self) -> usize {
|
|
((self.0 >> 3) & 1) as usize
|
|
}
|
|
|
|
fn horizontal_flip(&self) -> bool {
|
|
(self.0 >> 5) != 0
|
|
}
|
|
|
|
fn vertical_flip(&self) -> bool {
|
|
(self.0 >> 6) != 0
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
enum PixelOrigin {
|
|
Empty,
|
|
Background,
|
|
Window,
|
|
Sprite,
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct Pixel {
|
|
origin: PixelOrigin,
|
|
color: sdl2::pixels::Color,
|
|
}
|
|
|
|
impl Default for Pixel {
|
|
fn default() -> Pixel {
|
|
Pixel {
|
|
origin: PixelOrigin::Empty,
|
|
color: sdl2::pixels::Color::RGB(255, 0, 0),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum DisplayMode {
|
|
ReadOAMMemory,
|
|
ReadFullMemory,
|
|
HBlank,
|
|
VBlank,
|
|
}
|
|
|
|
impl Default for DisplayMode {
|
|
fn default() -> DisplayMode {
|
|
DisplayMode::HBlank
|
|
}
|
|
}
|
|
|
|
struct Sprite {
|
|
x: u8,
|
|
y: u8,
|
|
tile: u8,
|
|
flags: u8,
|
|
}
|
|
|
|
impl Sprite {
|
|
fn is_hidden(&self) -> bool {
|
|
self.x == 0 || self.y == 0
|
|
}
|
|
|
|
fn is_foreground(&self) -> bool {
|
|
(self.flags & SPRITE_OBJ_BG_PRIORITY) == 0
|
|
}
|
|
|
|
fn is_x_flipped(&self) -> bool {
|
|
(self.flags & SPRITE_X_FLIP) == SPRITE_X_FLIP
|
|
}
|
|
|
|
fn is_y_flipped(&self) -> bool {
|
|
(self.flags & SPRITE_Y_FLIP) == SPRITE_Y_FLIP
|
|
}
|
|
|
|
fn palette(&self) -> u8 {
|
|
// GB
|
|
/*
|
|
if (self.flags & SPRITE_PALETTE_NO) == 0 {
|
|
0
|
|
} else {
|
|
1
|
|
}
|
|
*/
|
|
// GBC
|
|
self.flags & 0b111
|
|
}
|
|
|
|
// GBC only
|
|
fn vram_bank(&self) -> u8 {
|
|
if (self.flags & SPRITE_TILE_VRAM_BANK) == 0 {
|
|
0
|
|
} else {
|
|
1
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Display {
|
|
control: u8,
|
|
status: u8,
|
|
background_palette: u8,
|
|
// Only 0 and 1 for GB
|
|
object_palette: [u8; 2],
|
|
scrollx: u8,
|
|
scrolly: u8,
|
|
windowx: u8,
|
|
windowy: u8,
|
|
curline: u8,
|
|
lyc: u8,
|
|
|
|
vram0: Box<[u8]>,
|
|
vram1: Box<[u8]>,
|
|
oam: Box<[u8]>,
|
|
|
|
vram_bank: u8,
|
|
current_ticks: u16,
|
|
current_mode: DisplayMode,
|
|
|
|
renderer: sdl2::render::Canvas<sdl2::video::Window>,
|
|
|
|
pub event_pump: sdl2::EventPump,
|
|
|
|
vblank_interrupt: bool,
|
|
stat_interrupt: bool,
|
|
|
|
pixels: [Pixel; GB_PIXELS_X * GB_PIXELS_Y],
|
|
|
|
frameskip: u8,
|
|
frame_no: u8,
|
|
|
|
// GBC:
|
|
background_palette_autoinc: bool,
|
|
background_palette_index: u8,
|
|
background_palette_cgb: [CgbPalette; 0x40 / 8],
|
|
|
|
object_palette_autoinc: bool,
|
|
object_palette_index: u8,
|
|
object_palette_cgb: [CgbPalette; 0x40 / 8],
|
|
}
|
|
|
|
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", WND_RES_X as u32, WND_RES_Y as u32)
|
|
.position_centered()
|
|
.build()
|
|
.expect("Failed to create window :<");
|
|
let renderer = wnd.into_canvas().build().expect("Could not build renderer");
|
|
let event_pump = sdl_ctx.event_pump().expect("Getting event pump failed");
|
|
let pixels = [Pixel::default(); (GB_PIXELS_X as usize) * (GB_PIXELS_Y as usize)];
|
|
|
|
Display {
|
|
control: 0,
|
|
status: 0,
|
|
background_palette: 0,
|
|
object_palette: [0u8; 2],
|
|
scrollx: 0,
|
|
scrolly: 0,
|
|
windowx: 0,
|
|
windowy: 0,
|
|
curline: 0,
|
|
lyc: 0,
|
|
|
|
current_ticks: 0,
|
|
current_mode: DisplayMode::default(),
|
|
vram0: vec![0; VRAM_SIZE].into_boxed_slice(),
|
|
vram1: vec![0; VRAM_SIZE].into_boxed_slice(),
|
|
vram_bank: 0,
|
|
oam: vec![0; OAM_SIZE].into_boxed_slice(),
|
|
renderer,
|
|
|
|
event_pump,
|
|
|
|
vblank_interrupt: false,
|
|
stat_interrupt: false,
|
|
pixels,
|
|
frameskip: 0,
|
|
frame_no: 0,
|
|
|
|
background_palette_autoinc: false,
|
|
background_palette_index: 0,
|
|
background_palette_cgb: [CgbPalette([0u8; 2 * 4]); 0x40 / 8],
|
|
object_palette_autoinc: false,
|
|
object_palette_index: 0,
|
|
object_palette_cgb: [CgbPalette([0u8; 2 * 4]); 0x40 / 8],
|
|
}
|
|
}
|
|
|
|
fn set_pixel(&mut self, x: u8, y: u8, color: sdl2::pixels::Color, origin: PixelOrigin) {
|
|
let p = &mut self.pixels[(x as usize) + (y as usize) * GB_PIXELS_X];
|
|
p.color = color;
|
|
p.origin = origin;
|
|
}
|
|
|
|
fn get_pixel_origin(&self, x: u8, y: u8) -> PixelOrigin {
|
|
self.pixels[(x as usize) + (y as usize) * GB_PIXELS_X].origin
|
|
}
|
|
|
|
fn render_screen(&mut self) {
|
|
for y in 0..GB_PIXELS_Y {
|
|
for x in 0..GB_PIXELS_X {
|
|
let f = &mut self.pixels[(x as usize) + (y as usize) * GB_PIXELS_X];
|
|
self.renderer.set_draw_color(f.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,
|
|
))
|
|
.expect("Rendering failed");
|
|
|
|
// Clear origin after rendering
|
|
f.origin = PixelOrigin::Empty;
|
|
}
|
|
}
|
|
|
|
self.renderer
|
|
.set_draw_color(sdl2::pixels::Color::RGB(255, 0, 255));
|
|
self.renderer
|
|
.draw_rect(sdl2::rect::Rect::new(
|
|
0,
|
|
0,
|
|
(GB_PIXELS_X * SCALE) as u32,
|
|
(GB_PIXELS_Y * SCALE) as u32,
|
|
))
|
|
.expect("Rendering failed");
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
pub fn stat_interrupt(&mut self) -> bool {
|
|
if self.stat_interrupt {
|
|
self.stat_interrupt = false;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn write_byte(&mut self, addr: u16, val: u8) {
|
|
match addr {
|
|
0x8000..=0x9FFF => {
|
|
if (self.vram_bank & 1) == 0 {
|
|
self.vram0[(addr - 0x8000) as usize] = val
|
|
} else {
|
|
self.vram1[(addr - 0x8000) as usize] = val
|
|
}
|
|
}
|
|
0xFE00..=0xFE9F => 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,
|
|
// GB classic
|
|
0xFF47 => self.background_palette = val,
|
|
0xFF48 => self.object_palette[0] = val,
|
|
0xFF49 => self.object_palette[1] = val,
|
|
// GBC
|
|
0xFF68 => {
|
|
self.background_palette_index = val & 0b0111_1111;
|
|
self.background_palette_autoinc = (val >> 7) != 0;
|
|
}
|
|
0xFF69 => {
|
|
let idx = self.background_palette_index as usize;
|
|
if idx < 64 {
|
|
self.background_palette_cgb[idx / 8].0[idx % 8] = val;
|
|
} else {
|
|
panic!("OOB palette w/ autoinc");
|
|
}
|
|
if self.background_palette_autoinc {
|
|
self.background_palette_index += 1;
|
|
}
|
|
}
|
|
0xFF6A => {
|
|
self.object_palette_index = val & 0b0111_1111;
|
|
self.object_palette_autoinc = (val >> 7) != 0;
|
|
}
|
|
0xFF6B => {
|
|
let idx = self.object_palette_index as usize;
|
|
if idx < 64 {
|
|
self.object_palette_cgb[idx / 8].0[idx % 8] = val;
|
|
} else {
|
|
panic!("OOB obj palette w/ autoinc");
|
|
}
|
|
if self.object_palette_autoinc {
|
|
self.object_palette_index += 1;
|
|
}
|
|
}
|
|
0xFF4A => {
|
|
if self.windowy != val {
|
|
println!("WY set to {:02X}", val);
|
|
}
|
|
self.windowy = val;
|
|
}
|
|
0xFF4B => {
|
|
if self.windowx != val {
|
|
println!("WX set to {:02X}", val);
|
|
}
|
|
self.windowx = val;
|
|
}
|
|
0xFF4F => self.vram_bank = val,
|
|
_ => panic!("Display: Write {:02X} to {:04X} unsupported", val, addr),
|
|
}
|
|
}
|
|
|
|
pub fn read_byte(&self, addr: u16) -> u8 {
|
|
match addr {
|
|
0x8000..=0x9FFF => {
|
|
if (self.vram_bank & 1) == 0 {
|
|
self.vram0[(addr - 0x8000) as usize]
|
|
} else {
|
|
self.vram1[(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,
|
|
0xFF4F => self.vram_bank | 0b1111_1110,
|
|
_ => panic!("Display: Read from {:04X} unsupported", addr),
|
|
}
|
|
}
|
|
|
|
fn get_background_attribute(
|
|
&self,
|
|
//tile_index_x: usize,
|
|
//tile_index_y: usize,
|
|
vram_offset: usize,
|
|
) -> BgMapAttributes {
|
|
/*
|
|
let background_map = if self.control & CTRL_BG_TILE_MAP_SELECT != 0 {
|
|
0x1C00
|
|
} else {
|
|
0x1800
|
|
};
|
|
|
|
let vram_offset: usize =
|
|
background_map + ((tile_index_y as usize) * 32) + tile_index_x as usize;
|
|
*/
|
|
// Background attributes are only in this memory range.
|
|
assert!(
|
|
vram_offset >= 0x1800 && vram_offset < 0x2000,
|
|
format!("offset: {:04X}", vram_offset)
|
|
);
|
|
|
|
BgMapAttributes(self.vram1[vram_offset])
|
|
}
|
|
|
|
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;
|
|
if self.frameskip < self.frame_no {
|
|
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;
|
|
}
|
|
} 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.
|
|
if self.frameskip < self.frame_no {
|
|
self.render_screen();
|
|
self.renderer.present();
|
|
self.renderer
|
|
.set_draw_color(sdl2::pixels::Color::RGB(255, 255, 255));
|
|
self.renderer.clear();
|
|
self.frame_no = 0;
|
|
} else {
|
|
self.frame_no += 1;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
fn render_sprites(&mut self, sprites: &[Sprite]) {
|
|
if self.control & CTRL_BG_SPRITE_ENABLE > 0 {
|
|
let mut num_rendered: u8 = 0;
|
|
for sprite in sprites.iter().take(39) {
|
|
// Gameboy limitation
|
|
if num_rendered >= 10 {
|
|
break;
|
|
}
|
|
|
|
// Skip hidden sprites
|
|
if sprite.is_hidden() {
|
|
continue;
|
|
}
|
|
|
|
// Calculate correct coords
|
|
let x: u8 = sprite.x.wrapping_sub(8);
|
|
// Flip sprite, TODO: What does this do in WIDE mode?
|
|
let y: u8 = sprite.y.wrapping_sub(16);
|
|
let render_y = self.curline;
|
|
|
|
// Is this sprite on the current line?
|
|
let wide_mode = self.control & CTRL_BG_SPRITE_SIZE > 0;
|
|
let actual_h = if wide_mode { 16 } else { 8 };
|
|
|
|
if sprite.is_y_flipped() {
|
|
panic!("Sorry, no y flip support yet");
|
|
}
|
|
|
|
if y.wrapping_add(actual_h) > render_y && y <= render_y {
|
|
num_rendered += 1;
|
|
let tile_offset_y: usize = render_y as usize - y as usize;
|
|
let tile_base_addr: usize = sprite.tile as usize * 16;
|
|
|
|
let tile_addr = tile_base_addr + tile_offset_y * 2;
|
|
let vram = if sprite.vram_bank() == 0 {
|
|
&*self.vram0
|
|
} else {
|
|
&*self.vram1
|
|
};
|
|
let tile_line_1 = vram[tile_addr + 0];
|
|
let tile_line_2 = vram[tile_addr + 1];
|
|
|
|
for x_o in 0..8 {
|
|
let b1: bool;
|
|
let b2: bool;
|
|
|
|
let scr_x = x.wrapping_add(x_o);
|
|
let scr_y = render_y;
|
|
let pixel_origin = self.get_pixel_origin(scr_x, scr_y);
|
|
// Do not draw if the sprite should be drawn in the background
|
|
if !sprite.is_foreground() {
|
|
match pixel_origin {
|
|
PixelOrigin::Background | PixelOrigin::Window => continue,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let x_maybe_flipped: u8 = if sprite.is_x_flipped() { x_o ^ 7 } else { x_o };
|
|
b1 = (tile_line_1 & 1 << (7 - x_o)) > 0;
|
|
b2 = (tile_line_2 & 1 << (7 - x_o)) > 0;
|
|
|
|
// Sprites may be transparent.
|
|
if !b1 && !b2 {
|
|
continue;
|
|
}
|
|
|
|
// GB
|
|
/*
|
|
let c = (b1 as u8) * 2 + b2 as u8;
|
|
let lookup: [u8; 4] = match sprite.palette() {
|
|
0 => [
|
|
self.object_palette[0] & 3,
|
|
(self.object_palette[0] >> 2) & 3,
|
|
(self.object_palette[0] >> 4) & 3,
|
|
(self.object_palette[0] >> 6) & 3,
|
|
],
|
|
1 => [
|
|
self.object_palette[1] & 3,
|
|
(self.object_palette[1] >> 2) & 3,
|
|
(self.object_palette[1] >> 4) & 3,
|
|
(self.object_palette[1] >> 6) & 3,
|
|
],
|
|
_ => unreachable!(),
|
|
};
|
|
let entry = MONOCHROME_PALETTE[lookup[c as usize] as usize];
|
|
|
|
// Draw stuff. We're currently only in monochrome mode
|
|
self.set_pixel(
|
|
x.wrapping_add(x_o),
|
|
render_y,
|
|
sdl2::pixels::Color::RGB(entry[0], entry[1], entry[2]),
|
|
PixelOrigin::Sprite,
|
|
);
|
|
*/
|
|
let c = ((b1 as u8) * 2 + b2 as u8) as usize;
|
|
if c == 0 {
|
|
continue;
|
|
}
|
|
let c = self.object_palette_cgb[sprite.palette() as usize].get_color(c);
|
|
self.set_pixel(
|
|
x.wrapping_add(x_maybe_flipped),
|
|
render_y,
|
|
c,
|
|
PixelOrigin::Sprite,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_bg_window_tile_addr(&self, tile_id: u8) -> usize {
|
|
if (self.control & CTRL_BG_WINDOW_TILE_DATA_SELECT) == CTRL_BG_WINDOW_TILE_DATA_SELECT {
|
|
let base_addr = 0x0000;
|
|
base_addr + ((tile_id as usize) << 4)
|
|
} else {
|
|
let base_addr = 0x0800;
|
|
let tile_id = (128u8 as i8).wrapping_add(tile_id as i8) as u8;
|
|
base_addr + ((tile_id as usize) << 4)
|
|
}
|
|
}
|
|
|
|
pub fn dump_vram(&self) {
|
|
std::fs::File::create("vram0.dat")
|
|
.unwrap()
|
|
.write_all(&self.vram0)
|
|
.unwrap();
|
|
std::fs::File::create("vram1.dat")
|
|
.unwrap()
|
|
.write_all(&self.vram1)
|
|
.unwrap();
|
|
}
|
|
|
|
fn renderscan(&mut self) {
|
|
// Points to the background map offset to use.
|
|
let background_map: usize;
|
|
let window_map: usize;
|
|
// verify!
|
|
if self.control & CTRL_BG_TILE_MAP_SELECT == CTRL_BG_TILE_MAP_SELECT {
|
|
background_map = 0x1C00;
|
|
} else {
|
|
background_map = 0x1800;
|
|
}
|
|
|
|
if self.control & CTRL_WND_TILE_MAP_SELECT == CTRL_WND_TILE_MAP_SELECT {
|
|
window_map = 0x1C00;
|
|
} else {
|
|
window_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;
|
|
// Order sprites by priority
|
|
let mut queue: Vec<Sprite> = Vec::new();
|
|
for i in 0..39 {
|
|
queue.push(Sprite {
|
|
y: self.oam[i * 4 + 0],
|
|
x: self.oam[i * 4 + 1],
|
|
tile: self.oam[i * 4 + 2],
|
|
flags: self.oam[i * 4 + 3],
|
|
});
|
|
}
|
|
|
|
// This is the non-CGB priority.
|
|
// Smaller x coord = higher.
|
|
use std::cmp;
|
|
queue.sort_by(|x, y| {
|
|
if x.x > y.x {
|
|
cmp::Ordering::Greater
|
|
} else if x.x < y.x {
|
|
cmp::Ordering::Less
|
|
} else {
|
|
cmp::Ordering::Equal
|
|
}
|
|
});
|
|
queue.reverse();
|
|
|
|
// Render background
|
|
if (self.control & CTRL_BG_DISPLAY) == CTRL_BG_DISPLAY {
|
|
// Render pixels (20 tiles)
|
|
let render_abs_y = map_offset_y.wrapping_add(render_y);
|
|
let tile_index_y: u8 = render_abs_y >> 3;
|
|
let tile_offset_y: u8 = render_abs_y & 7;
|
|
|
|
for render_x in 0..160 {
|
|
// Absolute render coordinates
|
|
let render_abs_x = map_offset_x.wrapping_add(render_x);
|
|
let tile_index_x: u8 = render_abs_x >> 3;
|
|
let tile_offset_x: u8 = render_abs_x & 7;
|
|
|
|
let vram_offset: usize =
|
|
background_map + ((tile_index_y as usize) * 32) + tile_index_x as usize;
|
|
|
|
// Obtain tile ID in this area
|
|
let tile_id = self.vram0[vram_offset];
|
|
|
|
// GBC stuff
|
|
// Get BG map attributes
|
|
let bg_attribs = self.get_background_attribute(vram_offset);
|
|
let vram = [&*self.vram0, &*self.vram1][bg_attribs.vram_bank_number()];
|
|
|
|
// TODO: Priority
|
|
let tile_offset_y = if bg_attribs.vertical_flip() {
|
|
7 ^ tile_offset_y
|
|
} else {
|
|
tile_offset_y
|
|
};
|
|
|
|
// Obtain tile information
|
|
let tile_base_addr: usize = self.get_bg_window_tile_addr(tile_id);
|
|
// TODO: Colored background
|
|
let addr = tile_base_addr + (tile_offset_y as usize) * 2;
|
|
|
|
let tile_line_1 = vram[addr];
|
|
let tile_line_2 = vram[addr + 1];
|
|
|
|
// Get the correct bit
|
|
let b1 = if bg_attribs.horizontal_flip() {
|
|
7 ^ (1 << (7 - tile_offset_x))
|
|
} else {
|
|
1 << (7 - tile_offset_x)
|
|
};
|
|
let b2 = if bg_attribs.horizontal_flip() {
|
|
7 ^ (1 << (7 - tile_offset_x))
|
|
} else {
|
|
1 << (7 - tile_offset_x)
|
|
};
|
|
let b1: bool = (tile_line_1 & b1) != 0;
|
|
let b2: bool = (tile_line_2 & b2) != 0;
|
|
|
|
// Lookup the color
|
|
let c = ((b1 as u8) * 2 + b2 as u8) as usize;
|
|
let origin = match c {
|
|
0 => PixelOrigin::Empty, // Hack so that objects will be in front of it.
|
|
_ => PixelOrigin::Background,
|
|
};
|
|
let c = self.background_palette_cgb[bg_attribs.palette_number()].get_color(c);
|
|
self.set_pixel(render_x, render_y, c, origin);
|
|
/*
|
|
let c = (b1 as u8) * 2 + b2 as u8;
|
|
let lookup: [u8; 4] = [
|
|
self.background_palette & 3,
|
|
(self.background_palette >> 2) & 3,
|
|
(self.background_palette >> 4) & 3,
|
|
(self.background_palette >> 6) & 3,
|
|
];
|
|
let entry = MONOCHROME_PALETTE[lookup[c as usize] as usize];
|
|
|
|
let origin = match c {
|
|
0 => PixelOrigin::Empty, // Hack so that objects will be in front of it.
|
|
_ => PixelOrigin::Background,
|
|
};
|
|
self.set_pixel(
|
|
render_x,
|
|
render_y,
|
|
sdl2::pixels::Color::RGB(entry[0], entry[1], entry[2]),
|
|
origin,
|
|
);
|
|
*/
|
|
}
|
|
}
|
|
|
|
if (self.control & CTRL_WND_DISPLAY_ENABLE) == CTRL_WND_DISPLAY_ENABLE {
|
|
// Draw 'window' over the background.
|
|
// Screen coordinates of the top left corner are WX-7, WY
|
|
if self.windowx < 167 && self.windowy < 144 {
|
|
//let rx = self.windowx.wrapping_add(7);
|
|
let rx = 7u8.wrapping_sub(self.windowx);
|
|
let ry = render_y.wrapping_add(self.windowy);
|
|
for r_x in 0..160u8 {
|
|
let render_x = r_x.wrapping_add(rx);
|
|
// Absolute render coordinates
|
|
let tile_index_x: u8 = render_x >> 3;
|
|
let tile_index_y: u8 = ry >> 3;
|
|
let tile_offset_x: u8 = render_x & 7;
|
|
let tile_offset_y: u8 = ry & 7;
|
|
|
|
let vram_offset: usize =
|
|
window_map + (tile_index_y as usize) * 32 + tile_index_x as usize;
|
|
|
|
// Obtain tile ID in this area
|
|
let tile_id = self.vram0[vram_offset];
|
|
|
|
let bg_attribs = self.get_background_attribute(
|
|
//tile_index_x as usize, tile_index_y as usize
|
|
vram_offset,
|
|
);
|
|
|
|
// Get BG map attributes
|
|
let vram = if bg_attribs.vram_bank_number() == 0 {
|
|
&*self.vram0
|
|
} else {
|
|
&*self.vram1
|
|
};
|
|
|
|
// TODO: Priority
|
|
let tile_offset_x = if bg_attribs.vertical_flip() {
|
|
7 ^ tile_offset_x
|
|
} else {
|
|
tile_offset_x
|
|
};
|
|
let tile_offset_y = if bg_attribs.horizontal_flip() {
|
|
7 ^ tile_offset_y
|
|
} else {
|
|
tile_offset_y
|
|
};
|
|
|
|
// Obtain tile information
|
|
let tile_base_addr: usize = self.get_bg_window_tile_addr(tile_id);
|
|
let tile_addr = tile_base_addr + (tile_offset_y as usize) * 2;
|
|
let tile_line_1 = vram[tile_addr];
|
|
let tile_line_2 = vram[tile_addr + 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 c = (b1 as u8) * 2 + b2 as u8;
|
|
let origin = match c {
|
|
0 => PixelOrigin::Empty, // Hack so that objects will be in front of it.
|
|
_ => PixelOrigin::Window,
|
|
};
|
|
let c = self.background_palette_cgb[bg_attribs.palette_number()]
|
|
.get_color(c as usize);
|
|
self.set_pixel(render_x, render_y, c, origin);
|
|
/*
|
|
let lookup: [u8; 4] = [
|
|
self.background_palette & 3,
|
|
(self.background_palette >> 2) & 3,
|
|
(self.background_palette >> 4) & 3,
|
|
(self.background_palette >> 6) & 3,
|
|
];
|
|
let entry = MONOCHROME_PALETTE[lookup[c as usize] as usize];
|
|
|
|
// Draw stuff. We're currently only in monochrome mode
|
|
let origin = match c {
|
|
0 => PixelOrigin::Empty, // Hack so that objects will be in front of it.
|
|
_ => PixelOrigin::Window,
|
|
};
|
|
self.set_pixel(
|
|
render_x,
|
|
render_y,
|
|
sdl2::pixels::Color::RGB(entry[0], entry[1], entry[2]),
|
|
origin,
|
|
);
|
|
*/
|
|
}
|
|
}
|
|
}
|
|
if (self.control & CTRL_BG_SPRITE_ENABLE) == CTRL_BG_SPRITE_ENABLE {
|
|
self.render_sprites(&queue);
|
|
}
|
|
}
|
|
}
|