918 lines
26 KiB
Rust
918 lines
26 KiB
Rust
use crate::bus::BusIo;
|
|
use crate::Cycle;
|
|
use crate::GB_HEIGHT;
|
|
use crate::GB_WIDTH;
|
|
use dma::DirectMemoryAccess;
|
|
use std::collections::VecDeque;
|
|
use std::convert::TryInto;
|
|
pub(crate) use types::PpuMode;
|
|
use types::{
|
|
BackgroundPalette, GrayShade, LCDControl, LCDStatus, ObjectFlags, ObjectPalette,
|
|
ObjectPaletteKind, ObjectSize, Pixels, RenderPriority, TileDataAddress,
|
|
};
|
|
|
|
mod dma;
|
|
mod types;
|
|
|
|
const VRAM_SIZE: usize = 0x2000;
|
|
const OAM_SIZE: usize = 0xA0;
|
|
const PPU_START_ADDRESS: usize = 0x8000;
|
|
|
|
// OAM Scan
|
|
const OBJECT_LIMIT: usize = 10;
|
|
|
|
// // White
|
|
// const WHITE: [u8; 4] = 0xFFFFFFFFu32.to_be_bytes();
|
|
// const LIGHT_GRAY: [u8; 4] = 0xB6B6B6FFu32.to_be_bytes();
|
|
// const DARK_GRAY: [u8; 4] = 0x676767FFu32.to_be_bytes();
|
|
// const BLACK: [u8; 4] = 0x000000FFu32.to_be_bytes();
|
|
|
|
// Green
|
|
const WHITE: [u8; 4] = 0xE3EEC0FFu32.to_be_bytes();
|
|
const LIGHT_GRAY: [u8; 4] = 0xAEBA89FFu32.to_be_bytes();
|
|
const DARK_GRAY: [u8; 4] = 0x5E6745FFu32.to_be_bytes();
|
|
const BLACK: [u8; 4] = 0x202020FFu32.to_be_bytes();
|
|
|
|
#[derive(Debug)]
|
|
pub struct Ppu {
|
|
pub(crate) int: Interrupt,
|
|
/// 0xFF40 | LCDC - LCD Control
|
|
pub(crate) ctrl: LCDControl,
|
|
/// 0xFF41 | STAT - LCD Status
|
|
pub(crate) stat: LCDStatus,
|
|
pub(crate) monochrome: Monochrome,
|
|
pub(crate) pos: ScreenPosition,
|
|
vram: Box<[u8; VRAM_SIZE]>,
|
|
pub(crate) oam: ObjectAttributeTable,
|
|
pub(crate) dma: DirectMemoryAccess,
|
|
scan_dot: Cycle,
|
|
fetch: PixelFetcher,
|
|
fifo: PixelFifo,
|
|
obj_buffer: ObjectBuffer,
|
|
frame_buf: Box<[u8; GB_WIDTH * GB_HEIGHT * 4]>,
|
|
win_stat: WindowStatus,
|
|
|
|
scanline_start: bool,
|
|
to_discard: u8,
|
|
|
|
x_pos: u8,
|
|
dot: Cycle,
|
|
}
|
|
|
|
impl BusIo for Ppu {
|
|
fn read_byte(&self, addr: u16) -> u8 {
|
|
self.vram[addr as usize - PPU_START_ADDRESS]
|
|
}
|
|
|
|
fn write_byte(&mut self, addr: u16, byte: u8) {
|
|
self.vram[addr as usize - PPU_START_ADDRESS] = byte;
|
|
}
|
|
}
|
|
|
|
impl Ppu {
|
|
pub(crate) fn tick(&mut self) {
|
|
self.dot += 1;
|
|
|
|
if !self.ctrl.lcd_enabled() {
|
|
return;
|
|
}
|
|
|
|
match self.stat.mode() {
|
|
PpuMode::OamScan => {
|
|
// Cycles 1 -> 80
|
|
if self.dot >= 80 {
|
|
self.x_pos = 0;
|
|
self.scanline_start = true;
|
|
self.fetch.back.tile_high_reset = true;
|
|
self.to_discard = 0;
|
|
self.fifo.back.clear();
|
|
self.fifo.obj.clear();
|
|
|
|
// Sort Sprites
|
|
self.obj_buffer.inner.sort_by(|left, right| {
|
|
left.zip(*right)
|
|
.map(|(left, right)| right.x.cmp(&left.x))
|
|
.unwrap_or(std::cmp::Ordering::Greater)
|
|
});
|
|
|
|
// if self.obj_buffer.len != 0 {
|
|
// dbg!(&self.obj_buffer);
|
|
// }
|
|
|
|
self.stat.set_mode(PpuMode::Drawing);
|
|
}
|
|
|
|
self.scan_oam();
|
|
}
|
|
PpuMode::Drawing => {
|
|
if self.ctrl.lcd_enabled() {
|
|
// Only Draw when the LCD Is Enabled
|
|
self.draw();
|
|
} else {
|
|
self.reset();
|
|
}
|
|
|
|
if self.x_pos == 160 {
|
|
if self.stat.hblank_int() {
|
|
// Enable HBlank LCDStat Interrupt
|
|
self.int.set_lcd_stat(true);
|
|
}
|
|
|
|
// Done with rendering this frame,
|
|
// we can reset the ppu x_pos and fetcher state now
|
|
|
|
// Increment Window line counter if scanline had any window pixels on it
|
|
// only increment once per scanline though
|
|
if self.win_stat.enabled {
|
|
self.fetch.back.wl_count += 1;
|
|
}
|
|
|
|
self.fetch.hblank_reset();
|
|
self.win_stat.enabled = false;
|
|
|
|
self.obj_buffer.clear();
|
|
self.stat.set_mode(PpuMode::HBlank);
|
|
}
|
|
}
|
|
PpuMode::HBlank => {
|
|
// This mode will always end at 456 cycles
|
|
|
|
if self.dot >= 456 {
|
|
self.dot %= 456;
|
|
self.pos.line_y += 1;
|
|
|
|
// Update LY==LYC bit
|
|
let are_equal = self.pos.line_y == self.pos.ly_compare;
|
|
self.stat.set_coincidence(are_equal);
|
|
|
|
// Request LCD STAT interrupt if conditions met
|
|
if self.stat.coincidence_int() && are_equal {
|
|
self.int.set_lcd_stat(true);
|
|
}
|
|
|
|
let next_mode = if self.pos.line_y >= 144 {
|
|
// Request VBlank Interrupt
|
|
self.int.set_vblank(true);
|
|
|
|
// Reset Window Line Counter in Fetcher
|
|
self.fetch.back.wl_count = 0;
|
|
|
|
// Reset WY=LY coincidence flag
|
|
self.win_stat.coincidence = false;
|
|
|
|
if self.stat.vblank_int() {
|
|
// Enable Vblank LCDStat Interrupt
|
|
self.int.set_lcd_stat(true);
|
|
}
|
|
|
|
PpuMode::VBlank
|
|
} else {
|
|
if self.stat.oam_int() {
|
|
// Enable OAM LCDStat Interrupt
|
|
self.int.set_lcd_stat(true);
|
|
}
|
|
|
|
self.scan_dot = Default::default();
|
|
PpuMode::OamScan
|
|
};
|
|
|
|
self.stat.set_mode(next_mode);
|
|
}
|
|
}
|
|
PpuMode::VBlank => {
|
|
if self.dot >= 456 {
|
|
self.dot %= 456;
|
|
self.pos.line_y += 1;
|
|
|
|
// Update LY==LYC bit
|
|
let are_equal = self.pos.line_y == self.pos.ly_compare;
|
|
self.stat.set_coincidence(are_equal);
|
|
|
|
// Request LCD STAT interrupt if conditions met
|
|
if self.stat.coincidence_int() && are_equal {
|
|
self.int.set_lcd_stat(true);
|
|
}
|
|
|
|
if self.pos.line_y == 154 {
|
|
self.pos.line_y = 0;
|
|
|
|
if self.stat.oam_int() {
|
|
// Enable OAM LCDStat Interrupt
|
|
self.int.set_lcd_stat(true);
|
|
}
|
|
|
|
self.scan_dot = Default::default();
|
|
self.stat.set_mode(PpuMode::OamScan);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn scan_oam(&mut self) {
|
|
if self.scan_dot % 2 == 0 {
|
|
if self.dma.is_active() {
|
|
return;
|
|
}
|
|
|
|
if !self.win_stat.coincidence && self.scan_dot == 0 {
|
|
self.win_stat.coincidence = self.pos.line_y == self.pos.window_y;
|
|
}
|
|
|
|
let obj_height = self.ctrl.obj_size().size();
|
|
let attr = self.oam.attribute(self.scan_dot as usize / 2);
|
|
let line_y = self.pos.line_y + 16;
|
|
|
|
if attr.x > 0
|
|
&& line_y >= attr.y
|
|
&& line_y < (attr.y + obj_height)
|
|
&& !self.obj_buffer.is_full()
|
|
{
|
|
self.obj_buffer.add(attr);
|
|
}
|
|
}
|
|
self.scan_dot += 1;
|
|
}
|
|
|
|
fn draw(&mut self) {
|
|
use FetcherState::*;
|
|
|
|
let mut obj_attr = &mut None;
|
|
|
|
for maybe_attr in &mut self.obj_buffer.inner {
|
|
match maybe_attr {
|
|
Some(attr) if self.ctrl.obj_enabled() => {
|
|
if attr.x <= (self.x_pos + 8) {
|
|
self.fetch.back.reset();
|
|
self.fetch.back.enabled = false;
|
|
self.fifo.pause();
|
|
|
|
obj_attr = maybe_attr;
|
|
break;
|
|
}
|
|
}
|
|
_ => break,
|
|
}
|
|
}
|
|
|
|
if let Some(attr) = obj_attr {
|
|
match self.fetch.obj.state {
|
|
TileNumberA => self.fetch.obj.state = TileNumberB,
|
|
TileNumberB => {
|
|
self.fetch.obj.tile.with_id(attr.tile_index);
|
|
self.fetch.obj.state = TileLowA;
|
|
}
|
|
TileLowA => self.fetch.obj.state = TileLowB,
|
|
TileLowB => {
|
|
let obj_size = self.ctrl.obj_size();
|
|
|
|
let addr = PixelFetcher::get_obj_addr(attr, &self.pos, obj_size);
|
|
|
|
let byte = self.read_byte(addr);
|
|
self.fetch.obj.tile.with_low(byte);
|
|
|
|
self.fetch.obj.state = TileHighA;
|
|
}
|
|
TileHighA => self.fetch.obj.state = TileHighB,
|
|
TileHighB => {
|
|
let obj_size = self.ctrl.obj_size();
|
|
|
|
let addr = PixelFetcher::get_obj_addr(attr, &self.pos, obj_size);
|
|
|
|
let byte = self.read_byte(addr + 1);
|
|
self.fetch.obj.tile.with_high(byte);
|
|
|
|
self.fetch.obj.state = ToFifoA;
|
|
}
|
|
ToFifoA => {
|
|
// Load into Fifo
|
|
let (high, low) = self
|
|
.fetch
|
|
.obj
|
|
.tile
|
|
.bytes()
|
|
.expect("Tile high & low bytes are present");
|
|
|
|
let tbpp = Pixels::from_bytes(high, low);
|
|
|
|
let palette_kind = attr.flags.palette();
|
|
let x_flip = attr.flags.x_flip();
|
|
|
|
let pixel_count = (attr.x - self.x_pos) as usize;
|
|
let start = self.fifo.obj.len();
|
|
|
|
for i in start..pixel_count {
|
|
let x = if x_flip { 7 - i } else { i };
|
|
|
|
let priority = attr.flags.priority();
|
|
let shade_id = tbpp.shade_id(x);
|
|
|
|
let fifo_info = ObjPixelProperty {
|
|
shade_id,
|
|
palette_kind,
|
|
priority,
|
|
};
|
|
|
|
self.fifo.obj.push_back(fifo_info);
|
|
}
|
|
|
|
self.fetch.back.enabled = true;
|
|
self.fifo.resume();
|
|
let _ = std::mem::take(obj_attr);
|
|
|
|
self.fetch.obj.state = ToFifoB;
|
|
}
|
|
ToFifoB => self.fetch.obj.reset(),
|
|
}
|
|
}
|
|
|
|
if self.fetch.back.enabled {
|
|
match self.fetch.back.state {
|
|
TileNumberA => self.fetch.back.state = TileNumberB,
|
|
TileNumberB => {
|
|
// Are we rendering the Window currently?
|
|
self.fetch.back.draw_window = self.win_stat.enabled;
|
|
|
|
let addr =
|
|
self.fetch
|
|
.back
|
|
.tile_id_addr(&self.ctrl, &self.pos, self.fetch.x_pos);
|
|
|
|
let id = self.read_byte(addr);
|
|
self.fetch.back.tile.with_id(id);
|
|
|
|
self.fetch.back.state = TileLowA;
|
|
}
|
|
TileLowA => self.fetch.back.state = TileLowB,
|
|
TileLowB => {
|
|
let id = self.fetch.back.tile.id.expect("Tile ID present");
|
|
|
|
let addr = self.fetch.back.tile_addr(&self.ctrl, &self.pos, id);
|
|
let byte = self.read_byte(addr);
|
|
self.fetch.back.tile.with_low(byte);
|
|
|
|
self.fetch.back.state = TileHighA;
|
|
}
|
|
TileHighA => self.fetch.back.state = TileHighB,
|
|
TileHighB => {
|
|
let id = self.fetch.back.tile.id.expect("Tile ID present");
|
|
|
|
let addr = self.fetch.back.tile_addr(&self.ctrl, &self.pos, id);
|
|
let byte = self.read_byte(addr + 1);
|
|
self.fetch.back.tile.with_high(byte);
|
|
|
|
if self.fetch.back.tile_high_reset {
|
|
self.fetch.back.reset();
|
|
self.fetch.back.tile_high_reset = false;
|
|
} else {
|
|
self.fetch.back.state = ToFifoA;
|
|
}
|
|
}
|
|
ToFifoA => {
|
|
if self.fetch.send_to_fifo(&mut self.fifo).is_ok() {
|
|
self.fetch.x_pos += 1;
|
|
self.fetch.back.state = ToFifoB;
|
|
}
|
|
}
|
|
ToFifoB => self.fetch.back.reset(),
|
|
}
|
|
}
|
|
|
|
if self.fifo.is_enabled() {
|
|
if self.x_pos == 0 && self.scanline_start {
|
|
self.to_discard = self.pos.scroll_x % 8;
|
|
self.scanline_start = false;
|
|
}
|
|
|
|
if !self.win_stat.enabled && self.to_discard > 0 && !self.fifo.back.is_empty() {
|
|
let _ = self.fifo.back.pop_front();
|
|
self.to_discard -= 1;
|
|
|
|
// Delay the PPU by one cycle
|
|
return;
|
|
}
|
|
|
|
// Handle Background Pixel and Sprite FIFO
|
|
if let Some(rgba) = self.clock_fifo().map(GrayShade::into_rgba) {
|
|
let y = self.pos.line_y as usize;
|
|
let x = self.x_pos as usize;
|
|
|
|
let i = (GB_WIDTH * 4) * y + (x * 4);
|
|
self.frame_buf[i..(i + rgba.len())].copy_from_slice(&rgba);
|
|
|
|
self.x_pos += 1;
|
|
}
|
|
|
|
if self.ctrl.window_enabled()
|
|
&& !self.win_stat.enabled
|
|
&& self.win_stat.coincidence
|
|
&& self.x_pos as i16 >= self.pos.window_x as i16 - 7
|
|
{
|
|
self.win_stat.enabled = true;
|
|
self.fetch.back.reset();
|
|
self.fetch.x_pos = 0;
|
|
self.fifo.back.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
self.pos.line_y = 0;
|
|
self.stat.set_mode(PpuMode::HBlank);
|
|
|
|
let mut blank = WHITE.repeat(self.frame_buf.len() / 4);
|
|
self.frame_buf.swap_with_slice(&mut blank);
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) fn frame_buf(&self) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] {
|
|
&self.frame_buf
|
|
}
|
|
|
|
fn clock_fifo(&mut self) -> Option<GrayShade> {
|
|
use RenderPriority::*;
|
|
|
|
self.fifo
|
|
.back
|
|
.pop_front()
|
|
.map(|bg| match self.fifo.obj.pop_front() {
|
|
Some(obj) => match obj.priority {
|
|
_ if obj.shade_id == 0 => self.bg_pixel(bg),
|
|
BackgroundAndWindow if bg.shade_id != 0 => self.bg_pixel(bg),
|
|
_ => self.obj_pixel(obj),
|
|
},
|
|
None => self.bg_pixel(bg),
|
|
})
|
|
}
|
|
|
|
fn obj_pixel(&self, obj: ObjPixelProperty) -> GrayShade {
|
|
use ObjectPaletteKind::*;
|
|
assert!(obj.shade_id != 0);
|
|
|
|
let p0 = &self.monochrome.obj_palette_0;
|
|
let p1 = &self.monochrome.obj_palette_1;
|
|
|
|
match obj.palette_kind {
|
|
Zero => p0.shade(obj.shade_id).expect("Object shade id is non-zero"),
|
|
One => p1.shade(obj.shade_id).expect("Object shade id is non-zero"),
|
|
}
|
|
}
|
|
|
|
fn bg_pixel(&self, bg: BgPixelProperty) -> GrayShade {
|
|
let bg_palette = &self.monochrome.bg_palette;
|
|
|
|
if self.ctrl.bg_win_enabled() {
|
|
bg_palette.shade(bg.shade_id)
|
|
} else {
|
|
bg_palette.shade(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Ppu {
|
|
fn default() -> Self {
|
|
Self {
|
|
vram: Box::new([0u8; VRAM_SIZE]),
|
|
dot: Default::default(),
|
|
frame_buf: Box::new([0; GB_WIDTH * GB_HEIGHT * 4]),
|
|
int: Default::default(),
|
|
ctrl: Default::default(),
|
|
monochrome: Default::default(),
|
|
pos: Default::default(),
|
|
stat: Default::default(),
|
|
oam: Default::default(),
|
|
scan_dot: Default::default(),
|
|
fetch: Default::default(),
|
|
fifo: Default::default(),
|
|
obj_buffer: Default::default(),
|
|
win_stat: Default::default(),
|
|
dma: Default::default(),
|
|
x_pos: 0,
|
|
scanline_start: true,
|
|
to_discard: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct Interrupt {
|
|
_vblank: bool,
|
|
_lcd_stat: bool,
|
|
}
|
|
|
|
impl Interrupt {
|
|
pub(crate) fn vblank(&self) -> bool {
|
|
self._vblank
|
|
}
|
|
|
|
pub(crate) fn set_vblank(&mut self, enabled: bool) {
|
|
self._vblank = enabled;
|
|
}
|
|
|
|
pub(crate) fn lcd_stat(&self) -> bool {
|
|
self._lcd_stat
|
|
}
|
|
|
|
pub(crate) fn set_lcd_stat(&mut self, enabled: bool) {
|
|
self._lcd_stat = enabled;
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct ScreenPosition {
|
|
/// 0xFF42 | SCY - Scroll Y
|
|
pub(crate) scroll_y: u8,
|
|
/// 0xFF43 | SCX - Scroll X
|
|
pub(crate) scroll_x: u8,
|
|
/// 0xFF44 | LY - LCD Y Coordinate
|
|
pub(crate) line_y: u8,
|
|
/// 0xFF45 | LYC - LY Compare
|
|
pub(crate) ly_compare: u8,
|
|
/// 0xFF4A | WY - Window Y Position
|
|
pub(crate) window_y: u8,
|
|
/// 0xFF4B | WX - Window X Position
|
|
pub(crate) window_x: u8,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct Monochrome {
|
|
/// 0xFF47 | BGP - Background Palette Data
|
|
pub(crate) bg_palette: BackgroundPalette,
|
|
/// 0xFF48 | OBP0 - Object Palette 0 Data
|
|
pub(crate) obj_palette_0: ObjectPalette,
|
|
/// 0xFF49 | OBP1 - Object Palette 1 Data
|
|
pub(crate) obj_palette_1: ObjectPalette,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct ObjectAttributeTable {
|
|
buf: Box<[u8; OAM_SIZE]>,
|
|
}
|
|
|
|
impl BusIo for ObjectAttributeTable {
|
|
fn read_byte(&self, addr: u16) -> u8 {
|
|
let index = (addr - 0xFE00) as usize;
|
|
self.buf[index]
|
|
}
|
|
|
|
fn write_byte(&mut self, addr: u16, byte: u8) {
|
|
let index = (addr - 0xFE00) as usize;
|
|
self.buf[index] = byte;
|
|
}
|
|
}
|
|
|
|
impl ObjectAttributeTable {
|
|
fn attribute(&self, index: usize) -> ObjectAttribute {
|
|
let start = index * 4;
|
|
|
|
let slice: &[u8; 4] = self.buf[start..(start + 4)]
|
|
.try_into()
|
|
.expect("TryInto trait called on a &[u8; 4]");
|
|
|
|
slice.into()
|
|
}
|
|
}
|
|
|
|
impl Default for ObjectAttributeTable {
|
|
fn default() -> Self {
|
|
Self {
|
|
buf: Box::new([0; OAM_SIZE]),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
struct ObjectAttribute {
|
|
y: u8,
|
|
x: u8,
|
|
tile_index: u8,
|
|
flags: ObjectFlags,
|
|
}
|
|
|
|
impl From<[u8; 4]> for ObjectAttribute {
|
|
fn from(bytes: [u8; 4]) -> Self {
|
|
Self {
|
|
y: bytes[0],
|
|
x: bytes[1],
|
|
tile_index: bytes[2],
|
|
flags: bytes[3].into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a [u8; 4]> for ObjectAttribute {
|
|
fn from(bytes: &'a [u8; 4]) -> Self {
|
|
Self {
|
|
y: bytes[0],
|
|
x: bytes[1],
|
|
tile_index: bytes[2],
|
|
flags: bytes[3].into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ObjectBuffer {
|
|
inner: [Option<ObjectAttribute>; OBJECT_LIMIT],
|
|
len: usize,
|
|
}
|
|
|
|
impl ObjectBuffer {
|
|
fn is_full(&self) -> bool {
|
|
self.len == OBJECT_LIMIT
|
|
}
|
|
|
|
fn clear(&mut self) {
|
|
self.inner = [Default::default(); 10];
|
|
self.len = 0;
|
|
}
|
|
|
|
fn add(&mut self, attr: ObjectAttribute) {
|
|
self.inner[self.len] = Some(attr);
|
|
self.len += 1;
|
|
}
|
|
}
|
|
|
|
impl Default for ObjectBuffer {
|
|
fn default() -> Self {
|
|
Self {
|
|
inner: [Default::default(); OBJECT_LIMIT],
|
|
len: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct PixelFetcher {
|
|
x_pos: u8,
|
|
back: BackgroundFetcher,
|
|
obj: ObjectFetcher,
|
|
}
|
|
|
|
impl PixelFetcher {
|
|
fn hblank_reset(&mut self) {
|
|
self.back.hblank_reset();
|
|
self.obj.hblank_reset();
|
|
self.x_pos = 0;
|
|
}
|
|
|
|
fn send_to_fifo(&self, fifo: &mut PixelFifo) -> Result<(), ()> {
|
|
if !fifo.back.is_empty() {
|
|
return Err(());
|
|
}
|
|
|
|
let (high, low) = self
|
|
.back
|
|
.tile
|
|
.bytes()
|
|
.expect("Tile high & low bytes are present");
|
|
|
|
let tbpp = Pixels::from_bytes(high, low);
|
|
|
|
for x in 0..Pixels::PIXEL_COUNT {
|
|
let shade_id = tbpp.shade_id(x);
|
|
|
|
let fifo_info = BgPixelProperty { shade_id };
|
|
fifo.back.push_back(fifo_info);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_obj_addr(attr: &ObjectAttribute, pos: &ScreenPosition, size: ObjectSize) -> u16 {
|
|
let line_y = pos.line_y;
|
|
|
|
// TODO: Why is the offset 14 and 30 respectively?
|
|
let (id, flip_offset) = match size {
|
|
ObjectSize::Eight => (attr.tile_index, 14),
|
|
ObjectSize::Sixteen => (attr.tile_index & !0x01, 30),
|
|
};
|
|
|
|
let offset = 2 * (line_y - (attr.y - 16));
|
|
|
|
let final_offset = if attr.flags.y_flip() {
|
|
flip_offset - offset
|
|
} else {
|
|
offset
|
|
};
|
|
|
|
0x8000 + (id as u16 * 16) + final_offset as u16
|
|
}
|
|
}
|
|
|
|
trait Fetcher {
|
|
fn reset(&mut self);
|
|
fn hblank_reset(&mut self);
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct BackgroundFetcher {
|
|
state: FetcherState,
|
|
tile: TileBuilder,
|
|
wl_count: u8,
|
|
draw_window: bool,
|
|
enabled: bool,
|
|
tile_high_reset: bool,
|
|
}
|
|
|
|
impl BackgroundFetcher {
|
|
fn tile_id_addr(&self, control: &LCDControl, pos: &ScreenPosition, x_pos: u8) -> u16 {
|
|
let line_y = pos.line_y;
|
|
let scroll_y = pos.scroll_y;
|
|
let scroll_x = pos.scroll_x;
|
|
let is_window = self.draw_window;
|
|
|
|
// Determine which tile map is being used
|
|
let tile_map = if is_window {
|
|
control.win_tile_map_addr()
|
|
} else {
|
|
control.bg_tile_map_addr()
|
|
};
|
|
let tile_map_addr = tile_map.into_address();
|
|
|
|
// Both Offsets are used to offset the tile map address we found above
|
|
// Offsets are ANDed wih 0x3FF so that we stay in bounds of tile map memory
|
|
|
|
let scx_offset = if is_window { 0 } else { scroll_x / 8 };
|
|
let y_offset = if is_window {
|
|
self.wl_count as u16 / 8
|
|
} else {
|
|
((line_y as u16 + scroll_y as u16) & 0xFF) / 8
|
|
};
|
|
|
|
let x_offset = (scx_offset + x_pos) & 0x1F;
|
|
let offset = (32 * y_offset) + (x_offset as u16);
|
|
|
|
tile_map_addr + (offset & 0x3FF)
|
|
}
|
|
|
|
fn tile_addr(&mut self, control: &LCDControl, pos: &ScreenPosition, id: u8) -> u16 {
|
|
let line_y = pos.line_y;
|
|
let scroll_y = pos.scroll_y;
|
|
|
|
let tile_data_addr = match control.tile_data_addr() {
|
|
TileDataAddress::X8800 => 0x9000u16.wrapping_add((id as i8 as i16 * 16) as u16),
|
|
TileDataAddress::X8000 => 0x8000 + (id as u16 * 16),
|
|
};
|
|
|
|
let offset = if self.draw_window {
|
|
self.wl_count as u16 % 8
|
|
} else {
|
|
(line_y as u16 + scroll_y as u16) % 8
|
|
};
|
|
|
|
tile_data_addr + (offset * 2)
|
|
}
|
|
}
|
|
|
|
impl Fetcher for BackgroundFetcher {
|
|
fn reset(&mut self) {
|
|
self.state = Default::default();
|
|
self.tile = Default::default();
|
|
}
|
|
|
|
fn hblank_reset(&mut self) {
|
|
self.reset();
|
|
|
|
self.draw_window = false;
|
|
|
|
self.enabled = true;
|
|
}
|
|
}
|
|
|
|
impl Default for BackgroundFetcher {
|
|
fn default() -> Self {
|
|
Self {
|
|
state: Default::default(),
|
|
tile: Default::default(),
|
|
draw_window: Default::default(),
|
|
wl_count: Default::default(),
|
|
enabled: true,
|
|
tile_high_reset: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ObjectFetcher {
|
|
state: FetcherState,
|
|
tile: TileBuilder,
|
|
}
|
|
|
|
impl Default for ObjectFetcher {
|
|
fn default() -> Self {
|
|
Self {
|
|
state: Default::default(),
|
|
tile: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Fetcher for ObjectFetcher {
|
|
fn reset(&mut self) {
|
|
self.state = Default::default();
|
|
self.tile = Default::default();
|
|
}
|
|
|
|
fn hblank_reset(&mut self) {
|
|
self.reset()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum FetcherState {
|
|
TileNumberA,
|
|
TileNumberB,
|
|
TileLowA,
|
|
TileLowB,
|
|
TileHighA,
|
|
TileHighB,
|
|
ToFifoA,
|
|
ToFifoB,
|
|
}
|
|
|
|
impl Default for FetcherState {
|
|
fn default() -> Self {
|
|
Self::TileNumberA
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct BgPixelProperty {
|
|
shade_id: u8,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct ObjPixelProperty {
|
|
shade_id: u8,
|
|
palette_kind: ObjectPaletteKind,
|
|
priority: RenderPriority,
|
|
}
|
|
|
|
// FIXME: Fifo Registers have a known size. Are heap allocations
|
|
// really necessary here?
|
|
#[derive(Debug)]
|
|
struct PixelFifo {
|
|
back: VecDeque<BgPixelProperty>,
|
|
obj: VecDeque<ObjPixelProperty>,
|
|
enabled: bool,
|
|
}
|
|
|
|
impl PixelFifo {
|
|
fn is_enabled(&self) -> bool {
|
|
self.enabled
|
|
}
|
|
|
|
fn pause(&mut self) {
|
|
self.enabled = false;
|
|
}
|
|
|
|
fn resume(&mut self) {
|
|
self.enabled = true;
|
|
}
|
|
}
|
|
|
|
impl Default for PixelFifo {
|
|
fn default() -> Self {
|
|
Self {
|
|
back: VecDeque::with_capacity(8),
|
|
obj: VecDeque::with_capacity(8),
|
|
enabled: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct TileBuilder {
|
|
id: Option<u8>,
|
|
low: Option<u8>,
|
|
high: Option<u8>,
|
|
}
|
|
|
|
impl TileBuilder {
|
|
fn with_id(&mut self, id: u8) {
|
|
self.id = Some(id);
|
|
}
|
|
|
|
fn with_low(&mut self, data: u8) {
|
|
self.low = Some(data);
|
|
}
|
|
|
|
fn with_high(&mut self, data: u8) {
|
|
self.high = Some(data);
|
|
}
|
|
|
|
fn bytes(&self) -> Option<(u8, u8)> {
|
|
self.high.zip(self.low)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct WindowStatus {
|
|
/// This will be true if WY == LY at any point in the frame thus far
|
|
coincidence: bool,
|
|
/// This will be true if the conditions which tell the PPU to start
|
|
/// drawing from the window tile map is true
|
|
enabled: bool,
|
|
}
|