#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpritePixel {
pub colour_index: u8,
pub palette: u8,
pub bg_priority: bool,
}
pub fn scan_oam_line(scanline: u8, oam: &[u8; 0xA0], lcdc: u8) -> Vec<usize> {
let height: u8 = if lcdc & 0x04 != 0 { 16 } else { 8 };
let mut result = Vec::new();
for i in 0..40usize {
let oam_y = oam[i * 4];
let screen_y = oam_y.wrapping_sub(16);
if scanline >= screen_y && scanline < screen_y.wrapping_add(height) {
result.push(i);
if result.len() >= 10 {
break;
}
}
}
result
}
pub fn fetch_sprite_pixel(
x: u32,
scanline: u8,
sprite_indices: &[usize],
oam: &[u8; 0xA0],
vram: &[u8; 0x2000],
lcdc: u8,
) -> Option<SpritePixel> {
let height: u8 = if lcdc & 0x04 != 0 { 16 } else { 8 };
for &i in sprite_indices {
let oam_y = oam[i * 4];
let oam_x = oam[i * 4 + 1];
let tile_num = oam[i * 4 + 2];
let attrs = oam[i * 4 + 3];
let screen_y = oam_y.wrapping_sub(16);
let screen_x = oam_x.wrapping_sub(8);
if x < screen_x as u32 || x >= screen_x as u32 + 8 {
continue;
}
let y_flip = attrs & 0x40 != 0;
let x_flip = attrs & 0x20 != 0;
let palette = (attrs >> 4) & 1;
let bg_priority = attrs & 0x80 != 0;
let mut row = (scanline - screen_y) as usize;
if y_flip {
row = (height as usize - 1) - row;
}
let mut pixel_x = (x as u8).wrapping_sub(screen_x);
if x_flip {
pixel_x = 7 - pixel_x;
}
let tile_index = if height == 16 {
if row < 8 {
(tile_num & 0xFE) as usize
} else {
row -= 8;
(tile_num | 0x01) as usize
}
} else {
tile_num as usize
};
let tile_addr = tile_index * 16;
let low = vram[tile_addr + row * 2];
let high = vram[tile_addr + row * 2 + 1];
let bit = 7 - pixel_x;
let colour_index = ((high >> bit) & 1) << 1 | ((low >> bit) & 1);
if colour_index == 0 {
continue; }
return Some(SpritePixel {
colour_index,
palette,
bg_priority,
});
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn blank_oam() -> [u8; 0xA0] {
[0u8; 0xA0]
}
fn blank_vram() -> [u8; 0x2000] {
[0u8; 0x2000]
}
fn oam_with_sprite_at(oam_y: u8, oam_x: u8, tile: u8, attrs: u8) -> [u8; 0xA0] {
let mut oam = blank_oam();
oam[0] = oam_y;
oam[1] = oam_x;
oam[2] = tile;
oam[3] = attrs;
oam
}
#[test]
fn test_sprite_on_scanline_is_found() {
let oam = oam_with_sprite_at(16, 8, 0, 0);
let lcdc = 0x02u8; let indices = scan_oam_line(0, &oam, lcdc);
assert!(indices.contains(&0));
}
#[test]
fn test_sprite_above_scanline_is_not_found() {
let oam = oam_with_sprite_at(16, 8, 0, 0);
let lcdc = 0x02u8;
let indices = scan_oam_line(8, &oam, lcdc);
assert!(!indices.contains(&0));
}
#[test]
fn test_sprite_below_scanline_is_not_found() {
let oam = oam_with_sprite_at(17, 8, 0, 0);
let lcdc = 0x02u8;
let indices = scan_oam_line(0, &oam, lcdc);
assert!(!indices.contains(&0));
}
#[test]
fn test_oam_scan_limits_to_10_sprites_per_scanline() {
let mut oam = blank_oam();
for i in 0..40usize {
oam[i * 4] = 16; oam[i * 4 + 1] = (i as u8 + 1) * 2; oam[i * 4 + 2] = 0;
oam[i * 4 + 3] = 0;
}
let lcdc = 0x02u8;
let indices = scan_oam_line(0, &oam, lcdc);
assert!(indices.len() <= 10);
assert_eq!(indices.len(), 10);
}
#[test]
fn test_8x16_sprite_covers_two_tile_rows() {
let mut oam = blank_oam();
oam[0] = 16; oam[1] = 8;
oam[2] = 0;
oam[3] = 0;
let lcdc = 0x06u8; assert!(scan_oam_line(0, &oam, lcdc).contains(&0));
assert!(scan_oam_line(15, &oam, lcdc).contains(&0));
assert!(!scan_oam_line(16, &oam, lcdc).contains(&0));
}
#[test]
fn test_transparent_sprite_pixel_returns_none() {
let oam = oam_with_sprite_at(16, 8, 0, 0); let vram = blank_vram(); let lcdc = 0x02u8;
let indices = vec![0usize];
let result = fetch_sprite_pixel(0, 0, &indices, &oam, &vram, lcdc);
assert_eq!(result, None);
}
#[test]
fn test_opaque_sprite_pixel_returns_some() {
let mut vram = blank_vram();
vram[0x0010] = 0xFF; vram[0x0011] = 0x00; let oam = oam_with_sprite_at(16, 8, 1, 0);
let lcdc = 0x02u8;
let indices = vec![0usize];
let result = fetch_sprite_pixel(0, 0, &indices, &oam, &vram, lcdc);
assert!(result.is_some());
let px = result.unwrap();
assert_eq!(px.colour_index, 1);
assert_eq!(px.palette, 0);
assert!(!px.bg_priority);
}
#[test]
fn test_sprite_palette_bit_selected_from_attr() {
let mut vram = blank_vram();
vram[0x0010] = 0xFF; vram[0x0011] = 0x00;
let oam = oam_with_sprite_at(16, 8, 1, 0x10); let lcdc = 0x02u8;
let indices = vec![0usize];
let result = fetch_sprite_pixel(0, 0, &indices, &oam, &vram, lcdc).unwrap();
assert_eq!(result.palette, 1);
}
}