Newer
Older
// 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
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
// calculates the sprite line offset by using the SCX register
// shifted by 3 meaning that the tiles are 8x8
let mut line_offset: usize = (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 = &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 {
&self.palettes_color_bg[tile_attr.palette as usize]
} else {
&self.palette_bg
};
// 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;
// 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;
for index in 0..DISPLAY_WIDTH {
// in case the current pixel to be drawn for the line
// is visible within the window draws it an increments
// the X coordinate of the tile
if index as i16 >= wx as i16 - 7 {
// obtains the current pixel data from the tile and
// re-maps it according to the current palette
let pixel = self.tiles[tile_index].get(x, y);
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;
}
// obtains the reference to the attributes of the new tile in
// drawing for meta processing (CGB only)
tile_attr = &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
palette = if self.gb_mode == GameBoyMode::Cgb {
&self.palettes_color_bg[tile_attr.palette as usize]
} else {
&self.palette_bg
};
// 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;
// increments the color offset by one, representing
// increments the offset of the frame buffer by the
// size of an RGB pixel (which is 3 bytes)
frame_offset += RGB_SIZE;
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];
// 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
(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 {
&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 = obj.tile_bank as usize * TILE_COUNT_DMG;
// in case we're facing a 8x16 object then we must
// differentiate between the handling of the top tile
// and the bottom tile through bitwise manipulation
// of the tile index
if self.obj_size {
if tile_offset < 8 {
let tile_index = (obj.tile as usize & 0xfe) + tile_bank_offset;
tile = &self.tiles[tile_index];
let tile_index = (obj.tile as usize | 0x01) + tile_bank_offset;
tile = &self.tiles[tile_index];
}
}
// otherwise we're facing a 8x8 sprite and we should grab
// the tile directly from the object's tile index
else {
let tile_index = obj.tile as usize + tile_bank_offset;
let tile_row = tile.get_row(tile_offset as usize);
for tile_x in 0..TILE_WIDTH {
let x = obj.x + tile_x as i16;
let is_contained = (x >= 0) && (x < DISPLAY_WIDTH as i16);
// the object is only considered visible if no background or
// window should be drawn over or if the underlying pixel
// is transparent (zero value) meaning there's no background
// or window for the provided pixel
let is_visible = !obj.bg_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 { 7 - tile_x } else { tile_x }];
if is_visible && has_priority && pixel != 0 {
// marks the current pixel in iteration as "owned"
// by the object with the defined X base position,
// to be used in priority calculus
index_buffer[x as usize] = obj.x;
// obtains the current pixel data from the tile row and
// re-maps it according to the object palette
let color = palette[pixel as usize];
// updates the pixel in the color buffer, which stores
// the raw pixel color information (unmapped)
self.color_buffer[color_offset as usize] = pixel;
// sets the color pixel in the frame buffer
self.frame_buffer[frame_offset as usize] = color[0];
self.frame_buffer[frame_offset as usize + 1] = color[1];
self.frame_buffer[frame_offset as usize + 2] = color[2];
// increment the color offset by one as this represents
// the advance of one color pixel
color_offset += 1;
// increments the offset of the frame buffer by the
// size of an RGB pixel (which is 3 bytes)
frame_offset += RGB_SIZE as i32;
// increments the counter so that we're able to keep
// track on the number of object drawn
draw_count += 1;
/// Runs an update operation on the LCD STAT interrupt meaning
/// that the flag that control will be updated in case the conditions
/// required for the LCD STAT interrupt to be triggered are met.
self.int_stat = self.stat_level();
}
/// Obtains the current level of the LCD STAT interrupt by
/// checking the current PPU state in various sections.
self.stat_lyc && self.lyc == self.ly
|| self.stat_oam && self.mode == PpuMode::OamRead
|| self.stat_vblank && self.mode == PpuMode::VBlank
|| self.stat_hblank && self.mode == PpuMode::HBlank
/// Computes the values for all of the palettes, this method
/// is useful to "flush" color computation whenever the base
/// palette colors are changed.
fn compute_palettes(&mut self) {
// 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;
impl Default for Ppu {
fn default() -> Self {
Self::new(
GameBoyMode::Dmg,
Rc::new(RefCell::new(GameBoyConfig::default())),
)
ppu.vram[0x0000] = 0xff;
ppu.vram[0x0001] = 0xff;
let result = ppu.tiles()[0].get(0, 0);
assert_eq!(result, 0);
ppu.update_tile(0x8000, 0x00);
let result = ppu.tiles()[0].get(0, 0);
assert_eq!(result, 3);
}
#[test]
fn test_update_tile_upper() {
ppu.vram[0x1000] = 0xff;
ppu.vram[0x1001] = 0xff;
let result = ppu.tiles()[256].get(0, 0);
assert_eq!(result, 0);
ppu.update_tile(0x9000, 0x00);
let result = ppu.tiles()[256].get(0, 0);
assert_eq!(result, 3);
}