Skip to content
Snippets Groups Projects
ppu.rs 67.88 KiB
use core::fmt;
use std::{
    borrow::BorrowMut,
    cell::RefCell,
    cmp::max,
    fmt::{Display, Formatter},
    rc::Rc,
};

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 HRAM_SIZE: usize = 128;
pub const OAM_SIZE: usize = 260;
pub const PALETTE_SIZE: usize = 4;
pub const RGB_SIZE: usize = 3;
pub const RGBA_SIZE: usize = 4;
pub const RGB888_SIZE: usize = 3;
pub const XRGB8888_SIZE: usize = 4;
pub const RGB1555_SIZE: usize = 2;
pub const RGB565_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.
pub const DISPLAY_HEIGHT: usize = 144;

/// 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).
pub type Pixel = [u8; RGB_SIZE];

/// 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,
    colors: Palette,
}

impl PaletteInfo {
    pub fn new(name: &str, colors: Palette) -> Self {
        Self {
            name: String::from(name),
            colors,
        }
    }

    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)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Tile {
    /// The buffer for the tile, should contain a byte
    /// per each pixel of the tile with values ranging
    /// from 0 to 3 (4 colors).
    buffer: [u8; 64],
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl Tile {
    pub fn get(&self, x: usize, y: usize) -> u8 {
        self.buffer[y * TILE_WIDTH + x]
    }

    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());
            }
            buffer.push('\n');
        }
        write!(f, "{}", buffer)
    }
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct ObjectData {
    x: i16,
    y: i16,
    tile: u8,
    palette_cgb: u8,
    tile_bank: u8,
    palette: u8,
    xflip: bool,
    yflip: bool,
    bg_over: bool,
    index: u8,
}

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 struct PpuRegisters {
    pub scy: u8,
    pub scx: u8,
    pub wy: u8,
    pub wx: u8,
    pub ly: u8,
    pub lyc: 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
/// use boytacean::ppu::Ppu;
/// let mut ppu = Ppu::default();
/// ppu.clock(8);
/// ```
pub struct Ppu {
    /// 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.
    vram: [u8; VRAM_SIZE],

    /// High RAM memory that should provide extra speed for regular
    /// operations.
    hram: [u8; HRAM_SIZE],

    /// OAM RAM (Sprite Attribute Table ) used for the storage of the
    /// sprite attributes for each of the 40 sprites of the Game Boy.
    oam: [u8; OAM_SIZE],

    /// 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.
    tiles: [Tile; TILE_COUNT],

    /// 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.
    palette_obj_0: Palette,

    /// The palette that is going to be used for sprites/objects #1.
    palette_obj_1: Palette,

    /// 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.
    palettes: [u8; 3],

    /// 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.
    ly: u8,

    /// The line compare register that is going to be used
    /// in the STATE and associated interrupts.
    lyc: u8,

    /// 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.
    switch_bg: bool,

    /// 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).
    bg_map: bool,

    /// If the background tile set is active meaning that the
    /// negative based indexes are going to be used.
    bg_tile: bool,

    /// Controls if the window is meant to be drawn.
    switch_window: bool,

    /// Controls the offset of the map that is going to be drawn
    /// for the window section of the screen.
    window_map: bool,

    /// Flag that controls if the LCD screen is ON and displaying
    /// content.
    switch_lcd: bool,

    // 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.
    window_counter: u8,

    /// 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.
    int_vblank: bool,

    /// Boolean value when the LCD STAT interrupt should be handled by
    /// the next CPU clock operation.
    int_stat: bool,

    /// 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.
    gb_mode: GameBoyMode,

    /// The pointer to the parent configuration of the running
    /// Game Boy emulator, that can be used to control the behaviour
    /// of Game Boy emulation.
    gbc: Rc<RefCell<GameBoyConfig>>,
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PpuMode {
    HBlank = 0,
    VBlank = 1,
    OamRead = 2,
    VramRead = 3,
}

impl Ppu {
    pub fn new(mode: GameBoyMode, gbc: Rc<RefCell<GameBoyConfig>>) -> Self {
        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: [0u8; VRAM_SIZE],
            hram: [0u8; HRAM_SIZE],
            oam: [0u8; OAM_SIZE],
            vram_bank: 0x0,
            vram_offset: 0x0000,
            tiles: [Tile { buffer: [0u8; 64] }; TILE_COUNT],
            obj_data: [ObjectData::default(); OBJ_COUNT],
            palette_colors: PALETTE_COLORS,
            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: [0u8; 3],
            palettes_color: [[0u8; 64]; 2],
            bg_map_attrs_0: [TileData::default(); 1024],
            bg_map_attrs_1: [TileData::default(); 1024],
            obj_priority: false,
            scy: 0x0,
            scx: 0x0,
            wy: 0x0,
            wx: 0x0,
            ly: 0x0,
            lyc: 0x0,
            mode: PpuMode::OamRead,
            mode_clock: 0,
            switch_bg: false,
            switch_obj: false,
            obj_size: false,
            bg_map: false,
            bg_tile: false,
            switch_window: false,
            window_map: false,
            switch_lcd: false,
            window_counter: 0x0,
            auto_increment_bg: false,
            palette_address_bg: 0x0,
            auto_increment_obj: false,
            palette_address_obj: 0x0,
            first_frame: false,
            frame_index: 0,
            stat_hblank: false,
            stat_vblank: false,
            stat_oam: false,
            stat_lyc: false,
            int_vblank: false,
            int_stat: false,
            dmg_compat: false,
            gb_mode: mode,
            gbc,
        }
    }

    pub fn reset(&mut self) {
        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.hram = [0u8; HRAM_SIZE];
        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 = [0u8; 3];
        self.palettes_color = [[0u8; 64]; 2];
        self.bg_map_attrs_0 = [TileData::default(); 1024];
        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.first_frame = false;
        self.frame_index = 0;
        self.stat_hblank = false;
        self.stat_vblank = false;
        self.stat_oam = false;
        self.stat_lyc = false;
        self.int_vblank = false;
        self.int_stat = false;
        self.dmg_compat = 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)
        self.mode_clock += cycles;

        match self.mode {
            PpuMode::OamRead => {
                if self.mode_clock >= 80 {
                    self.mode = PpuMode::VramRead;
                    self.mode_clock -= 80;
                }
            }
            PpuMode::VramRead => {
                if self.mode_clock >= 172 {
                    self.render_line();

                    self.mode = PpuMode::HBlank;
                    self.mode_clock -= 172;
                    self.update_stat()
                }
            }
            PpuMode::HBlank => {
                if self.mode_clock >= 204 {
                    // 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
                    if self.switch_window
                        && self.wx as i16 - 7 < DISPLAY_WIDTH as i16
                        && self.wy < DISPLAY_HEIGHT as u8
                        && self.ly >= self.wy
                    {
                        self.window_counter += 1;
                    }

                    // increments the register that holds the
                    // information about the current line in drawing
                    self.ly += 1;

                    // in case we've reached the end of the
                    // screen we're now entering the V-Blank
                    if self.ly == 144 {
                        self.int_vblank = true;
                        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)
                    self.ly += 1;

                    // 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
                    if self.ly == 154 {
                        self.mode = PpuMode::OamRead;
                        self.ly = 0;
                        self.window_counter = 0;
                        self.first_frame = false;
                        self.frame_index = self.frame_index.wrapping_add(1);
                        self.update_stat()
                    }

                    self.mode_clock -= 456;
                }
            }
        }
    }

    pub fn read(&mut self, addr: u16) -> u8 {
        match addr {
            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_bg { 0x01 } else { 0x00 }
                    | 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.switch_lcd { 0x80 } else { 0x00 })
            }
            0xff41 => {
                (if self.stat_hblank { 0x08 } else { 0x00 }
                    | if self.stat_vblank { 0x10 } else { 0x00 }
                    | if self.stat_oam { 0x20 } else { 0x00 }
                    | if self.stat_lyc { 0x40 } else { 0x00 }
                    | if self.lyc == self.ly { 0x04 } else { 0x00 }
                    | (self.mode as u8 & 0x03))
            }
            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],
            // 0xFF6C — OPRI (CGB only)
            0xff6c =>
            {
                #[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.switch_obj = value & 0x02 == 0x02;
                self.obj_size = value & 0x04 == 0x04;
                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 = PpuMode::HBlank;
                    self.mode_clock = 0;
                    self.ly = 0;
                    self.int_vblank = false;
                    self.int_stat = false;
                    self.first_frame = true;
                    self.clear_frame_buffer();
                }
            }
            0xff41 => {
                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);
                }
                self.palettes[0] = value;
            }
            0xff48 => {
                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);
                }
                self.palettes[1] = value;
            }
            0xff49 => {
                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);
                }
                self.palettes[2] = 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,
            0xff7f => (),
            _ => 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;
        }
        buffer
    }

    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];
        }
        buffer
    }

    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],
            );
            *pixel = Self::rgb888_to_rgb1555_u16(r, g, b);
        }
        buffer
    }

    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];
        }
        buffer
    }

    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],
            );
            *pixel = Self::rgb888_to_rgb565_u16(r, g, b);
        }
        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;
        self.compute_palettes()
    }

    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
    }

    #[inline(always)]
    pub fn int_vblank(&self) -> bool {
        self.int_vblank
    }

    #[inline(always)]
    pub fn set_int_vblank(&mut self, value: bool) {
        self.int_vblank = value;
    }

    #[inline(always)]
    pub fn ack_vblank(&mut self) {
        self.set_int_vblank(false);
    }

    #[inline(always)]
    pub fn int_stat(&self) -> bool {
        self.int_stat
    }

    #[inline(always)]
    pub fn set_int_stat(&mut self, value: bool) {
        self.int_stat = value;
    }

    #[inline(always)]
    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
        // that the colors are correct according to
        // 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) {
        self.color_buffer.fill(0);
        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();
        let y = (addr >> 1) & 0x0007;

        let mut mask;

        for x in 0..TILE_WIDTH {
            mask = 1 << (TILE_WIDTH_I - x);
            #[allow(clippy::bool_to_int_with_if)]
            tile.set(
                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;
        }
        let obj = self.obj_data[obj_index].borrow_mut();
        match addr & 0x03 {
            0x00 => obj.y = value as i16 - 16,
            0x01 => obj.x = value as i16 - 8,
            0x02 => obj.tile = value,
            0x03 => {
                obj.palette_cgb = value & 0x07;
                obj.tile_bank = (value & 0x08 == 0x08) as u8;
                obj.palette = (value & 0x10 == 0x10) as u8;
                obj.xflip = value & 0x20 == 0x20;
                obj.yflip = value & 0x40 == 0x40;
                obj.bg_over = value & 0x80 == 0x80;
                obj.index = obj_index as u8;
            }
            _ => (),
        }
    }

    fn update_bg_map_attrs(&mut self, addr: u16, value: u8) {
        let bg_map = addr >= 0x9c00;
        let tile_index = if bg_map { addr - 0x9c00 } else { addr - 0x9800 };
        let bg_map_attrs = if bg_map {
            &mut self.bg_map_attrs_1
        } else {
            &mut self.bg_map_attrs_0
        };
        let tile_data: &mut TileData = bg_map_attrs[tile_index as usize].borrow_mut();
        tile_data.palette = value & 0x07;
        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,
        }
    }

    fn render_line(&mut self) {
        if self.gb_mode == GameBoyMode::Dmg {
            self.render_line_dmg();
        } else {
            self.render_line_cgb();
        }
    }

    fn render_line_dmg(&mut self) {
        if self.first_frame {
            return;
        }
        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;
        if switch_bg_window {
            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
        };

        // 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;
        }

        // 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;

        // 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;

            // 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
        // 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;
        }

        // 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];

            // 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
            // 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_objects(&mut self) {
        // 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
        for index in 0..OBJ_COUNT {
            // 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 = &self.obj_data[index];

            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
            let is_contained =
                (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
            } else {
                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];
                } else {
                    let tile_index = (obj.tile as usize | 0x01) + tile_bank_offset;
                    tile = &self.tiles[tile_index];
                    tile_offset -= 8;
                }
            }
            // 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;
                tile = &self.tiles[tile_index];
            }

            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);
                if is_contained {
                    // 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.
    fn update_stat(&mut self) {
        self.int_stat = self.stat_level();
    }

    /// Obtains the current level of the LCD STAT interrupt by
    /// checking the current PPU state in various sections.
    fn stat_level(&self) -> bool {
        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) {
        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;
        [r, g, b]
    }

    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;
        let a = 1;
        (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())),
        )
    }
}

#[cfg(test)]
mod tests {
    use super::Ppu;

    #[test]
    fn test_update_tile_simple() {
        let mut ppu = Ppu::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() {
        let mut ppu = Ppu::default();
        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);
    }
}