Newer
Older
fn update_object(&mut self, addr: u16, value: u8) {
let addr = (addr & 0x01ff) as usize;
let obj_index = addr >> 2;
if obj_index >= OBJ_COUNT {
return;
}
0x00 => obj.y = value as i16 - 16,
0x01 => obj.x = value as i16 - 8,
0x02 => obj.tile = value,
obj.palette_cgb = value & 0x07;
obj.tile_bank = (value & 0x08 == 0x08) as u8;
obj.xflip = value & 0x20 == 0x20;
obj.yflip = value & 0x40 == 0x40;
obj.bg_over = value & 0x80 == 0x80;
}
_ => (),
}
}
fn update_bg_map_attrs(&mut self, addr: u16, value: u8) {
let tile_index = if bg_map { addr - 0x9c00 } else { addr - 0x9800 };
let bg_map_attrs = if bg_map {
&mut self.bg_map_attrs_1
let tile_data: &mut TileData = bg_map_attrs[tile_index as usize].borrow_mut();
tile_data.vram_bank = (value & 0x08 == 0x08) as u8;
tile_data.xflip = value & 0x20 == 0x20;
tile_data.yflip = value & 0x40 == 0x40;
tile_data.priority = value & 0x80 == 0x80;
pub fn registers(&self) -> PpuRegisters {
PpuRegisters {
scy: self.scy,
scx: self.scx,
wy: self.wy,
wx: self.wx,
ly: self.ly,
lyc: self.lyc,
}
}
let switch_bg_window =
(self.gb_mode == GameBoyMode::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
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)
// @TODO: This strategy seems a bit naive, need to figure out
// if there's a better way to do this and a more performant one
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 {
} 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;
// 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_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;
}
// obtains the reference to the attributes of the new tile in
// drawing for meta processing (CGB only)
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
palette = if self.gb_mode == GameBoyMode::Cgb {
} 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
xflip = tile_attr.xflip;
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;
// 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];
// 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
};
// 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 {
if self.dmg_compat {
if obj.palette == 0 {
&self.palette_obj_0
} else if obj.palette == 1 {
&self.palette_obj_1
} else {
panic!("Invalid object palette: {:02x}", obj.palette);
}
} else {
&self.palettes_color_obj[obj.palette_cgb as usize]
}
} else if obj.palette == 0 {
&self.palette_obj_0
} else if obj.palette == 1 {
&self.palette_obj_1
panic!("Invalid object palette: {:02x}", obj.palette);
// calculates the offset in the color buffer (raw color information
// from 0 to 3) for the sprit that is going to be drawn, this value
// is kept as a signed integer to allow proper negative number math
let mut color_offset = self.ly as i32 * DISPLAY_WIDTH as i32 + obj.x as i32;
// calculates the offset in the frame buffer for the sprite
// that is going to be drawn, this is going to be the starting
// point for the draw operation to be performed
let mut frame_offset =
(self.ly as i32 * DISPLAY_WIDTH as i32 + obj.x as i32) * RGB_SIZE as i32;
// the relative title offset should range from 0 to 7 in 8x8
// objects and from 0 to 15 in 8x16 objects
let mut tile_offset = self.ly as i16 - obj.y;
// in case we're flipping the object we must recompute the
// tile offset as an inverted value using the object's height
if obj.yflip {
tile_offset = obj_height as i16 - tile_offset - 1;
}
// saves some space for the reference to the tile that
// is going to be used in the current operation
let tile: &Tile;
// "calculates" the index offset that is going to be applied
// to the tile index to retrieve the proper tile taking into
// consideration the VRAM in which the tile is stored
let tile_bank_offset = if self.dmg_compat {
0
} else {
obj.tile_bank as usize * TILE_COUNT_DMG
};
// in case we're facing a 8x16 object then we must
// differentiate between the handling of the top tile
// and the bottom tile through bitwise manipulation
// of the tile index
if self.obj_size {
if tile_offset < 8 {
let tile_index = (obj.tile as usize & 0xfe) + tile_bank_offset;
tile = &self.tiles[tile_index];
let tile_index = (obj.tile as usize | 0x01) + tile_bank_offset;
tile = &self.tiles[tile_index];
}
}
// otherwise we're facing a 8x8 sprite and we should grab
// the tile directly from the object's tile index
else {
let tile_index = obj.tile as usize + tile_bank_offset;
let tile_row = tile.get_row(tile_offset as usize);
// determines if the object should always be placed over the
// previously placed background or window pixels
let obj_over = always_over || !obj.bg_over;
for tile_x in 0..TILE_WIDTH {
let x = obj.x + tile_x as i16;
let is_contained = (x >= 0) && (x < DISPLAY_WIDTH as i16);
// the object is only considered visible if no background or
// window should be drawn over or if the underlying pixel
// is transparent (zero value) meaning there's no background
// or window for the provided pixel
let 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 { 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) {
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
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;
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);
}