use num_traits::FromPrimitive;
use crate::gameboy::interrupt::{Interrupt, InterruptHandler};
pub const SCREEN_WIDTH: u8 = 160;
pub const SCREEN_HEIGHT: u8 = 144;
#[derive(Clone, Copy, Debug, FromPrimitive)]
pub enum TileDataAddressRange {
TileDataAddr8800_97FF = 0,
TileDataAddr8000_8FFF = 1,
}
impl From<u8> for TileDataAddressRange {
fn from(value: u8) -> TileDataAddressRange {
FromPrimitive::from_u8(value).expect("invalid tile data address bit")
}
}
impl From<TileDataAddressRange> for u8 {
fn from(value: TileDataAddressRange) -> u8 {
value as u8
}
}
#[derive(Clone, Copy, Debug, FromPrimitive)]
pub enum TileMapAddressRange {
TileMapAddr9800_9BFF = 0,
TileMapAddr9C00_9FFF = 1,
}
impl From<u8> for TileMapAddressRange {
fn from(value: u8) -> TileMapAddressRange {
FromPrimitive::from_u8(value).expect("invalid tile map address bit")
}
}
impl From<TileMapAddressRange> for u8 {
fn from(value: TileMapAddressRange) -> u8 {
value as u8
}
}
#[derive(Clone, Copy, Debug, FromPrimitive)]
pub enum SpriteSizes {
Size8x8 = 0,
Size8x16 = 1,
}
impl From<u8> for SpriteSizes {
fn from(value: u8) -> SpriteSizes {
FromPrimitive::from_u8(value).expect("invalid sprite size bit")
}
}
impl From<SpriteSizes> for u8 {
fn from(value: SpriteSizes) -> u8 {
value as u8
}
}
bitfield!{
struct Control(u8);
impl Debug;
enable, _: 7;
from into TileMapAddressRange, window_map, _: 6,6;
window_enable, _: 5;
from into TileDataAddressRange, tile_data, _: 4,4;
from into TileMapAddressRange, bg_map, _: 3,3;
from into SpriteSizes, sprite_size, _: 2,2;
sprite_enable, _: 1;
bg_enable, _: 0;
from into u8, bits, set_bits: 7,0;
}
#[derive(Clone, Copy, Debug, PartialEq, FromPrimitive)]
enum Mode {
HBlank = 0b00,
VBlank = 0b01,
OAMSearch = 0b10,
Transfer = 0b11,
}
impl From<u8> for Mode {
fn from(value: u8) -> Mode {
FromPrimitive::from_u8(value).expect("invalid mode")
}
}
impl From<Mode> for u8 {
fn from(value: Mode) -> u8 {
value as u8
}
}
bitfield!{
struct Status(u8);
impl Debug;
ly_coincidence_interrupt, _: 6;
oam_interrupt, _: 5;
vblank_interrupt, _: 4;
hblank_interrupt, _: 3;
coincidence_flag, set_coincidence_flag: 2;
from into Mode, mode_flag, set_mode_flag: 1,0;
from into u8, bits, set_bits: 7,0;
}
bitfield!{
#[derive(Clone, Copy)]
struct Attributes(u8);
impl Debug;
obj_to_bg_priority, _: 7;
y_flip, _: 6;
x_flip, _: 5;
u8, palette, _: 4,4;
from into u8, bits, set_bits: 7,0;
}
#[derive(Clone, Copy, Debug)]
pub struct OAM {
y_position: u8,
x_position: u8,
tile_number: u8,
attributes: Attributes,
}
impl OAM {
fn new() -> OAM {
OAM {
y_position: 0x00,
x_position: 0x00,
tile_number: 0x00,
attributes: Attributes(0x00),
}
}
}
bitfield!{
struct Palette(u8);
impl Debug;
from into Shade, colour, set_colour: 1,0,4;
from into u8, bits, set_bits: 7,0;
}
#[derive(Clone, Copy, Debug, FromPrimitive)]
enum Shade {
White = 0b00,
LightGray = 0b01,
DarkGray = 0b10,
Black = 0b11,
}
impl From<u8> for Shade {
fn from(value: u8) -> Shade {
FromPrimitive::from_u8(value).expect("invalid shade")
}
}
impl From<Shade> for u8 {
fn from(value: Shade) -> u8 {
value as u8
}
}
impl Shade {
fn into_pixel(&self) -> &[u8] {
use Shade::*;
match *self {
White => &[0xFF, 0xFF, 0xFF, 0xFF],
LightGray => &[0xCC, 0xCC, 0xCC, 0xFF],
DarkGray => &[0x77, 0x77, 0x77, 0xFF],
Black => &[0x00, 0x00, 0x00, 0xFF],
}
}
}
pub struct LCD {
pub vram_tile_data: [u8; 0x1800], pub vram_bg_maps: [u8; 0x0800], pub vram_oam: [OAM; 40],
control: Control,
status: Status,
scroll_y: u8,
scroll_x: u8,
scanline_cycle_count: i16,
lcd_y: u8, lcd_y_compare: u8,
bg_palette: Palette,
sprite_palette_0: Palette,
sprite_palette_1: Palette,
window_y: u8,
window_x: u8,
frame: [u8; SCREEN_WIDTH as usize * SCREEN_HEIGHT as usize * 4],
last_frame_hash: u64,
vblank_set: bool,
}
impl LCD {
const SCANLINE_CYCLE_TOTAL: i16 = 456; const MODE2_CYCLE_RANGE: i16 = LCD::SCANLINE_CYCLE_TOTAL - 80;
const MODE3_CYCLE_RANGE: i16 = LCD::MODE2_CYCLE_RANGE - 172;
const VBLANK_HEIGHT: u8 = 154;
pub fn new() -> LCD {
LCD {
vram_tile_data: [0x00; 0x1800],
vram_bg_maps: [0x00; 0x0800],
vram_oam: [OAM::new(); 40],
control: Control(0x80),
status: Status(0x00),
scroll_y: 0x00,
scroll_x: 0x00,
scanline_cycle_count: LCD::SCANLINE_CYCLE_TOTAL,
lcd_y: 0x00,
lcd_y_compare: 0x00,
bg_palette: Palette(0x00),
sprite_palette_0: Palette(0x00),
sprite_palette_1: Palette(0x00),
window_y: 0x00,
window_x: 0x00,
frame: [0x00; SCREEN_WIDTH as usize * SCREEN_HEIGHT as usize * 4],
last_frame_hash: 0,
vblank_set: false,
}
}
pub fn read_register(&self, addr: u16) -> u8 {
match addr {
0xFF40 => self.control.bits(),
0xFF41 => self.status.bits(),
0xFF42 => self.scroll_y,
0xFF43 => self.scroll_x,
0xFF44 => self.lcd_y,
0xFF45 => self.lcd_y_compare,
0xFF46 => 0xFF, 0xFF47 => self.bg_palette.bits(), 0xFF48 => self.sprite_palette_0.bits(), 0xFF49 => self.sprite_palette_1.bits(), 0xFF4A => self.window_y,
0xFF4B => self.window_x,
_ => unreachable!(), }
}
pub fn write_register(&mut self, addr: u16, value: u8) {
match addr {
0xFF40 => {
if self.control.enable() != (value & 0x80 > 0) {
self.lcd_y = 0;
self.scanline_cycle_count = LCD::SCANLINE_CYCLE_TOTAL;
let mode = if value & 0x80 > 0 { Mode::OAMSearch } else { Mode::HBlank };
self.status.set_mode_flag(mode);
}
self.control.set_bits(value)
},
0xFF41 => self.status.set_bits(value),
0xFF42 => self.scroll_y = value,
0xFF43 => self.scroll_x = value,
0xFF44 => self.lcd_y = 0x00, 0xFF45 => self.lcd_y_compare = value,
0xFF47 => self.bg_palette.set_bits(value), 0xFF48 => self.sprite_palette_0.set_bits(value), 0xFF49 => self.sprite_palette_1.set_bits(value), 0xFF4A => self.window_y = value,
0xFF4B => self.window_x = value,
_ => unreachable!(), }
}
pub fn read_oam(&self, addr: u16) -> u8 {
let oam_addr = (addr / 4) as usize;
match addr % 4 {
0x0 => self.vram_oam[oam_addr].y_position,
0x1 => self.vram_oam[oam_addr].x_position,
0x2 => self.vram_oam[oam_addr].tile_number,
0x3 => self.vram_oam[oam_addr].attributes.bits() as u8,
_ => unreachable!(),
}
}
pub fn write_oam(&mut self, addr: u16, value: u8) {
let oam_addr = (addr / 4) as usize;
match addr % 4 {
0x0 => self.vram_oam[oam_addr].y_position = value,
0x1 => self.vram_oam[oam_addr].x_position = value,
0x2 => self.vram_oam[oam_addr].tile_number = value,
0x3 => self.vram_oam[oam_addr].attributes.set_bits(value),
_ => unreachable!(),
}
}
pub fn step(&mut self, ih: &mut InterruptHandler) {
self.set_status(ih);
if !self.control.enable() {
return;
}
self.scanline_cycle_count -= 4;
if self.scanline_cycle_count > 0 {
return;
}
self.scanline_cycle_count = LCD::SCANLINE_CYCLE_TOTAL;
match self.lcd_y {
0..=SCREEN_HEIGHT if self.lcd_y < SCREEN_HEIGHT => {
self.draw_scanline();
self.lcd_y += 1;
},
SCREEN_HEIGHT => {
ih.set_interrupt(Interrupt::VBlank);
self.lcd_y += 1;
},
LCD::VBLANK_HEIGHT => self.lcd_y = 0,
_ => self.lcd_y += 1,
}
}
pub fn vblank_reached(&mut self) -> bool {
if self.vblank_set {
self.vblank_set = false;
true
} else {
false
}
}
pub fn get_frame(&self) -> &[u8] {
&self.frame
}
fn set_status(&mut self, ih: &mut InterruptHandler) {
if !self.control.enable() {
self.scanline_cycle_count = LCD::SCANLINE_CYCLE_TOTAL;
self.lcd_y = 0;
self.status.set_mode_flag(Mode::VBlank);
return;
}
let prev_mode = self.status.mode_flag();
if self.lcd_y >= SCREEN_HEIGHT {
self.status.set_mode_flag(Mode::VBlank);
} else {
if self.scanline_cycle_count >= LCD::MODE2_CYCLE_RANGE as i16 {
self.status.set_mode_flag(Mode::OAMSearch);
} else if self.scanline_cycle_count >= LCD::MODE3_CYCLE_RANGE as i16 {
self.status.set_mode_flag(Mode::Transfer);
} else {
self.status.set_mode_flag(Mode::HBlank);
}
}
if prev_mode != self.status.mode_flag() {
match self.status.mode_flag() {
Mode::HBlank => self.hblank(ih),
Mode::VBlank => self.vblank(ih),
Mode::OAMSearch => self.oam_search(ih),
Mode::Transfer => (),
}
}
if self.lcd_y == self.lcd_y_compare {
self.status.set_coincidence_flag(true);
if self.status.ly_coincidence_interrupt() {
self.lcdc_interrupt(ih)
}
} else {
self.status.set_coincidence_flag(false);
}
}
fn hblank(&self, ih: &mut InterruptHandler) {
if self.status.hblank_interrupt() {
self.lcdc_interrupt(ih)
}
}
fn vblank(&mut self, ih: &mut InterruptHandler) {
if self.status.vblank_interrupt() {
self.lcdc_interrupt(ih);
}
self.vblank_set = true;
}
fn oam_search(&self, ih: &mut InterruptHandler) {
if self.status.oam_interrupt() {
self.lcdc_interrupt(ih);
}
}
fn lcdc_interrupt(&self, ih: &mut InterruptHandler) {
ih.set_interrupt(Interrupt::LCDC);
}
fn draw_scanline(&mut self) {
if self.control.bg_enable() {
self.draw_bg();
}
if self.control.sprite_enable() {
self.draw_sprites();
}
}
fn draw_bg(&mut self) {
use TileDataAddressRange::*;
use TileMapAddressRange::*;
let in_window = self.control.window_enable() && self.lcd_y >= self.window_y;
let tile_data_offset = match self.control.tile_data() {
TileDataAddr8000_8FFF => 0x0000 as u16,
TileDataAddr8800_97FF => 0x0800 as u16,
};
let map = if in_window { self.control.window_map() } else { self.control.bg_map() };
let tile_map_offset = match map {
TileMapAddr9800_9BFF => 0x0000 as u16,
TileMapAddr9C00_9FFF => 0x0400 as u16,
};
let map_y = if in_window {
self.lcd_y - self.window_y
} else {
self.scroll_y.wrapping_add(self.lcd_y)
};
let tile_y = (map_y / 8) as u16;
for pixel_x in 0..SCREEN_WIDTH {
let map_x = if in_window && pixel_x >= self.window_x - 7 {
pixel_x - (self.window_x - 7)
} else {
pixel_x.wrapping_add(self.scroll_x)
};
let tile_x = (map_x / 8) as u16;
let tile_map_addr = tile_map_offset + (tile_y * 32) + tile_x;
let tile_id = match self.control.tile_data() {
TileDataAddr8000_8FFF => self.vram_bg_maps[tile_map_addr as usize] as u16,
TileDataAddr8800_97FF => {
(self.vram_bg_maps[tile_map_addr as usize] as i8 as i16 + 128) as u16
}
};
let tile_data_addr = tile_data_offset + (tile_id * 16);
let tile_row_offset = ((map_y % 8) * 2) as u16;
let pixel_start = (tile_data_addr + tile_row_offset) as usize;
let pixel_end = pixel_start + 2;
let pixel_data = &self.vram_tile_data[pixel_start..pixel_end];
let pixel_bit = 7 - (map_x % 8);
let shade = self.get_shade(pixel_data, pixel_bit, &self.bg_palette);
let pixel = shade.into_pixel();
let frame_pixel_start =
(self.lcd_y as usize * SCREEN_WIDTH as usize * 4) + (pixel_x as usize * 4);
let frame_pixel_end = frame_pixel_start + 4;
let pixel_slice = &mut self.frame[frame_pixel_start..frame_pixel_end];
pixel_slice.clone_from_slice(&pixel[..4]);
}
}
fn draw_sprites(&mut self) {
let y_size = match self.control.sprite_size() {
SpriteSizes::Size8x8 => 8,
SpriteSizes::Size8x16 => 16,
};
for sprite in self.vram_oam.iter() {
let y_pos = sprite.y_position;
if y_pos <= (self.lcd_y + 16 - y_size) || y_pos > (self.lcd_y + 16) {
continue;
}
let sprite_line = if sprite.attributes.y_flip() {
(y_size - 1) - (self.lcd_y + 16 - y_pos)
} else {
self.lcd_y + 16 - y_pos
};
let sprite_data_start =
((sprite.tile_number as u16 * 16) + (sprite_line as u16 * 2)) as usize;
let sprite_data_end = sprite_data_start + 2;
let pixel_data = &self.vram_tile_data[sprite_data_start..sprite_data_end];
for sprite_column in 0..8 {
let mut pixel_x = sprite.x_position as u16 + sprite_column as u16;
if pixel_x < 8 || pixel_x >= SCREEN_WIDTH as u16 + 8 {
continue;
}
pixel_x = pixel_x - 8;
let pixel_bit = if sprite.attributes.x_flip() {
sprite_column
} else {
7 - sprite_column
};
let palette = match sprite.attributes.palette() {
0 => &self.sprite_palette_0,
1 => &self.sprite_palette_1,
_ => unreachable!(), };
let palette_index = self.get_palette_index(pixel_data, pixel_bit);
if palette_index == 0 {
continue;
}
let shade = palette.colour(palette_index);
let pixel = shade.into_pixel();
let frame_pixel_start =
(self.lcd_y as usize * SCREEN_WIDTH as usize * 4) + (pixel_x as usize * 4);
let frame_pixel_end = frame_pixel_start + 4;
let pixel_slice = &mut self.frame[frame_pixel_start..frame_pixel_end];
pixel_slice.clone_from_slice(&pixel[..4]);
}
}
}
fn get_palette_index(&self, pixel_data: &[u8], pixel_bit: u8) -> usize {
let top_bit = (pixel_data[1] >> pixel_bit) & 0b1;
let bot_bit = (pixel_data[0] >> pixel_bit) & 0b1;
((top_bit << 1) | bot_bit) as usize
}
fn get_shade(&self, pixel_data: &[u8], pixel_bit: u8, palette: &Palette) -> Shade {
let palette_index = self.get_palette_index(pixel_data, pixel_bit);
palette.colour(palette_index)
}
fn save_frame(&self) -> Result<(), png::EncodingError> {
use std::fs::File;
use std::io::BufWriter;
use std::path::Path;
let path = Path::new(r"./frame.png");
let file = File::create(path)?;
let ref mut w = BufWriter::new(file);
let mut png_encoder =
png::Encoder::new(w, SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32);
png_encoder.set_color(png::ColorType::RGBA);
png_encoder.set_depth(png::BitDepth::Eight);
let mut writer = png_encoder.write_header()?;
writer.write_image_data(&self.frame)?;
Ok(())
}
fn save_tile_data(&self) -> Result<(), png::EncodingError> {
let mut tile_pixels = [0x00; 256 * 96 * 4];
for line in 0..96 {
let tile_row_offset = (line % 8) * 2;
for col in 0..256u16 {
let tile_id = (line / 8) * 32 + (col / 8);
let tile_data_offset = tile_id * 16;
let pixel_start = (tile_data_offset + tile_row_offset) as usize;
let pixel_end = pixel_start + 2;
let pixel_data = &self.vram_tile_data[pixel_start..pixel_end];
let pixel_bit = 7 - (col % 8);
let shade = self.get_shade(pixel_data, pixel_bit as u8, &self.bg_palette);
let pixel = shade.into_pixel();
let pixel_start = (line as usize * 256 as usize * 4) + (col as usize * 4);
let pixel_end = pixel_start + 4;
let pixel_slice = &mut tile_pixels[pixel_start..pixel_end];
pixel_slice.clone_from_slice(&pixel[..4]);
}
}
use std::path::Path;
use std::fs::File;
use std::io::BufWriter;
let path = Path::new(r"./tiledata.png");
let file = File::create(path)?;
let ref mut w = BufWriter::new(file);
let mut png_encoder = png::Encoder::new(w, 256, 96);
png_encoder.set_color(png::ColorType::RGBA);
png_encoder.set_depth(png::BitDepth::Eight);
let mut writer = png_encoder.write_header()?;
writer.write_image_data(&tile_pixels)?;
Ok(())
}
}