Newer
Older
//! PPU (Picture Processing Unit) functions and structures.
use crate::{
gb::{GameBoyConfig, GameBoyMode},
warnln,
};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
pub const VRAM_SIZE_DMG: usize = 8192;
pub const VRAM_SIZE_CGB: usize = 16384;
pub const VRAM_SIZE: usize = VRAM_SIZE_CGB;
pub const OAM_SIZE: usize = 260;
pub const RGB_SIZE: usize = 3;
pub const RGB888_SIZE: usize = 3;
pub const XRGB8888_SIZE: usize = 4;
pub const RGB1555_SIZE: usize = 2;
pub const TILE_WIDTH: usize = 8;
pub const TILE_HEIGHT: usize = 8;
pub const TILE_WIDTH_I: usize = 7;
pub const TILE_HEIGHT_I: usize = 7;
pub const TILE_DOUBLE_HEIGHT: usize = 16;
pub const TILE_COUNT_DMG: usize = 384;
pub const TILE_COUNT_CGB: usize = 768;
/// The number of tiles that can be store in Game Boy's
/// VRAM memory according to specifications.
pub const TILE_COUNT: usize = TILE_COUNT_CGB;
/// The number of objects/sprites that can be handled at
/// the same time by the Game Boy.
pub const OBJ_COUNT: usize = 40;
/// The width of the Game Boy screen in pixels.
pub const DISPLAY_WIDTH: usize = 160;
/// The height of the Game Boy screen in pixels.
/// The size in pixels of the display.
pub const DISPLAY_SIZE: usize = DISPLAY_WIDTH * DISPLAY_HEIGHT;
/// The size to be used by the buffer of colors
/// for the Game Boy screen the values there should
/// range from 0 to 3.
pub const COLOR_BUFFER_SIZE: usize = DISPLAY_SIZE;
/// The size of the RGB frame buffer in bytes.
pub const FRAME_BUFFER_SIZE: usize = DISPLAY_SIZE * RGB_SIZE;
/// The size of the RGB888 frame buffer in bytes.
pub const FRAME_BUFFER_RGB888_SIZE: usize = DISPLAY_SIZE * RGB888_SIZE;
/// The size of the XRGB8888 frame buffer in bytes.
pub const FRAME_BUFFER_XRGB8888_SIZE: usize = DISPLAY_SIZE * XRGB8888_SIZE;
/// The size of the RGB1555 frame buffer in bytes.
pub const FRAME_BUFFER_RGB1555_SIZE: usize = DISPLAY_SIZE * RGB1555_SIZE;
/// The size of the RGB565 frame buffer in bytes.
pub const FRAME_BUFFER_RGB565_SIZE: usize = DISPLAY_SIZE * RGB565_SIZE;
/// The base colors to be used to populate the
/// custom palettes of the Game Boy.
pub const PALETTE_COLORS: Palette = [[255, 255, 255], [192, 192, 192], [96, 96, 96], [0, 0, 0]];
pub const DEFAULT_TILE_ATTR: TileData = TileData {
palette: 0,
vram_bank: 0,
xflip: false,
yflip: false,
priority: false,
};
/// Defines the Game Boy pixel type as a buffer
/// with the size of RGB (3 bytes).
/// Defines a transparent Game Boy pixel type as a buffer
/// with the size of RGBA (4 bytes).
pub type PixelAlpha = [u8; RGBA_SIZE];
/// Defines a pixel with 5 bits per channel plus a padding
/// bit at the beginning.
pub type PixelRgb1555 = [u8; RGB1555_SIZE];
/// Defines a pixel with 5 bits per channel except for the
/// green channel which uses 6 bits.
pub type PixelRgb565 = [u8; RGB565_SIZE];
/// Defines a type that represents a color palette
/// within the Game Boy context.
pub type Palette = [Pixel; PALETTE_SIZE];
/// Defines a type that represents a color palette
/// with alpha within the Game Boy context.
pub type PaletteAlpha = [PixelAlpha; PALETTE_SIZE];
/// Represents a palette with the metadata that is
/// associated with it.
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Clone, PartialEq, Eq)]
pub struct PaletteInfo {
name: String,
}
impl PaletteInfo {
pub fn new(name: &str, colors: Palette) -> Self {
Self {
name: String::from(name),
}
}
pub fn name(&self) -> &String {
&self.name
}
pub fn colors(&self) -> &Palette {
&self.colors
}
}
/// Represents a tile within the Game Boy context,
/// should contain the pixel buffer of the tile.
/// The tiles are always 8x8 pixels in size.
#[cfg_attr(feature = "wasm", wasm_bindgen)]
/// The buffer for the tile, should contain a byte
/// per each pixel of the tile with values ranging
/// from 0 to 3 (4 colors).
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl Tile {
pub fn get(&self, x: usize, y: usize) -> u8 {
pub fn get_flipped(&self, x: usize, y: usize, xflip: bool, yflip: bool) -> u8 {
let x: usize = if xflip { TILE_WIDTH_I - x } else { x };
let y = if yflip { TILE_HEIGHT_I - y } else { y };
self.buffer[y * TILE_WIDTH + x]
}
pub fn set(&mut self, x: usize, y: usize, value: u8) {
self.buffer[y * TILE_WIDTH + x] = value;
}
pub fn buffer(&self) -> Vec<u8> {
self.buffer.to_vec()
}
impl Tile {
pub fn get_row(&self, y: usize) -> &[u8] {
&self.buffer[y * TILE_WIDTH..(y + 1) * TILE_WIDTH]
}
}
impl Tile {
pub fn palette_buffer(&self, palette: Palette) -> Vec<u8> {
self.buffer
.iter()
.flat_map(|p| palette[*p as usize])
.collect()
}
}
impl Display for Tile {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut buffer = String::new();
for y in 0..8 {
for x in 0..8 {
buffer.push_str(format!("{}", self.get(x, y)).as_str());
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub struct ObjectData {
x: i16,
y: i16,
tile: u8,
palette_cgb: u8,
tile_bank: u8,
palette: u8,
xflip: bool,
yflip: bool,
impl ObjectData {
pub fn new() -> Self {
Self {
x: 0,
y: 0,
tile: 0,
palette_cgb: 0,
tile_bank: 0,
palette: 0,
xflip: false,
yflip: false,
bg_over: false,
index: 0,
}
}
}
impl Default for ObjectData {
fn default() -> Self {
Self::new()
}
}
impl Display for ObjectData {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"Index: {}, X: {}, Y: {}, Tile: {}",
self.index, self.x, self.y, self.tile
)
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct TileData {
palette: u8,
vram_bank: u8,
xflip: bool,
yflip: bool,
priority: bool,
}
impl TileData {
pub fn new() -> Self {
Self {
palette: 0,
vram_bank: 0,
xflip: false,
yflip: false,
priority: false,
}
}
}
impl Default for TileData {
fn default() -> Self {
Self::new()
}
}
impl Display for TileData {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"Palette: {}, VRAM Bank: {}, X Flip: {}, Y Flip: {}",
self.palette, self.vram_bank, self.xflip, self.yflip
)
}
}
pub scy: u8,
pub scx: u8,
pub wy: u8,
pub wx: u8,
/// Represents the Game Boy PPU (Pixel Processing Unit) and controls
/// all of the logic behind the graphics processing and presentation.
/// Should store both the VRAM and HRAM together with the internal
/// graphic related registers.
/// Outputs the screen as a RGB 8 bit frame buffer.
///
/// # Basic usage
/// ```rust
/// The color buffer that is going to store the colors
/// (from 0 to 3) for all the pixels in the screen.
pub color_buffer: Box<[u8; COLOR_BUFFER_SIZE]>,
/// The 8 bit based RGB frame buffer with the
/// processed set of pixels ready to be displayed on screen.
pub frame_buffer: Box<[u8; FRAME_BUFFER_SIZE]>,
/// The buffer that will control the background to OAM
/// priority, allowing the background to be drawn over
/// the sprites/objects if necessary.
priority_buffer: Box<[bool; COLOR_BUFFER_SIZE]>,
/// Video dedicated memory (VRAM) where both the tiles and
/// the sprites/objects are going to be stored.
/// High RAM memory that should provide extra speed for regular
/// operations.
/// OAM RAM (Sprite Attribute Table ) used for the storage of the
/// sprite attributes for each of the 40 sprites of the Game Boy.
/// The VRAM bank to be used in the read and write operation of
/// the 0x8000-0x9FFF memory range (CGB only).
vram_bank: u8,
/// The offset to be used in the read and write operation of
/// the VRAM, this value should be consistent with the VRAM bank
/// that is currently selected (CGB only).
vram_offset: u16,
/// The current set of processed tiles that are store in the
/// PPU related structures.
/// The meta information about the sprites/objects that are going
/// to be drawn to the screen,
obj_data: [ObjectData; OBJ_COUNT],
/// The base colors that are going to be used in the registration
/// of the concrete palettes, this value basically controls the
/// colors that are going to be shown for each of the four base
/// values - 0x00, 0x01, 0x02, and 0x03.
palette_colors: Palette,
/// The palette of colors that is currently loaded in Game Boy
/// and used for background (tiles) and window.
palette_bg: Palette,
/// The palette that is going to be used for sprites/objects #0.
/// The palette that is going to be used for sprites/objects #1.
/// The complete set of background palettes that are going to be
/// used in CGB emulation to provide the full set of colors (CGB only).
palettes_color_bg: [Palette; 8],
/// The complete set of object/sprite palettes that are going to be
/// used in CGB emulation to provide the full set of colors (CGB only).
palettes_color_obj: [Palette; 8],
/// The complete set of palettes in binary data so that they can
/// be re-read if required by the system.
/// The raw binary information (64 bytes) for the color palettes,
/// contains binary information for both the background and
/// the objects palettes (CGB only).
palettes_color: [[u8; 64]; 2],
/// The complete list of attributes for the first background
/// map that is located in 0x9800-0x9BFF (CGB only).
bg_map_attrs_0: [TileData; 1024],
/// The complete list of attributes for the second background
/// map that is located in 0x9C00-0x9FFF (CGB only).
bg_map_attrs_1: [TileData; 1024],
/// The flag that controls if the object/sprite priority
/// if set means that the priority mode to be used is the
/// X coordinate otherwise the normal CGB OAM memory mode
/// mode is used, the value of this flag is controlled by
/// the OPRI register (CGB only)
obj_priority: bool,
/// The scroll Y register that controls the Y offset
/// of the background.
scy: u8,
/// The scroll X register that controls the X offset
/// of the background.
scx: u8,
/// The top most Y coordinate of the window,
/// going to be used while drawing the window.
wy: u8,
/// The top most X coordinate of the window plus 7,
/// going to be used while drawing the window.
wx: u8,
/// The current scan line in processing, should
/// range between 0 (0x00) and 153 (0x99), representing
/// the 154 lines plus 10 extra V-Blank lines.
/// The line compare register that is going to be used
/// in the STATE and associated interrupts.
/// The current execution mode of the PPU, should change
/// between states over the drawing of a frame.
mode: PpuMode,
/// Internal clock counter used to control the time in ticks
/// spent in each of the PPU modes.
mode_clock: u16,
/// Controls if the background is going to be drawn to screen.
/// In CGB mode this flag controls the master priority instead
/// enabling or disabling complex priority rules.
/// Controls if the sprites/objects are going to be drawn to screen.
switch_obj: bool,
/// Defines the size in pixels of the object (false=8x8, true=8x16).
obj_size: bool,
/// Controls the map that is going to be drawn to screen, the
/// offset in VRAM will be adjusted according to this
/// (false=0x9800, true=0x9c000).
/// If the background tile set is active meaning that the
/// negative based indexes are going to be used.
/// Controls if the window is meant to be drawn.
/// Controls the offset of the map that is going to be drawn
/// for the window section of the screen.
/// Flag that controls if the LCD screen is ON and displaying
/// content.
// Internal window counter value used to control the lines that
// were effectively rendered as part of the window tile drawing process.
// A line is only considered rendered when the WX and WY registers
// are within the valid screen range and the window switch register
// is valid.
/// If the auto increment of the background color palette is enabled
/// so that the next address is going to be set on every write.
auto_increment_bg: bool,
/// The current address in usage for the background color palettes.
palette_address_bg: u8,
/// If the auto increment of the object/sprite color palette is enabled
/// so that the next address is going to be set on every write.
auto_increment_obj: bool,
/// The current address in usage for the object/sprite color palettes.
palette_address_obj: u8,
/// Flag that controls if the frame currently in rendering is the
/// first one, preventing actions.
first_frame: bool,
/// Almost unique identifier of the frame that can be used to debug
/// and uniquely identify the frame that is currently ind drawing,
/// the identifier wraps on the u16 edges.
frame_index: u16,
stat_hblank: bool,
stat_vblank: bool,
stat_oam: bool,
stat_lyc: bool,
/// Boolean value set when the V-Blank interrupt should be handled
/// by the next CPU clock operation.
/// Boolean value when the LCD STAT interrupt should be handled by
/// the next CPU clock operation.
/// Flag that controls if the DMG compatibility mode is
/// enabled meaning that some of the PPU decisions will
/// be made differently to address this special situation
/// (CGB only).
dmg_compat: bool,
/// The current running mode of the emulator, this
/// may affect many aspects of the emulation.
/// The pointer to the parent configuration of the running
/// Game Boy emulator, that can be used to control the behaviour
/// of Game Boy emulation.
#[cfg_attr(feature = "wasm", wasm_bindgen)]
VBlank = 1,
OamRead = 2,
VramRead = 3,
pub fn new(mode: GameBoyMode, gbc: Rc<RefCell<GameBoyConfig>>) -> Self {
color_buffer: Box::new([0u8; COLOR_BUFFER_SIZE]),
frame_buffer: Box::new([0u8; FRAME_BUFFER_SIZE]),
priority_buffer: Box::new([false; COLOR_BUFFER_SIZE]),
vram_bank: 0x0,
vram_offset: 0x0000,
tiles: [Tile { buffer: [0u8; 64] }; TILE_COUNT],
obj_data: [ObjectData::default(); OBJ_COUNT],
palette_bg: [[0u8; RGB_SIZE]; PALETTE_SIZE],
palette_obj_0: [[0u8; RGB_SIZE]; PALETTE_SIZE],
palette_obj_1: [[0u8; RGB_SIZE]; PALETTE_SIZE],
palettes_color_bg: [[[0u8; RGB_SIZE]; PALETTE_SIZE]; 8],
palettes_color_obj: [[[0u8; RGB_SIZE]; PALETTE_SIZE]; 8],
palettes_color: [[0u8; 64]; 2],
bg_map_attrs_0: [TileData::default(); 1024],
bg_map_attrs_1: [TileData::default(); 1024],
mode: PpuMode::OamRead,
mode_clock: 0,
auto_increment_bg: false,
palette_address_bg: 0x0,
auto_increment_obj: false,
palette_address_obj: 0x0,
stat_hblank: false,
stat_vblank: false,
stat_oam: false,
stat_lyc: false,
self.color_buffer = Box::new([0u8; COLOR_BUFFER_SIZE]);
self.frame_buffer = Box::new([0u8; FRAME_BUFFER_SIZE]);
self.priority_buffer = Box::new([false; COLOR_BUFFER_SIZE]);
self.vram = [0u8; VRAM_SIZE_CGB];
self.vram_bank = 0x0;
self.vram_offset = 0x0000;
self.tiles = [Tile { buffer: [0u8; 64] }; TILE_COUNT];
self.obj_data = [ObjectData::default(); OBJ_COUNT];
self.palette_bg = [[0u8; RGB_SIZE]; PALETTE_SIZE];
self.palette_obj_0 = [[0u8; RGB_SIZE]; PALETTE_SIZE];
self.palette_obj_1 = [[0u8; RGB_SIZE]; PALETTE_SIZE];
self.palettes_color_bg = [[[0u8; RGB_SIZE]; PALETTE_SIZE]; 8];
self.palettes_color_obj = [[[0u8; RGB_SIZE]; PALETTE_SIZE]; 8];
self.palettes_color = [[0u8; 64]; 2];
self.bg_map_attrs_1 = [TileData::default(); 1024];
self.obj_priority = false;
self.scy = 0x0;
self.scx = 0x0;
self.ly = 0x0;
self.lyc = 0x0;
self.mode = PpuMode::OamRead;
self.mode_clock = 0;
self.switch_bg = false;
self.switch_obj = false;
self.obj_size = false;
self.bg_map = false;
self.bg_tile = false;
self.switch_window = false;
self.window_map = false;
self.switch_lcd = false;
self.window_counter = 0;
self.auto_increment_bg = false;
self.palette_address_bg = 0x0;
self.auto_increment_obj = false;
self.palette_address_obj = 0x0;
self.stat_hblank = false;
self.stat_vblank = false;
self.stat_oam = false;
self.stat_lyc = false;
self.int_vblank = false;
pub fn clock(&mut self, cycles: u16) {
// in case the LCD is currently off then we skip the current
// clock operation the PPU should not work
if !self.switch_lcd {
return;
}
// increments the current mode clock by the provided amount
// of CPU cycles (probably coming from a previous CPU clock)
match self.mode {
PpuMode::OamRead => {
}
}
PpuMode::VramRead => {
if self.mode_clock >= 172 {
// increments the window counter making sure that the
// valid is only incremented when both the WX and WY
// registers make sense (are within range), the window
// switch is on and the line in drawing is above WY
&& self.wx as i16 - 7 < DISPLAY_WIDTH as i16
{
self.window_counter += 1;
}
// increments the register that holds the
// information about the current line in drawing
// in case we've reached the end of the
// screen we're now entering the V-Blank
self.mode = PpuMode::VBlank;
} else {
self.mode = PpuMode::OamRead;
}
self.mode_clock -= 204;
self.update_stat()
}
}
PpuMode::VBlank => {
if self.mode_clock >= 456 {
// increments the register that controls the line count,
// notice that these represent the extra 10 horizontal
// scanlines that are virtual and not real (off-screen)
// in case the end of V-Blank has been reached then
// we must jump again to the OAM read mode and reset
// the scan line counter to the zero value
self.frame_index = self.frame_index.wrapping_add(1);
pub fn read(&mut self, addr: u16) -> u8 {
0x8000..=0x9fff => self.vram[(self.vram_offset + (addr & 0x1fff)) as usize],
0xfe00..=0xfe9f => self.oam[(addr & 0x009f) as usize],
// Not Usable
0xfea0..=0xfeff => 0xff,
0xff80..=0xfffe => self.hram[(addr & 0x007f) as usize],
0xff40 =>
{
#[allow(clippy::bool_to_int_with_if)]
| if self.switch_obj { 0x02 } else { 0x00 }
| if self.obj_size { 0x04 } else { 0x00 }
| if self.bg_map { 0x08 } else { 0x00 }
| if self.bg_tile { 0x10 } else { 0x00 }
| if self.switch_window { 0x20 } else { 0x00 }
| if self.window_map { 0x40 } else { 0x00 }
| if self.stat_vblank { 0x10 } else { 0x00 }
| if self.stat_oam { 0x20 } else { 0x00 }
| if self.stat_lyc { 0x40 } else { 0x00 }
0xff42 => self.scy,
0xff43 => self.scx,
0xff44 => self.ly,
0xff45 => self.lyc,
0xff47 => self.palettes[0],
0xff48 => self.palettes[1],
0xff49 => self.palettes[2],
0xff4a => self.wy,
0xff4b => self.wx,
// 0xFF4F — VBK (CGB only)
0xff4f => self.vram_bank | 0xfe,
// 0xFF68 — BCPS/BGPI (CGB only)
0xff68 => self.palette_address_bg | if self.auto_increment_bg { 0x80 } else { 0x00 },
// 0xFF69 — BCPD/BGPD (CGB only)
0xff69 => self.palettes_color[0][self.palette_address_bg as usize],
// 0xFF6A — OCPS/OBPI (CGB only)
0xff6a => self.palette_address_obj | if self.auto_increment_obj { 0x80 } else { 0x00 },
// 0xFF6B — OCPD/OBPD (CGB only)
0xff6b => self.palettes_color[1][self.palette_address_obj as usize],
#[allow(clippy::bool_to_int_with_if)]
if self.obj_priority {
0x01
} else {
0x00
}
}
_ => {
warnln!("Reading from unknown PPU location 0x{:04x}", addr);
0xff
}
}
}
pub fn write(&mut self, addr: u16, value: u8) {
match addr {
0x8000..=0x9fff => {
self.vram[(self.vram_offset + (addr & 0x1fff)) as usize] = value;
if addr < 0x9800 {
self.update_tile(addr, value);
} else if self.vram_bank == 0x1 {
self.update_bg_map_attrs(addr, value);
}
0xfe00..=0xfe9f => {
self.oam[(addr & 0x009f) as usize] = value;
self.update_object(addr, value);
}
// Not Usable
0xfea0..=0xfeff => (),
0xff80..=0xfffe => self.hram[(addr & 0x007f) as usize] = value,
0xff40 => {
self.switch_bg = value & 0x01 == 0x01;
self.bg_map = value & 0x08 == 0x08;
self.bg_tile = value & 0x10 == 0x10;
self.switch_window = value & 0x20 == 0x20;
self.window_map = value & 0x40 == 0x40;
self.switch_lcd = value & 0x80 == 0x80;
// in case the LCD is off takes the opportunity
// to clear the screen, this is the expected
// behaviour for this specific situation
if !self.switch_lcd {
self.mode_clock = 0;
self.ly = 0;
self.int_vblank = false;
self.int_stat = false;
self.stat_hblank = value & 0x08 == 0x08;
self.stat_vblank = value & 0x10 == 0x10;
self.stat_oam = value & 0x20 == 0x20;
self.stat_lyc = value & 0x40 == 0x40;
0xff42 => self.scy = value,
0xff43 => self.scx = value,
0xff45 => self.lyc = value,
0xff47 => {
if value == self.palettes[0] {
return;
}
if self.dmg_compat {
Self::compute_palette(&mut self.palette_bg, &self.palettes_color_bg[0], value);
} else {
Self::compute_palette(&mut self.palette_bg, &self.palette_colors, value);
}
if value == self.palettes[1] {
return;
}
if self.dmg_compat {
Self::compute_palette(
&mut self.palette_obj_0,
&self.palettes_color_obj[0],
value,
);
} else {
Self::compute_palette(&mut self.palette_obj_0, &self.palette_colors, value);
}
if value == self.palettes[2] {
return;
}
if self.dmg_compat {
Self::compute_palette(
&mut self.palette_obj_1,
&self.palettes_color_obj[1],
value,
);
} else {
Self::compute_palette(&mut self.palette_obj_1, &self.palette_colors, value);
}
0xff4a => self.wy = value,
0xff4b => self.wx = value,
// 0xFF4F — VBK (CGB only)
0xff4f => {
self.vram_bank = value & 0x01;
self.vram_offset = self.vram_bank as u16 * 0x2000;
}
// 0xFF68 — BCPS/BGPI (CGB only)
0xff68 => {
self.palette_address_bg = value & 0x3f;
self.auto_increment_bg = value & 0x80 == 0x80;
}
// 0xFF69 — BCPD/BGPD (CGB only)
0xff69 => {
let palette_index = self.palette_address_bg / 8;
let color_index = (self.palette_address_bg % 8) / 2;
let palette_color = &mut self.palettes_color[0];
palette_color[self.palette_address_bg as usize] = value;
let palette = &mut self.palettes_color_bg[palette_index as usize];
Self::compute_palette_color(palette, palette_color, palette_index, color_index);
if self.auto_increment_bg {
self.palette_address_bg = (self.palette_address_bg + 1) & 0x3f;
}
}
// 0xFF6A — OCPS/OBPI (CGB only)
0xff6a => {
self.palette_address_obj = value & 0x3f;
self.auto_increment_obj = value & 0x80 == 0x80;
}
// 0xFF6B — OCPD/OBPD (CGB only)
0xff6b => {
let palette_index = self.palette_address_obj / 8;
let color_index = (self.palette_address_obj % 8) / 2;
let palette_color = &mut self.palettes_color[1];
palette_color[self.palette_address_obj as usize] = value;
let palette = &mut self.palettes_color_obj[palette_index as usize];
Self::compute_palette_color(palette, palette_color, palette_index, color_index);
if self.auto_increment_obj {
self.palette_address_obj = (self.palette_address_obj + 1) & 0x3f;
}
}
// 0xFF6C — OPRI (CGB only)
0xff6c => self.obj_priority = value & 0x01 == 0x01,
_ => warnln!("Writing in unknown PPU location 0x{:04x}", addr),
pub fn frame_buffer_xrgb8888(&self) -> [u8; FRAME_BUFFER_XRGB8888_SIZE] {
let mut buffer = [0u8; FRAME_BUFFER_XRGB8888_SIZE];
for index in 0..DISPLAY_SIZE {
let (r, g, b) = (
self.frame_buffer[index * RGB_SIZE],
self.frame_buffer[index * RGB_SIZE + 1],
self.frame_buffer[index * RGB_SIZE + 2],
);
buffer[index * XRGB8888_SIZE] = b;
buffer[index * XRGB8888_SIZE + 1] = g;
buffer[index * XRGB8888_SIZE + 2] = r;
buffer[index * XRGB8888_SIZE + 3] = 0xff;
}
buffer
}
pub fn frame_buffer_xrgb8888_u32(&self) -> [u32; FRAME_BUFFER_SIZE] {
let mut buffer = [0u32; FRAME_BUFFER_SIZE];
for (index, pixel) in buffer.iter_mut().enumerate().take(DISPLAY_SIZE) {
let (r, g, b) = (
self.frame_buffer[index * RGB_SIZE],
self.frame_buffer[index * RGB_SIZE + 1],
self.frame_buffer[index * RGB_SIZE + 2],
);
*pixel = ((r as u32) << 16) | ((g as u32) << 8) | b as u32;
pub fn frame_buffer_rgb1555(&self) -> [u8; FRAME_BUFFER_RGB1555_SIZE] {
let mut buffer = [0u8; FRAME_BUFFER_RGB1555_SIZE];
for index in 0..DISPLAY_SIZE {
let (r, g, b) = (
self.frame_buffer[index * RGB_SIZE],
self.frame_buffer[index * RGB_SIZE + 1],
self.frame_buffer[index * RGB_SIZE + 2],
);
let rgb1555 = Self::rgb888_to_rgb1555(r, g, b);
buffer[index * RGB1555_SIZE] = rgb1555[0];
buffer[index * RGB1555_SIZE + 1] = rgb1555[1];
pub fn frame_buffer_rgb1555_u16(&self) -> [u16; FRAME_BUFFER_SIZE] {
let mut buffer = [0u16; FRAME_BUFFER_SIZE];
for (index, pixel) in buffer.iter_mut().enumerate().take(DISPLAY_SIZE) {
let (r, g, b) = (
self.frame_buffer[index * RGB_SIZE],
self.frame_buffer[index * RGB_SIZE + 1],
self.frame_buffer[index * RGB_SIZE + 2],
);
pub fn frame_buffer_rgb565(&self) -> [u8; FRAME_BUFFER_RGB565_SIZE] {
let mut buffer = [0u8; FRAME_BUFFER_RGB565_SIZE];
for index in 0..DISPLAY_SIZE {
let (r, g, b) = (
self.frame_buffer[index * RGB_SIZE],
self.frame_buffer[index * RGB_SIZE + 1],
self.frame_buffer[index * RGB_SIZE + 2],
);
let rgb565 = Self::rgb888_to_rgb565(r, g, b);
buffer[index * RGB565_SIZE] = rgb565[0];
buffer[index * RGB565_SIZE + 1] = rgb565[1];
pub fn frame_buffer_rgb565_u16(&self) -> [u16; FRAME_BUFFER_SIZE] {
let mut buffer = [0u16; FRAME_BUFFER_SIZE];
for (index, pixel) in buffer.iter_mut().enumerate().take(DISPLAY_SIZE) {
let (r, g, b) = (
self.frame_buffer[index * RGB_SIZE],
self.frame_buffer[index * RGB_SIZE + 1],
self.frame_buffer[index * RGB_SIZE + 2],
);
}
buffer
}
pub fn vram(&self) -> &[u8; VRAM_SIZE] {
&self.vram
}
pub fn hram(&self) -> &[u8; HRAM_SIZE] {
&self.hram
}
pub fn tiles(&self) -> &[Tile; TILE_COUNT] {
&self.tiles
pub fn set_palette_colors(&mut self, value: &Palette) {
self.palette_colors = *value;
pub fn palette_bg(&self) -> Palette {
self.palette_bg
}
pub fn palette_obj_0(&self) -> Palette {
self.palette_obj_0
}
pub fn palette_obj_1(&self) -> Palette {
self.palette_obj_1
}
pub fn ly(&self) -> u8 {
self.ly
}
pub fn mode(&self) -> PpuMode {
self.mode
}
pub fn frame_index(&self) -> u16 {
self.frame_index
}
pub fn int_vblank(&self) -> bool {
self.int_vblank
}
pub fn set_int_vblank(&mut self, value: bool) {
self.int_vblank = value;
}
pub fn int_stat(&self) -> bool {
self.int_stat
}
pub fn set_int_stat(&mut self, value: bool) {
self.int_stat = value;
}
pub fn ack_stat(&mut self) {
self.set_int_stat(false);
}
pub fn dmg_compat(&self) -> bool {
self.dmg_compat
}
pub fn set_dmg_compat(&mut self, value: bool) {
self.dmg_compat = value;
// if we're switching to the DMG compat mode
// then we need to recompute the palettes so
// the compat palettes set by the Boot ROM
if value {
self.compute_palettes();
}
pub fn gb_mode(&self) -> GameBoyMode {
self.gb_mode
}
pub fn set_gb_mode(&mut self, value: GameBoyMode) {
self.gb_mode = value;
}
pub fn set_gbc(&mut self, value: Rc<RefCell<GameBoyConfig>>) {
self.gbc = value;
}
/// Fills the frame buffer with pixels of the provided color,
/// this method must represent the fastest way of achieving
/// the fill background with color operation.
pub fn fill_frame_buffer(&mut self, color: Pixel) {
for index in (0..self.frame_buffer.len()).step_by(RGB_SIZE) {
self.frame_buffer[index] = color[0];
self.frame_buffer[index + 1] = color[1];
self.frame_buffer[index + 2] = color[2];
}
}
/// Clears the current frame buffer, setting the background color
/// for all the pixels in the frame buffer.
pub fn clear_frame_buffer(&mut self) {
self.fill_frame_buffer(self.palette_colors[0]);
/// Prints the tile data information to the stdout, this is
/// useful for debugging purposes.
pub fn print_tile_stdout(&self, tile_index: usize) {
println!("{}", self.tiles[tile_index]);
/// Updates the tile structure with the value that has
/// just been written to a location on the VRAM associated
/// with tiles.
fn update_tile(&mut self, addr: u16, _value: u8) {
let addr = (self.vram_offset + (addr & 0x1ffe)) as usize;
let tile_index = ((addr >> 4) & 0x01ff) + (self.vram_bank as usize * TILE_COUNT_DMG);
let tile = self.tiles[tile_index].borrow_mut();
mask = 1 << (TILE_WIDTH_I - x);
x,
y,
if self.vram[addr] & mask > 0 { 0x1 } else { 0x0 }
| if self.vram[addr + 1] & mask > 0 {
0x2
} else {
0x0
},
);
fn update_object(&mut self, addr: u16, value: u8) {
let addr = (addr & 0x01ff) as usize;
let obj_index = addr >> 2;
if obj_index >= OBJ_COUNT {
return;
}
0x00 => obj.y = value as i16 - 16,
0x01 => obj.x = value as i16 - 8,
0x02 => obj.tile = value,
obj.palette_cgb = value & 0x07;
obj.tile_bank = (value & 0x08 == 0x08) as u8;
obj.xflip = value & 0x20 == 0x20;
obj.yflip = value & 0x40 == 0x40;
obj.bg_over = value & 0x80 == 0x80;
}
_ => (),
}
}
fn update_bg_map_attrs(&mut self, addr: u16, value: u8) {
let tile_index = if bg_map { addr - 0x9c00 } else { addr - 0x9800 };
let bg_map_attrs = if bg_map {
&mut self.bg_map_attrs_1
let tile_data: &mut TileData = bg_map_attrs[tile_index as usize].borrow_mut();
tile_data.vram_bank = (value & 0x08 == 0x08) as u8;
tile_data.xflip = value & 0x20 == 0x20;
tile_data.yflip = value & 0x40 == 0x40;
tile_data.priority = value & 0x80 == 0x80;
pub fn registers(&self) -> PpuRegisters {
PpuRegisters {
scy: self.scy,
scx: self.scx,
wy: self.wy,
wx: self.wx,
ly: self.ly,
lyc: self.lyc,
}
}
if self.gb_mode == GameBoyMode::Dmg {
self.render_line_dmg();
} else {
self.render_line_cgb();
}
}
fn render_line_dmg(&mut self) {
if self.switch_bg {
self.render_map_dmg(self.bg_map, self.scx, self.scy, 0, 0, self.ly);
}
if self.switch_bg && self.switch_window {
self.render_map_dmg(self.window_map, 0, 0, self.wx, self.wy, self.window_counter);
}
if self.switch_obj {
self.render_objects();
}
}
fn render_line_cgb(&mut self) {
if self.first_frame {
return;
}
let switch_bg_window = (self.gb_mode.is_cgb() && !self.dmg_compat) || self.switch_bg;
self.render_map(self.bg_map, self.scx, self.scy, 0, 0, self.ly);
if switch_bg_window && self.switch_window {
self.render_map(self.window_map, 0, 0, self.wx, self.wy, self.window_counter);
}
if self.switch_obj {
self.render_objects();
}
}
fn render_map(&mut self, map: bool, scx: u8, scy: u8, wx: u8, wy: u8, ld: u8) {
// in case the target window Y position has not yet been reached
// then there's nothing to be done, returns control flow immediately
if self.ly < wy {
return;
}
// selects the correct background attributes map based on the bg map flag
// because the attributes are separated according to the map they represent
// this is only relevant for CGB mode
let bg_map_attrs = if map {
self.bg_map_attrs_1
} else {
self.bg_map_attrs_0
};
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
// obtains the base address of the background map using the bg map flag
// that control which background map is going to be used
let map_offset: usize = if map { 0x1c00 } else { 0x1800 };
// calculates the map row index for the tile by using the current line
// index and the DY (scroll Y) divided by 8 (as the tiles are 8x8 pixels),
// on top of that ensures that the result is modulus 32 meaning that the
// drawing wraps around the Y axis
let row_index = (((ld as usize + scy as usize) & 0xff) >> 3) % 32;
// calculates the map offset by the row offset multiplied by the number
// of tiles in each row (32)
let row_offset = row_index * 32;
// calculates the sprite line offset by using the SCX register
// shifted by 3 meaning that the tiles are 8x8
let mut line_offset = (scx >> 3) as usize;
// calculates the index of the initial tile in drawing,
// if the tile data set in use is #1, the indexes are
// signed, then calculates a real tile offset
let mut tile_index = self.vram[map_offset + row_offset + line_offset] as usize;
if !self.bg_tile && tile_index < 128 {
tile_index += 256;
}
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
// obtains the reference to the attributes of the new tile in
// drawing for meta processing (CGB only)
let mut tile_attr = if self.dmg_compat {
&DEFAULT_TILE_ATTR
} else {
&bg_map_attrs[row_offset + line_offset]
};
// retrieves the proper palette for the current tile in drawing
// taking into consideration if we're running in CGB mode or not
let mut palette = if self.gb_mode == GameBoyMode::Cgb {
if self.dmg_compat {
&self.palette_bg
} else {
&self.palettes_color_bg[tile_attr.palette as usize]
}
} else {
&self.palette_bg
};
// obtains the values of both X and Y flips for the current tile
// they will be applied by the get tile pixel method
let mut xflip = tile_attr.xflip;
let mut yflip = tile_attr.yflip;
// obtains the value the BG-to-OAM priority to be used in the computation
// of the final pixel value (CGB only)
let mut priority = tile_attr.priority;
// increments the tile index value by the required offset for the VRAM
// bank in which the tile is stored, this is only required for CGB mode
tile_index += tile_attr.vram_bank as usize * TILE_COUNT_DMG;
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
// obtains the reference to the tile that is going to be drawn
let mut tile = &self.tiles[tile_index];
// calculates the offset that is going to be used in the update of the color buffer
// which stores Game Boy colors from 0 to 3
let mut color_offset = self.ly as usize * DISPLAY_WIDTH;
// calculates the frame buffer offset position assuming the proper
// Game Boy screen width and RGB pixel (3 bytes) size
let mut frame_offset = self.ly as usize * DISPLAY_WIDTH * RGB_SIZE;
// calculates both the current Y and X positions within the tiles
// using the bitwise and operation as an effective modulus 8
let y = (ld as usize + scy as usize) & 0x07;
let mut x = (scx & 0x07) as usize;
// calculates the initial tile X position in drawing, doing this
// allows us to position the background map properly in the display
let initial_index = max(wx as i16 - 7, 0) as usize;
color_offset += initial_index;
frame_offset += initial_index * RGB_SIZE;
// iterates over all the pixels in the current line of the display
// to draw the background map, note that the initial index is used
// to skip the drawing of the tiles that are not visible (WX)
for _ in initial_index..DISPLAY_WIDTH {
// obtains the current pixel data from the tile and
// re-maps it according to the current palette
let pixel = tile.get_flipped(x, y, xflip, yflip);
let color = &palette[pixel as usize];
// updates the pixel in the color buffer, which stores
// the raw pixel color information (unmapped)
self.color_buffer[color_offset] = pixel;
// set the color pixel in the frame buffer
self.frame_buffer[frame_offset] = color[0];
self.frame_buffer[frame_offset + 1] = color[1];
self.frame_buffer[frame_offset + 2] = color[2];
// updates the priority buffer with the current pixel
// the priority is only set in case the priority of
// the background (over OAM) is set in the attributes
// and the pixel is not transparent
self.priority_buffer[color_offset] = priority && pixel != 0;
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
// increments the current tile X position in drawing
x += 1;
// in case the end of tile width has been reached then
// a new tile must be retrieved for rendering
if x == TILE_WIDTH {
// resets the tile X position to the base value
// as a new tile is going to be drawn
x = 0;
// calculates the new line tile offset making sure that
// the maximum of 32 is not overflown
line_offset = (line_offset + 1) % 32;
// calculates the tile index and makes sure the value
// takes into consideration the bg tile value
tile_index = self.vram[map_offset + row_offset + line_offset] as usize;
if !self.bg_tile && tile_index < 128 {
tile_index += 256;
}
// in case the current mode is CGB and the DMG compatibility
// flag is not set then a series of tile values must be
// updated according to the tile attributes field
if self.gb_mode == GameBoyMode::Cgb && !self.dmg_compat {
tile_attr = &bg_map_attrs[row_offset + line_offset];
palette = &self.palettes_color_bg[tile_attr.palette as usize];
xflip = tile_attr.xflip;
yflip = tile_attr.yflip;
priority = tile_attr.priority;
tile_index += tile_attr.vram_bank as usize * TILE_COUNT_DMG;
}
// obtains the reference to the new tile in drawing
tile = &self.tiles[tile_index];
}
// increments the color offset by one, representing
// the drawing of one pixel
color_offset += 1;
// increments the offset of the frame buffer by the
// size of an RGB pixel (which is 3 bytes)
frame_offset += RGB_SIZE;
}
}
fn render_map_dmg(&mut self, map: bool, scx: u8, scy: u8, wx: u8, wy: u8, ld: u8) {
// in case the target window Y position has not yet been reached
// then there's nothing to be done, returns control flow immediately
if self.ly < wy {
return;
}
// obtains the base address of the background map using the bg map flag
// that control which background map is going to be used
let map_offset: usize = if map { 0x1c00 } else { 0x1800 };
// calculates the map row index for the tile by using the current line
// index and the DY (scroll Y) divided by 8 (as the tiles are 8x8 pixels),
// on top of that ensures that the result is modulus 32 meaning that the
// drawing wraps around the Y axis
let row_index = (((ld as usize + scy as usize) & 0xff) >> 3) % 32;
// calculates the map offset by the row offset multiplied by the number
// calculates the sprite line offset by using the SCX register
// shifted by 3 meaning that the tiles are 8x8
// calculates the index of the initial tile in drawing,
// if the tile data set in use is #1, the indexes are
// signed, then calculates a real tile offset
let mut tile_index = self.vram[map_offset + row_offset + line_offset] as usize;
if !self.bg_tile && tile_index < 128 {
tile_index += 256;
}
// obtains the reference to the tile that is going to be drawn
let mut tile = &self.tiles[tile_index];
// calculates the offset that is going to be used in the update of the color buffer
// which stores Game Boy colors from 0 to 3
let mut color_offset = self.ly as usize * DISPLAY_WIDTH;
// calculates the frame buffer offset position assuming the proper
// Game Boy screen width and RGB pixel (3 bytes) size
let mut frame_offset = self.ly as usize * DISPLAY_WIDTH * RGB_SIZE;
// calculates both the current Y and X positions within the tiles
// using the bitwise and operation as an effective modulus 8
let y = (ld as usize + scy as usize) & 0x07;
let mut x = (scx & 0x07) as usize;
// calculates the initial tile X position in drawing, doing this
// allows us to position the background map properly in the display
let initial_index = max(wx as i16 - 7, 0) as usize;
color_offset += initial_index;
frame_offset += initial_index * RGB_SIZE;
// iterates over all the pixels in the current line of the display
// to draw the background map, note that the initial index is used
// to skip the drawing of the tiles that are not visible (WX)
for _ in initial_index..DISPLAY_WIDTH {
// obtains the current pixel data from the tile and
// re-maps it according to the current palette
let pixel = tile.get(x, y);
let color = &self.palette_bg[pixel as usize];
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
// updates the pixel in the color buffer, which stores
// the raw pixel color information (unmapped)
self.color_buffer[color_offset] = pixel;
// set the color pixel in the frame buffer
self.frame_buffer[frame_offset] = color[0];
self.frame_buffer[frame_offset + 1] = color[1];
self.frame_buffer[frame_offset + 2] = color[2];
// increments the current tile X position in drawing
x += 1;
// in case the end of tile width has been reached then
// a new tile must be retrieved for rendering
if x == TILE_WIDTH {
// resets the tile X position to the base value
// as a new tile is going to be drawn
x = 0;
// calculates the new line tile offset making sure that
// the maximum of 32 is not overflown
line_offset = (line_offset + 1) % 32;
// calculates the tile index and makes sure the value
// takes into consideration the bg tile value
tile_index = self.vram[map_offset + row_offset + line_offset] as usize;
if !self.bg_tile && tile_index < 128 {
tile_index += 256;
}
// obtains the reference to the new tile in drawing
tile = &self.tiles[tile_index];
// increments the color offset by one, representing
// increments the offset of the frame buffer by the
// size of an RGB pixel (which is 3 bytes)
frame_offset += RGB_SIZE;
// the mode in which the object priority should be computed
// if true this means that the X coordinate priority mode will
// be used otherwise the object priority will be defined according
// to the object's index in the OAM memory, notice that this
// control of priority is only present in the CGB and to be able
// to offer retro-compatibility with DMG
let obj_priority_mode = self.gb_mode != GameBoyMode::Cgb || self.obj_priority;
// creates a local counter object to count the total number
// of object that were drawn in the current line, this will
// be used for instance to limit the total number of objects
// to 10 per line (Game Boy limitation)
let mut draw_count = 0u8;
// allocates the buffer that is going to be used to determine
// drawing priority for overlapping pixels between different
// objects, in MBR mode the object that has the smallest X
// coordinate takes priority in drawing the pixel
let mut index_buffer = [-256i16; DISPLAY_WIDTH];
// determines if the object should always be placed over the
// possible background, this is only required for CGB mode
let always_over = if self.gb_mode == GameBoyMode::Cgb && !self.dmg_compat {
!self.switch_bg
} else {
false
};
// iterates over the complete set of available object to check
// the ones that require drawing and draws them
// in case the limit on the number of objects to be draw per
// line has been reached breaks the loop avoiding more draws
if draw_count == 10 {
break;
}
// obtains the meta data of the object that is currently
// under iteration to be checked for drawing
let obj_height = if self.obj_size {
TILE_DOUBLE_HEIGHT
} else {
TILE_HEIGHT
};
// verifies if the sprite is currently located at the
// current line that is going to be drawn and skips it
// in case it's not
(obj.y <= self.ly as i16) && ((obj.y + obj_height as i16) > self.ly as i16);
if !is_contained {
continue;
}
let palette = if self.gb_mode == GameBoyMode::Cgb {
if self.dmg_compat {
if obj.palette == 0 {
&self.palette_obj_0
} else if obj.palette == 1 {
&self.palette_obj_1
} else {
panic!("Invalid object palette: {:02x}", obj.palette);
}
} else {
&self.palettes_color_obj[obj.palette_cgb as usize]
}
} else if obj.palette == 0 {
&self.palette_obj_0
} else if obj.palette == 1 {
&self.palette_obj_1
panic!("Invalid object palette: {:02x}", obj.palette);
// calculates the offset in the color buffer (raw color information
// from 0 to 3) for the sprit that is going to be drawn, this value
// is kept as a signed integer to allow proper negative number math
let mut color_offset = self.ly as i32 * DISPLAY_WIDTH as i32 + obj.x as i32;
// calculates the offset in the frame buffer for the sprite
// that is going to be drawn, this is going to be the starting
// point for the draw operation to be performed
let mut frame_offset =
(self.ly as i32 * DISPLAY_WIDTH as i32 + obj.x as i32) * RGB_SIZE as i32;
// the relative title offset should range from 0 to 7 in 8x8
// objects and from 0 to 15 in 8x16 objects
let mut tile_offset = self.ly as i16 - obj.y;
// in case we're flipping the object we must recompute the
// tile offset as an inverted value using the object's height
if obj.yflip {
tile_offset = obj_height as i16 - tile_offset - 1;
}
// saves some space for the reference to the tile that
// is going to be used in the current operation
let tile: &Tile;
// "calculates" the index offset that is going to be applied
// to the tile index to retrieve the proper tile taking into
// consideration the VRAM in which the tile is stored
let tile_bank_offset = if self.dmg_compat {
0
} else {
obj.tile_bank as usize * TILE_COUNT_DMG
};
// in case we're facing a 8x16 object then we must
// differentiate between the handling of the top tile
// and the bottom tile through bitwise manipulation
// of the tile index
if self.obj_size {
if tile_offset < 8 {
let tile_index = (obj.tile as usize & 0xfe) + tile_bank_offset;
tile = &self.tiles[tile_index];
let tile_index = (obj.tile as usize | 0x01) + tile_bank_offset;
tile = &self.tiles[tile_index];
}
}
// otherwise we're facing a 8x8 sprite and we should grab
// the tile directly from the object's tile index
else {
let tile_index = obj.tile as usize + tile_bank_offset;
let tile_row = tile.get_row(tile_offset as usize);
// determines if the object should always be placed over the
// previously placed background or window pixels
let obj_over = always_over || !obj.bg_over;
for tile_x in 0..TILE_WIDTH {
let x = obj.x + tile_x as i16;
let is_contained = (x >= 0) && (x < DISPLAY_WIDTH as i16);
// the object is only considered visible if no background or
// window should be drawn over or if the underlying pixel
// is transparent (zero value) meaning there's no background
// or window for the provided pixel
let mut is_visible = obj_over || self.color_buffer[color_offset as usize] == 0;
// additionally (in CCG mode) the object is only considered to
// be visible if the priority buffer is not set for the current
// pixel, this means that the background is capturing priority
// by having the BG-to-OAM priority bit set in the bg map attributes
is_visible &= always_over || !self.priority_buffer[color_offset as usize];
// determines if the current pixel has priority over a possible
// one that has been drawn by a previous object, this happens
// in case the current object has a small X coordinate according
// to the MBR algorithm
let has_priority = index_buffer[x as usize] == -256
|| (obj_priority_mode && obj.x < index_buffer[x as usize]);
let pixel = tile_row[if obj.xflip {
TILE_WIDTH_I - tile_x
} else {
tile_x
}];
if is_visible && has_priority && pixel != 0 {
// marks the current pixel in iteration as "owned"
// by the object with the defined X base position,
// to be used in priority calculus
index_buffer[x as usize] = obj.x;
// obtains the current pixel data from the tile row and
// re-maps it according to the object palette
let color = palette[pixel as usize];
// updates the pixel in the color buffer, which stores
// the raw pixel color information (unmapped)
self.color_buffer[color_offset as usize] = pixel;
// sets the color pixel in the frame buffer
self.frame_buffer[frame_offset as usize] = color[0];
self.frame_buffer[frame_offset as usize + 1] = color[1];
self.frame_buffer[frame_offset as usize + 2] = color[2];
// increment the color offset by one as this represents
// the advance of one color pixel
color_offset += 1;
// increments the offset of the frame buffer by the
// size of an RGB pixel (which is 3 bytes)
frame_offset += RGB_SIZE as i32;
// increments the counter so that we're able to keep
// track on the number of object drawn
draw_count += 1;
/// Runs an update operation on the LCD STAT interrupt meaning
/// that the flag that control will be updated in case the conditions
/// required for the LCD STAT interrupt to be triggered are met.
self.int_stat = self.stat_level();
}
/// Obtains the current level of the LCD STAT interrupt by
/// checking the current PPU state in various sections.
self.stat_lyc && self.lyc == self.ly
|| self.stat_oam && self.mode == PpuMode::OamRead
|| self.stat_vblank && self.mode == PpuMode::VBlank
|| self.stat_hblank && self.mode == PpuMode::HBlank
/// Computes the values for all of the palettes, this method
/// is useful to "flush" color computation whenever the base
/// palette colors are changed.
fn compute_palettes(&mut self) {
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
if self.dmg_compat {
Self::compute_palette(
&mut self.palette_bg,
&self.palettes_color_bg[0],
self.palettes[0],
);
Self::compute_palette(
&mut self.palette_obj_0,
&self.palettes_color_obj[0],
self.palettes[1],
);
Self::compute_palette(
&mut self.palette_obj_1,
&self.palettes_color_obj[1],
self.palettes[2],
);
} else {
// re-computes the complete set of palettes according to
// the currently set palette colors (that may have changed)
Self::compute_palette(&mut self.palette_bg, &self.palette_colors, self.palettes[0]);
Self::compute_palette(
&mut self.palette_obj_0,
&self.palette_colors,
self.palettes[1],
);
Self::compute_palette(
&mut self.palette_obj_1,
&self.palette_colors,
self.palettes[2],
);
}
// clears the frame buffer to allow the new background
// color to be used
self.clear_frame_buffer();
}
/// Static method used for the base logic of computation of RGB
/// based palettes from the internal Game Boy color indexes.
/// This method should be called whenever the palette indexes
/// are changed.
fn compute_palette(palette: &mut Palette, palette_colors: &Palette, value: u8) {
for (index, palette_item) in palette.iter_mut().enumerate() {
let color_index: usize = (value as usize >> (index * 2)) & 3;
match color_index {
0..=3 => *palette_item = palette_colors[color_index],
color_index => panic!("Invalid palette color index {:04x}", color_index),
}
}
}
/// Static method that computes an RGB888 color palette ready to
/// be used for frame buffer operations from 4 (colors) x 2 bytes (RGB555)
/// that represent an RGB555 set of colors. This method should be
/// used for CGB mode where colors are represented using RGB555.
fn compute_palette_color(
palette: &mut Palette,
palette_color: &[u8; 64],
palette_index: u8,
color_index: u8,
) {
let palette_offset = (palette_index * 4 * 2) as usize;
let color_offset = (color_index * 2) as usize;
palette[color_index as usize] = Self::rgb555_to_rgb888(
palette_color[palette_offset + color_offset],
palette_color[palette_offset + color_offset + 1],
);
}
fn rgb555_to_rgb888(first: u8, second: u8) -> Pixel {
let r = (first & 0x1f) << 3;
let g = (((first & 0xe0) >> 5) | ((second & 0x03) << 3)) << 3;
let b = ((second & 0x7c) >> 2) << 3;
fn rgb888_to_rgb1555(first: u8, second: u8, third: u8) -> PixelRgb1555 {
let pixel = Self::rgb888_to_rgb1555_u16(first, second, third);
[pixel as u8, (pixel >> 8) as u8]
}
fn rgb888_to_rgb1555_u16(first: u8, second: u8, third: u8) -> u16 {
let r = (first as u16 >> 3) & 0x1f;
let g = (second as u16 >> 3) & 0x1f;
let b = (third as u16 >> 3) & 0x1f;
(a << 15) | (r << 10) | (g << 5) | b
}
fn rgb888_to_rgb565(first: u8, second: u8, third: u8) -> PixelRgb565 {
let pixel = Self::rgb888_to_rgb565_u16(first, second, third);
[pixel as u8, (pixel >> 8) as u8]
}
fn rgb888_to_rgb565_u16(first: u8, second: u8, third: u8) -> u16 {
let r = (first as u16 >> 3) & 0x1f;
let g = (second as u16 >> 2) & 0x3f;
let b = (third as u16 >> 3) & 0x1f;
(r << 11) | (g << 5) | b
impl Default for Ppu {
fn default() -> Self {
Self::new(
GameBoyMode::Dmg,
Rc::new(RefCell::new(GameBoyConfig::default())),
)
ppu.vram[0x0000] = 0xff;
ppu.vram[0x0001] = 0xff;
let result = ppu.tiles()[0].get(0, 0);
assert_eq!(result, 0);
ppu.update_tile(0x8000, 0x00);
let result = ppu.tiles()[0].get(0, 0);
assert_eq!(result, 3);
}
#[test]
fn test_update_tile_upper() {
ppu.vram[0x1000] = 0xff;
ppu.vram[0x1001] = 0xff;
let result = ppu.tiles()[256].get(0, 0);
assert_eq!(result, 0);
ppu.update_tile(0x9000, 0x00);
let result = ppu.tiles()[256].get(0, 0);
assert_eq!(result, 3);
}