Skip to content
Snippets Groups Projects
ppu.rs 59.5 KiB
Newer Older
  • Learn to ignore specific revisions
  •                 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.first_frame {
                return;
            }
    
            let switch_bg_window =
                (self.gb_mode == GameBoyMode::Cgb && !self.dmg_compat) || self.switch_bg;
    
            if switch_bg_window {
    
                if self.gb_mode == GameBoyMode::Dmg {
                    self.render_map_dmg(self.bg_map, self.scx, self.scy, 0, 0, self.ly);
                } else {
                    self.render_map(self.bg_map, self.scx, self.scy, 0, 0, self.ly);
                }
    
            if switch_bg_window && self.switch_window {
    
                if self.gb_mode == GameBoyMode::Dmg {
                    self.render_map_dmg(self.window_map, 0, 0, self.wx, self.wy, self.window_counter);
                } else {
                    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_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_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 {
    
            // 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;
    
    
            // 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
    
    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_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];
    
                // 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;
                        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_objects(&mut self) {
    
            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 checks
            // the ones that required 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 is_visible = obj_over || self.color_buffer[color_offset as usize] == 0;
    
    
                        // 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.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]
    
    
    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);
    
            ppu.update_tile(0x9000, 0x00);
            let result = ppu.tiles()[256].get(0, 0);
            assert_eq!(result, 3);
        }