2023-02-06 12:14:51 +11:00
|
|
|
use minifb::{Window, WindowOptions};
|
|
|
|
|
2023-02-05 22:56:18 +11:00
|
|
|
use crate::{FACTOR, HEIGHT, WIDTH};
|
2023-02-05 18:46:55 +11:00
|
|
|
|
2023-02-06 12:14:51 +11:00
|
|
|
use super::{as_signed, get_bit, set_bit, set_or_clear_bit, CPU};
|
|
|
|
|
|
|
|
mod tile_window;
|
2023-02-05 18:46:55 +11:00
|
|
|
|
|
|
|
#[derive(PartialEq)]
|
|
|
|
enum DrawMode {
|
|
|
|
HBlank,
|
|
|
|
VBlank,
|
|
|
|
Mode2,
|
|
|
|
Mode3,
|
|
|
|
}
|
|
|
|
|
2023-02-05 22:37:49 +11:00
|
|
|
enum TilemapArea {
|
|
|
|
T9800,
|
|
|
|
T9C00,
|
|
|
|
}
|
|
|
|
|
2023-02-06 11:42:45 +11:00
|
|
|
impl TilemapArea {
|
|
|
|
fn get_offset(&self) -> u16 {
|
|
|
|
match self {
|
|
|
|
TilemapArea::T9800 => 0x9800,
|
|
|
|
TilemapArea::T9C00 => 0x9C00,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-05 22:37:49 +11:00
|
|
|
enum TiledataArea {
|
|
|
|
D8000,
|
2023-02-06 11:42:45 +11:00
|
|
|
D9000,
|
2023-02-05 22:37:49 +11:00
|
|
|
}
|
|
|
|
|
2023-02-05 23:07:55 +11:00
|
|
|
impl TiledataArea {
|
|
|
|
fn get_addr(&self, addr: u8) -> u16 {
|
|
|
|
match self {
|
2023-02-06 12:14:51 +11:00
|
|
|
TiledataArea::D8000 => 0x8000 + ((addr as u16) * 16),
|
|
|
|
TiledataArea::D9000 => 0x9000_u16.wrapping_add_signed((as_signed(addr) as i16) * 16),
|
2023-02-05 23:07:55 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-05 22:37:49 +11:00
|
|
|
enum ObjSize {
|
|
|
|
S8x8,
|
|
|
|
S8x16,
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
2023-02-06 11:42:45 +11:00
|
|
|
#[derive(Clone, Copy, Debug)]
|
2023-02-05 22:37:49 +11:00
|
|
|
enum Colour {
|
|
|
|
White,
|
|
|
|
LightGray,
|
|
|
|
DarkGray,
|
|
|
|
Black,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Colour {
|
|
|
|
fn to_rgb(&self) -> u32 {
|
|
|
|
match self {
|
|
|
|
Colour::White => Self::from_u8_rgb(255, 255, 255),
|
|
|
|
Colour::LightGray => Self::from_u8_rgb(190, 190, 190),
|
|
|
|
Colour::DarkGray => Self::from_u8_rgb(110, 110, 110),
|
|
|
|
Colour::Black => Self::from_u8_rgb(20, 20, 20),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
2023-02-06 12:14:51 +11:00
|
|
|
const TILE_WINDOW_EDGE_LENGTH: usize = 16 * 8;
|
|
|
|
const TILE_WINDOW_EDGE_LENGTH_SCALED: usize = TILE_WINDOW_EDGE_LENGTH * FACTOR;
|
|
|
|
|
2023-02-05 18:46:55 +11:00
|
|
|
pub struct GPU {
|
|
|
|
pub buffer: Vec<u32>,
|
2023-02-05 22:56:18 +11:00
|
|
|
scaled_buffer: Vec<u32>,
|
2023-02-05 18:46:55 +11:00
|
|
|
mode: DrawMode,
|
|
|
|
mode_clock: usize,
|
2023-02-05 22:37:49 +11:00
|
|
|
scanline: u8,
|
2023-02-06 12:14:51 +11:00
|
|
|
sprite_buffer: Vec<u32>,
|
|
|
|
sprite_buffer_scaled: Vec<u32>,
|
|
|
|
sprite_window: Window,
|
2023-02-05 18:46:55 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for GPU {
|
|
|
|
fn default() -> Self {
|
2023-02-06 12:14:51 +11:00
|
|
|
let window = Window::new(
|
|
|
|
"Tiles",
|
|
|
|
TILE_WINDOW_EDGE_LENGTH_SCALED,
|
|
|
|
TILE_WINDOW_EDGE_LENGTH_SCALED,
|
|
|
|
WindowOptions::default(),
|
|
|
|
)
|
|
|
|
.unwrap_or_else(|e| {
|
|
|
|
panic!("{}", e);
|
|
|
|
});
|
2023-02-05 18:46:55 +11:00
|
|
|
Self {
|
|
|
|
buffer: vec![0; WIDTH * HEIGHT],
|
2023-02-05 22:56:18 +11:00
|
|
|
scaled_buffer: vec![0; WIDTH * HEIGHT * 4],
|
2023-02-05 18:46:55 +11:00
|
|
|
mode: DrawMode::Mode2,
|
|
|
|
mode_clock: 0,
|
|
|
|
scanline: 0,
|
2023-02-06 12:14:51 +11:00
|
|
|
sprite_buffer: vec![0; TILE_WINDOW_EDGE_LENGTH * TILE_WINDOW_EDGE_LENGTH],
|
|
|
|
sprite_buffer_scaled: vec![
|
|
|
|
0;
|
|
|
|
TILE_WINDOW_EDGE_LENGTH_SCALED
|
|
|
|
* TILE_WINDOW_EDGE_LENGTH_SCALED
|
|
|
|
],
|
|
|
|
sprite_window: window,
|
2023-02-05 18:46:55 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CPU {
|
|
|
|
pub fn advance_gpu_clock(&mut self, steps: u8) {
|
|
|
|
let real_steps = (steps as usize) * 4;
|
|
|
|
self.gpu.mode_clock += real_steps;
|
2023-02-05 22:37:49 +11:00
|
|
|
|
|
|
|
let lcdc = self.get_lcdc();
|
|
|
|
|
2023-02-05 18:46:55 +11:00
|
|
|
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 {
|
2023-02-05 22:37:49 +11:00
|
|
|
self.enter_vblank(&lcdc);
|
2023-02-05 18:46:55 +11:00
|
|
|
} 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 {
|
2023-02-05 18:50:07 +11:00
|
|
|
self.exit_vblank();
|
2023-02-05 18:46:55 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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;
|
2023-02-05 22:37:49 +11:00
|
|
|
self.enter_hblank(&lcdc);
|
2023-02-05 18:46:55 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self.set_lcd_status();
|
|
|
|
}
|
|
|
|
|
2023-02-05 22:37:49 +11:00
|
|
|
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 {
|
2023-02-06 11:42:45 +11:00
|
|
|
TiledataArea::D9000
|
2023-02-05 22:37:49 +11:00
|
|
|
},
|
|
|
|
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) {
|
2023-02-05 18:46:55 +11:00
|
|
|
self.gpu.mode = DrawMode::HBlank;
|
2023-02-05 22:37:49 +11:00
|
|
|
self.render_scanline(self.gpu.scanline, lcdc);
|
2023-02-05 18:46:55 +11:00
|
|
|
}
|
|
|
|
|
2023-02-05 22:37:49 +11:00
|
|
|
fn enter_vblank(&mut self, lcdc: &LCDC) {
|
2023-02-05 18:46:55 +11:00
|
|
|
self.gpu.mode = DrawMode::VBlank;
|
2023-02-05 22:56:18 +11:00
|
|
|
if lcdc.enable {
|
2023-02-05 23:07:55 +11:00
|
|
|
self.render_window();
|
|
|
|
self.memory.set(0xFF0F, set_bit(self.memory.get(0xFF0F), 0));
|
2023-02-05 22:56:18 +11:00
|
|
|
}
|
2023-02-05 18:46:55 +11:00
|
|
|
}
|
|
|
|
|
2023-02-05 18:50:07 +11:00
|
|
|
fn exit_vblank(&mut self) {
|
|
|
|
self.gpu.mode = DrawMode::Mode2;
|
|
|
|
self.gpu.scanline = 0;
|
2023-02-06 12:14:51 +11:00
|
|
|
self.draw_sprite_window(
|
|
|
|
TiledataArea::D8000,
|
|
|
|
byte_to_palette(self.memory.get(0xFF47)),
|
|
|
|
);
|
2023-02-05 22:37:49 +11:00
|
|
|
// self.memory
|
|
|
|
// .set(0xFF0F, clear_bit(self.memory.get(0xFF0F), 0));
|
2023-02-05 18:50:07 +11:00
|
|
|
}
|
|
|
|
|
2023-02-05 18:46:55 +11:00
|
|
|
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);
|
2023-02-05 22:37:49 +11:00
|
|
|
self.memory.set(0xFF44, self.gpu.scanline);
|
|
|
|
// println!("set scanline to {}", self.memory.get(0xFF44));
|
|
|
|
}
|
|
|
|
|
|
|
|
fn render_scanline(&mut self, scanline: u8, lcdc: &LCDC) {
|
|
|
|
if lcdc.bg_window_enable {
|
2023-02-06 12:14:02 +11:00
|
|
|
let palette = byte_to_palette(self.memory.get(0xFF47));
|
|
|
|
self.render_scanline_bg(scanline, lcdc, palette);
|
2023-02-05 22:37:49 +11:00
|
|
|
if lcdc.window_enable {
|
2023-02-06 12:14:02 +11:00
|
|
|
self.render_scanline_window(scanline, lcdc, palette);
|
2023-02-05 22:37:49 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if lcdc.obj_enable {
|
|
|
|
self.render_scanline_obj(scanline, lcdc);
|
|
|
|
}
|
2023-02-05 18:46:55 +11:00
|
|
|
}
|
|
|
|
|
2023-02-06 12:14:02 +11:00
|
|
|
fn render_scanline_bg(&mut self, scanline: u8, lcdc: &LCDC, palette: Palette) {
|
2023-02-05 22:37:49 +11:00
|
|
|
let scroll_y = self.memory.get(0xFF42);
|
|
|
|
let scroll_x = self.memory.get(0xFF43);
|
2023-02-05 23:07:55 +11:00
|
|
|
self.render_tiles(
|
|
|
|
scanline,
|
|
|
|
&lcdc.bg_tilemap,
|
|
|
|
&lcdc.tile_area,
|
|
|
|
palette,
|
|
|
|
scroll_x,
|
|
|
|
scroll_y,
|
|
|
|
);
|
2023-02-05 22:37:49 +11:00
|
|
|
}
|
|
|
|
|
2023-02-06 12:14:02 +11:00
|
|
|
fn render_scanline_window(&mut self, scanline: u8, lcdc: &LCDC, palette: Palette) {
|
2023-02-05 22:37:49 +11:00
|
|
|
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 {
|
2023-02-06 12:14:02 +11:00
|
|
|
self.render_tiles(
|
|
|
|
scanline,
|
|
|
|
&lcdc.window_tilemap,
|
|
|
|
&lcdc.tile_area,
|
|
|
|
palette,
|
|
|
|
pos_x,
|
|
|
|
pos_y,
|
|
|
|
)
|
2023-02-05 22:37:49 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn render_scanline_obj(&mut self, _scanline: u8, _lcdc: &LCDC) {}
|
|
|
|
|
2023-02-05 23:07:55 +11:00
|
|
|
fn render_tiles(
|
|
|
|
&mut self,
|
|
|
|
scanline: u8,
|
|
|
|
tilemap: &TilemapArea,
|
|
|
|
tiledata: &TiledataArea,
|
|
|
|
palette: Palette,
|
2023-02-06 12:14:02 +11:00
|
|
|
_offset_x: u8,
|
2023-02-05 23:07:55 +11:00
|
|
|
offset_y: u8,
|
|
|
|
) {
|
|
|
|
let tile_line = (scanline as usize) + (offset_y as usize);
|
2023-02-06 11:42:45 +11:00
|
|
|
let tilemap_row = tile_line / 8;
|
|
|
|
let tile_px = (tile_line) % 8;
|
|
|
|
let tiledata_offset = tile_px * 2;
|
|
|
|
let row_addr = (tilemap_row * 32) as u16 + tilemap.get_offset();
|
2023-02-05 22:37:49 +11:00
|
|
|
for x in 0..32 {
|
2023-02-06 11:42:45 +11:00
|
|
|
let tile_addr =
|
|
|
|
tiledata.get_addr(self.memory.get(row_addr + x)) + tiledata_offset as u16;
|
|
|
|
let lsbs = self.memory.get(tile_addr);
|
|
|
|
let msbs = self.memory.get(tile_addr + 1);
|
2023-02-05 22:37:49 +11:00
|
|
|
for px_x in 0..8 {
|
2023-02-06 12:14:02 +11:00
|
|
|
let lsb = get_bit(lsbs, 7 - px_x);
|
|
|
|
let msb = get_bit(msbs, 7 - px_x);
|
2023-02-05 22:37:49 +11:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-02-05 18:46:55 +11:00
|
|
|
|
|
|
|
fn render_window(&mut self) {
|
2023-02-05 22:56:18 +11:00
|
|
|
self.gpu.scaled_buffer = scale_buffer(&self.gpu.buffer, WIDTH, HEIGHT, FACTOR);
|
2023-02-05 18:46:55 +11:00
|
|
|
self.window
|
2023-02-05 22:56:18 +11:00
|
|
|
.update_with_buffer(&self.gpu.scaled_buffer, WIDTH * FACTOR, HEIGHT * FACTOR)
|
2023-02-05 18:46:55 +11:00
|
|
|
.unwrap();
|
|
|
|
}
|
|
|
|
}
|
2023-02-05 22:37:49 +11:00
|
|
|
|
2023-02-05 22:56:18 +11:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-02-05 22:37:49 +11:00
|
|
|
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::DarkGray,
|
|
|
|
(false, true) => Colour::LightGray,
|
|
|
|
(false, false) => Colour::White,
|
|
|
|
}
|
|
|
|
}
|