Skip to content
Snippets Groups Projects
ppu.rs 72.1 KiB
Newer Older
  • Learn to ignore specific revisions
  •     pub fn frame_buffer_rgb565_u16(&self) -> [u16; FRAME_BUFFER_SIZE] {
            let mut buffer = [0u16; FRAME_BUFFER_SIZE];
    
    João Magalhães's avatar
    João Magalhães committed
            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],
                );
    
    João Magalhães's avatar
    João Magalhães committed
                *pixel = Self::rgb888_to_rgb565_u16(r, g, b);
    
        pub fn vram(&self) -> &[u8; VRAM_SIZE] {
            &self.vram
        }
    
    
        pub fn vram_dmg(&self) -> &[u8] {
            &self.vram[0..VRAM_SIZE_DMG]
        }
    
        pub fn vram_cgb(&self) -> &[u8] {
            &self.vram[0..VRAM_SIZE_CGB]
        }
    
        pub fn vram_device(&self) -> &[u8] {
            match self.gb_mode {
                GameBoyMode::Dmg => self.vram_dmg(),
                GameBoyMode::Cgb => self.vram_cgb(),
                GameBoyMode::Sgb => self.vram_dmg(),
            }
        }
    
        pub fn set_vram(&mut self, value: &[u8]) {
            self.vram[0..value.len()].copy_from_slice(value);
            self.update_vram();
        }
    
    
        pub fn hram(&self) -> &[u8; HRAM_SIZE] {
            &self.hram
        }
    
    
        pub fn set_hram(&mut self, value: [u8; HRAM_SIZE]) {
            self.hram = value;
        }
    
    
        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 palettes_color(&self) -> &[[u8; 64]; 2] {
            &self.palettes_color
        }
    
        pub fn set_palettes_color(&mut self, palettes_color: [[u8; 64]; 2]) {
            self.palettes_color = palettes_color;
            Self::compute_palettes_color(
                &mut [&mut self.palettes_color_bg, &mut self.palettes_color_obj],
                &self.palettes_color,
            );
        }
    
    
        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 ack_vblank(&mut self) {
    
            self.set_int_vblank(false);
    
        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
    
            // 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 internal PPU state (calculated values) according
        /// to the VRAM values, this should be called whenever the VRAM
        /// is replaced.
        pub fn update_vram(&mut self) {
    
            // "saves" the old values of the VRAM bank and offset
            // as they are going to be needed later, this is required
            // as we're going to trick the PPU into switching banks
            // over the update of the calculated values for the new VRAM,
            // essentially required for the `update_tile()` method
            let (vram_bank_old, vram_offset_old) = (self.vram_bank, self.vram_offset);
    
            // determines the number of VRAM banks available according
            // to the running Game Boy running mode (CGB vs DMG)
    
            let vram_banks = if self.gb_mode == GameBoyMode::Cgb {
    
            // goes over all the VRAM banks, and over all the VRAM addresses
            // in those banks to update the internal tiles and background map
            // attributes structures accordingly
    
            for vram_bank in 0..vram_banks {
    
                self.vram_bank = vram_bank;
                self.vram_offset = self.vram_bank as u16 * 0x2000;
    
                for addr in 0x8000..=0x9fff {
    
                    let value = self.vram[(self.vram_offset + (addr & 0x1fff)) as usize];
    
                    if addr < 0x9800 {
                        self.update_tile(addr, value);
    
                    } else if self.vram_bank == 0x1 {
    
                        self.update_bg_map_attrs(addr, value);
                    }
                }
            }
    
    
            // restores the "old" values for VRAM bank and offset
            (self.vram_bank, self.vram_offset) = (vram_bank_old, vram_offset_old);
    
        /// 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;
    
            for x in 0..TILE_WIDTH {
    
                mask = 1 << (TILE_WIDTH_I - x);
    
    João Magalhães's avatar
    João Magalhães committed
                #[allow(clippy::bool_to_int_with_if)]
    
                    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;
            }
    
    João Magalhães's avatar
    João Magalhães committed
            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,
    
                    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
    
                &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 {
    
            // 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
    
    João Magalhães's avatar
    João Magalhães committed
            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
    
                    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];
    
                        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
    
    João Magalhães's avatar
    João Magalhães committed
                        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.
    
        /// Notice that this is only applicable to the DMG running mode
        /// either in the original DMG or in CGB with DMG compatibility.
    
        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],
            );
    
        /// Re-computes the complete set of CGB only color palettes using the
        /// raw `palettes_color` information and computing the `Palette` structure
        /// for both background and objects palettes.
        fn compute_palettes_color(
            palettes: &mut [&mut [Palette; 8]; 2],
            palettes_color: &[[u8; 64]; 2],
        ) {
            for index in 0..2 {
                let palette = &mut palettes[index];
                let palette_color = &palettes_color[index];
    
                for palette_index in 0..palette.len() {
    
                    Self::compute_color_palette(
    
                        &mut palette[palette_index],
                        &palette_color[palette_index * 8..(palette_index + 1) * 8]
    
                            .try_into()
                            .unwrap(),
                    );
                }
            }
        }
    
        /// Computes an individual structured CGB color palette from 8 raw bytes
    
        /// coming from the raw `palette_color` information, this 8 bytes should
        /// represent the 4 colors of the palette in the RGB555 format.
    
        fn compute_color_palette(palette: &mut Palette, palette_color: &[u8; 8]) {
            for color_index in 0..palette.len() {
    
                palette[color_index] = Self::rgb555_to_rgb888(
    
                    palette_color[color_index * 2],
                    palette_color[color_index * 2 + 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;
    
            (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())),
            )
    
    João Magalhães's avatar
    João Magalhães committed
    
    #[cfg(test)]
    mod tests {
    
        use super::Ppu;
    
    João Magalhães's avatar
    João Magalhães committed
    
        #[test]
        fn test_update_tile_simple() {
    
            let mut ppu = Ppu::default();
    
    João Magalhães's avatar
    João Magalhães committed
            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();
    
    João Magalhães's avatar
    João Magalhães committed
            ppu.vram[0x1000] = 0xff;
            ppu.vram[0x1001] = 0xff;
    
            let result = ppu.tiles()[256].get(0, 0);
            assert_eq!(result, 0);