const SCREEN_WIDTH: usize = 240;
const OBJ_COUNT: usize = 128;
const OBJ_VRAM_BASE: usize = 0x1_0000;
#[derive(Clone, Copy, Default)]
pub struct ObjPixel {
pub color: u16,
pub opaque: bool,
pub priority: u8,
pub semi_transparent: bool,
}
pub struct ObjScanline {
pub pixels: [ObjPixel; SCREEN_WIDTH],
pub obj_window: [bool; SCREEN_WIDTH],
}
impl Default for ObjScanline {
fn default() -> Self {
Self {
pixels: [ObjPixel::default(); SCREEN_WIDTH],
obj_window: [false; SCREEN_WIDTH],
}
}
}
fn obj_size(shape: u8, size: u8) -> (u32, u32) {
match (shape, size) {
(0, 0) => (8, 8),
(0, 1) => (16, 16),
(0, 2) => (32, 32),
(0, 3) => (64, 64),
(1, 0) => (16, 8),
(1, 1) => (32, 8),
(1, 2) => (32, 16),
(1, 3) => (64, 32),
(2, 0) => (8, 16),
(2, 1) => (8, 32),
(2, 2) => (16, 32),
(2, 3) => (32, 64),
_ => (0, 0),
}
}
#[allow(clippy::too_many_arguments)]
fn fetch_obj_pixel(
tile_id: usize,
obj_width: u32,
pixel_col: u32,
pixel_row: u32,
is_8bpp: bool,
obj_mapping_1d: bool,
vram: &[u8],
) -> usize {
let tile_col = pixel_col / 8;
let tile_row = pixel_row / 8;
let pixel_x = (pixel_col % 8) as usize;
let pixel_y = (pixel_row % 8) as usize;
let col_stride: u32 = if is_8bpp { 2 } else { 1 };
let tile_offset = if obj_mapping_1d {
let width_in_tiles = obj_width / 8;
let row_stride = width_in_tiles * col_stride;
tile_id + (tile_row * row_stride + tile_col * col_stride) as usize
} else {
let effective_tile_id = if is_8bpp { tile_id & !1 } else { tile_id };
effective_tile_id + (tile_row as usize) * 32 + (tile_col * col_stride) as usize
};
if is_8bpp {
let addr = OBJ_VRAM_BASE + tile_offset * 32 + pixel_y * 8 + pixel_x;
vram.get(addr).copied().unwrap_or(0) as usize
} else {
let addr = OBJ_VRAM_BASE + tile_offset * 32 + pixel_y * 4 + pixel_x / 2;
let byte = vram.get(addr).copied().unwrap_or(0);
if pixel_x & 1 == 0 {
(byte & 0x0F) as usize
} else {
(byte >> 4) as usize
}
}
}
fn read_affine_params(oam: &[u8], group: usize) -> (i16, i16, i16, i16) {
let base = group * 32;
let pa = i16::from_le_bytes([oam[base + 6], oam[base + 7]]);
let pb = i16::from_le_bytes([oam[base + 14], oam[base + 15]]);
let pc = i16::from_le_bytes([oam[base + 22], oam[base + 23]]);
let pd = i16::from_le_bytes([oam[base + 30], oam[base + 31]]);
(pa, pb, pc, pd)
}
pub const OBJ_CYCLE_BUDGET_NORMAL: u32 = 1210;
pub const OBJ_CYCLE_BUDGET_HBLANK_FREE: u32 = 954;
#[allow(clippy::too_many_arguments)]
pub fn render_obj_scanline(
y: u32,
oam: &[u8],
vram: &[u8],
pram: &[u8],
obj_mapping_1d: bool,
bitmap_mode: bool,
mosaic: u16,
cycle_budget: u32,
) -> ObjScanline {
let mut result = ObjScanline::default();
let obj_mosaic_h = super::mosaic_size(mosaic, 8);
let obj_mosaic_v = super::mosaic_size(mosaic, 12);
let mut cycles_used: u32 = 0;
for obj_idx in 0..OBJ_COUNT {
let base = obj_idx * 8;
if base + 5 >= oam.len() {
break;
}
let attr0 = u16::from_le_bytes([oam[base], oam[base + 1]]);
let attr1 = u16::from_le_bytes([oam[base + 2], oam[base + 3]]);
let attr2 = u16::from_le_bytes([oam[base + 4], oam[base + 5]]);
let obj_mode = (attr0 >> 8) & 3;
if obj_mode == 2 {
continue;
}
let is_affine = obj_mode == 1 || obj_mode == 3;
let is_double_size = obj_mode == 3;
let mosaic_enabled = attr0 & (1 << 12) != 0;
let gfx_mode = (attr0 >> 10) & 3;
if gfx_mode == 3 {
continue;
}
let is_obj_window = gfx_mode == 2;
let is_semi_transparent = gfx_mode == 1;
let is_8bpp = (attr0 >> 13) & 1 != 0;
let shape = ((attr0 >> 14) & 3) as u8;
let size_bits = ((attr1 >> 14) & 3) as u8;
let (obj_width, obj_height) = obj_size(shape, size_bits);
if obj_width == 0 || obj_height == 0 {
continue;
}
let (bound_width, bound_height) = if is_double_size {
(obj_width * 2, obj_height * 2)
} else {
(obj_width, obj_height)
};
let obj_y = (attr0 & 0xFF) as u32;
let rel_y = y.wrapping_sub(obj_y) & 0xFF;
if rel_y >= bound_height {
continue;
}
let cycle_cost = if is_affine {
10 + bound_width * 2
} else {
bound_width
};
if cycles_used + cycle_cost > cycle_budget {
break;
}
cycles_used += cycle_cost;
let obj_x = {
let raw = (attr1 & 0x1FF) as i32;
if raw >= 256 { raw - 512 } else { raw }
};
let tile_id = (attr2 & 0x03FF) as usize;
if bitmap_mode && tile_id < 512 {
continue;
}
let priority = ((attr2 >> 10) & 3) as u8;
let palette_bank = ((attr2 >> 12) & 0xF) as usize;
if is_affine {
let affine_group = ((attr1 >> 9) & 0x1F) as usize;
let (pa, pb, pc, pd) = read_affine_params(oam, affine_group);
let cx = bound_width as i32 / 2;
let cy = bound_height as i32 / 2;
let sample_rel_y = if mosaic_enabled {
super::mosaic_anchor(rel_y, obj_mosaic_v)
} else {
rel_y
};
let iry = sample_rel_y as i32 - cy;
for bx in 0..bound_width {
let screen_x = obj_x + bx as i32;
if screen_x < 0 || screen_x >= SCREEN_WIDTH as i32 {
continue;
}
let sx = screen_x as usize;
let sample_bx = if mosaic_enabled {
super::mosaic_anchor(bx, obj_mosaic_h)
} else {
bx
};
let irx = sample_bx as i32 - cx;
let tex_x = (pa as i32 * irx + pb as i32 * iry) + ((obj_width as i32 / 2) << 8);
let tex_y = (pc as i32 * irx + pd as i32 * iry) + ((obj_height as i32 / 2) << 8);
if tex_x < 0
|| tex_y < 0
|| tex_x >= (obj_width as i32) << 8
|| tex_y >= (obj_height as i32) << 8
{
continue;
}
let pixel_col = (tex_x >> 8) as u32;
let pixel_row = (tex_y >> 8) as u32;
let palette_index = fetch_obj_pixel(
tile_id,
obj_width,
pixel_col,
pixel_row,
is_8bpp,
obj_mapping_1d,
vram,
);
if palette_index == 0 {
continue;
}
if is_obj_window {
result.obj_window[sx] = true;
continue;
}
if result.pixels[sx].opaque {
continue;
}
let pram_offset = if is_8bpp {
0x200 + palette_index * 2
} else {
0x200 + (palette_bank * 16 + palette_index) * 2
};
let bgr555 = if pram_offset + 1 < pram.len() {
u16::from_le_bytes([pram[pram_offset], pram[pram_offset + 1]])
} else {
0
};
result.pixels[sx] = ObjPixel {
color: bgr555,
opaque: true,
priority,
semi_transparent: is_semi_transparent,
};
}
} else {
let h_flip = (attr1 >> 12) & 1 != 0;
let v_flip = (attr1 >> 13) & 1 != 0;
let sample_rel_y = if mosaic_enabled {
super::mosaic_anchor(rel_y, obj_mosaic_v)
} else {
rel_y
};
let sprite_row = if v_flip {
obj_height - 1 - sample_rel_y
} else {
sample_rel_y
};
for sprite_col in 0..obj_width {
let screen_x = obj_x + sprite_col as i32;
if screen_x < 0 || screen_x >= SCREEN_WIDTH as i32 {
continue;
}
let sx = screen_x as usize;
let sample_col = if mosaic_enabled {
super::mosaic_anchor(sprite_col, obj_mosaic_h)
} else {
sprite_col
};
let pixel_col = if h_flip {
obj_width - 1 - sample_col
} else {
sample_col
};
let palette_index = fetch_obj_pixel(
tile_id,
obj_width,
pixel_col,
sprite_row,
is_8bpp,
obj_mapping_1d,
vram,
);
if palette_index == 0 {
continue;
}
if is_obj_window {
result.obj_window[sx] = true;
continue;
}
if result.pixels[sx].opaque {
continue;
}
let pram_offset = if is_8bpp {
0x200 + palette_index * 2
} else {
0x200 + (palette_bank * 16 + palette_index) * 2
};
let bgr555 = if pram_offset + 1 < pram.len() {
u16::from_le_bytes([pram[pram_offset], pram[pram_offset + 1]])
} else {
0
};
result.pixels[sx] = ObjPixel {
color: bgr555,
opaque: true,
priority,
semi_transparent: is_semi_transparent,
};
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
const OBJ_VRAM_BASE: usize = 0x1_0000;
fn make_oam() -> Vec<u8> {
vec![0u8; 1024]
}
fn make_vram() -> Vec<u8> {
vec![0u8; 96 * 1024]
}
fn make_pram() -> Vec<u8> {
vec![0u8; 1024]
}
fn write_obj(oam: &mut [u8], idx: usize, attr0: u16, attr1: u16, attr2: u16) {
let base = idx * 8;
oam[base..base + 2].copy_from_slice(&attr0.to_le_bytes());
oam[base + 2..base + 4].copy_from_slice(&attr1.to_le_bytes());
oam[base + 4..base + 6].copy_from_slice(&attr2.to_le_bytes());
}
fn fill_4bpp_tile(vram: &mut [u8], tile_id: usize, pal_idx: u8) {
let base = OBJ_VRAM_BASE + tile_id * 32;
let nibble_pair = pal_idx | (pal_idx << 4);
for i in 0..32 {
vram[base + i] = nibble_pair;
}
}
fn fill_8bpp_tile(vram: &mut [u8], tile_id: usize, pal_idx: u8) {
let base = OBJ_VRAM_BASE + tile_id * 32;
for i in 0..64 {
vram[base + i] = pal_idx;
}
}
fn set_obj_color(pram: &mut [u8], index: usize, bgr555: u16) {
let offset = 0x200 + index * 2;
pram[offset] = bgr555 as u8;
pram[offset + 1] = (bgr555 >> 8) as u8;
}
#[test]
fn empty_oam_produces_no_pixels() {
let oam = make_oam();
let vram = make_vram();
let pram = make_pram();
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert!(result.pixels.iter().all(|p| !p.opaque));
assert!(result.obj_window.iter().all(|&w| !w));
}
#[test]
fn hidden_obj_not_rendered() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
let attr0 = 2 << 8; let attr1 = 100; let attr2 = 1; write_obj(&mut oam, 0, attr0, attr1, attr2);
fill_4bpp_tile(&mut vram, 1, 1);
set_obj_color(&mut pram, 1, 0x001F);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert!(!result.pixels[100].opaque, "hidden OBJ should not render");
}
#[test]
fn basic_8x8_4bpp_sprite_renders() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
let attr0 = 5; let attr1 = 10; let attr2 = 0; write_obj(&mut oam, 0, attr0, attr1, attr2);
fill_4bpp_tile(&mut vram, 0, 3);
set_obj_color(&mut pram, 3, 0x03E0);
let result = render_obj_scanline(5, &oam, &vram, &pram, true, false, 0, u32::MAX);
for x in 10..18 {
assert!(result.pixels[x].opaque, "pixel {x} should be opaque");
assert_eq!(result.pixels[x].color, 0x03E0, "pixel {x} should be green");
assert_eq!(result.pixels[x].priority, 0);
}
assert!(!result.pixels[9].opaque);
assert!(!result.pixels[18].opaque);
}
#[test]
fn basic_8x8_8bpp_sprite_renders() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
let attr0 = 1 << 13; let attr1 = 0; let attr2 = 0; write_obj(&mut oam, 0, attr0, attr1, attr2);
fill_8bpp_tile(&mut vram, 0, 5);
set_obj_color(&mut pram, 5, 0x7C00);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
for x in 0..8 {
assert!(result.pixels[x].opaque, "pixel {x} should be opaque");
assert_eq!(result.pixels[x].color, 0x7C00, "pixel {x} should be blue");
}
}
#[test]
fn bitmap_mode_ignores_obj_tile_ids_below_512() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
for i in 0..OBJ_COUNT {
write_obj(&mut oam, i, 2 << 8, 0, 0);
}
write_obj(&mut oam, 0, 0, 0, 0);
fill_4bpp_tile(&mut vram, 512, 1);
set_obj_color(&mut pram, 1, 0x001F);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, true, 0, u32::MAX);
assert!(
!result.pixels[0].opaque,
"tile IDs 0-511 must be invisible in bitmap modes"
);
}
#[test]
fn bitmap_mode_renders_obj_tile_id_512() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
for i in 0..OBJ_COUNT {
write_obj(&mut oam, i, 2 << 8, 0, 0);
}
write_obj(&mut oam, 0, 0, 0, 512);
fill_4bpp_tile(&mut vram, 512, 2);
set_obj_color(&mut pram, 2, 0x03E0);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, true, 0, u32::MAX);
assert!(
result.pixels[0].opaque,
"tile 512 should be visible in bitmap modes"
);
assert_eq!(result.pixels[0].color, 0x03E0);
}
#[test]
fn horizontal_flip_reverses_pixels() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
let attr0 = 0;
let attr1 = 1 << 12; let attr2 = 0;
write_obj(&mut oam, 0, attr0, attr1, attr2);
let base = OBJ_VRAM_BASE;
for row in 0..8 {
for col in 0..4 {
let addr = base + row * 4 + col;
let px = col * 2;
if px < 4 {
vram[addr] = 0x11; } else {
vram[addr] = 0x22; }
}
}
set_obj_color(&mut pram, 1, 0x001F); set_obj_color(&mut pram, 2, 0x03E0);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert_eq!(
result.pixels[0].color, 0x03E0,
"flipped: left should be green"
);
assert_eq!(
result.pixels[4].color, 0x001F,
"flipped: right should be red"
);
}
#[test]
fn vertical_flip_reverses_rows() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
let attr0 = 0;
let attr1 = 1 << 13; let attr2 = 0;
write_obj(&mut oam, 0, attr0, attr1, attr2);
let base = OBJ_VRAM_BASE;
for col in 0..4 {
vram[base + col] = 0x11; vram[base + 7 * 4 + col] = 0x22; }
set_obj_color(&mut pram, 1, 0x001F); set_obj_color(&mut pram, 2, 0x03E0);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert_eq!(
result.pixels[0].color, 0x03E0,
"v-flip: scanline 0 should show row 7 data (green)"
);
let result7 = render_obj_scanline(7, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert_eq!(
result7.pixels[0].color, 0x001F,
"v-flip: scanline 7 should show row 0 data (red)"
);
}
#[test]
fn obj_mosaic_repeats_upper_left_anchor_pixel() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
write_obj(&mut oam, 0, 1 << 12, 0, 0);
set_obj_color(&mut pram, 1, 0x001F);
set_obj_color(&mut pram, 2, 0x03E0);
set_obj_color(&mut pram, 3, 0x7C00);
set_obj_color(&mut pram, 4, 0x7FFF);
vram[OBJ_VRAM_BASE] = 0x21;
vram[OBJ_VRAM_BASE + 1] = 0x32;
vram[OBJ_VRAM_BASE + 4] = 0x43;
let result = render_obj_scanline(1, &oam, &vram, &pram, true, false, 0x1100, u32::MAX);
assert_eq!(result.pixels[0].color, 0x001F);
assert_eq!(result.pixels[1].color, 0x001F);
assert_eq!(result.pixels[2].color, 0x03E0);
}
#[test]
fn lower_obj_number_wins_same_pixel() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
write_obj(&mut oam, 0, 0, 0, 0);
fill_4bpp_tile(&mut vram, 0, 1);
set_obj_color(&mut pram, 1, 0x001F);
write_obj(&mut oam, 1, 0, 0, 1);
fill_4bpp_tile(&mut vram, 1, 2);
set_obj_color(&mut pram, 2, 0x03E0);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert_eq!(
result.pixels[0].color, 0x001F,
"OBJ 0 should win over OBJ 1"
);
}
#[test]
fn transparent_pixel_does_not_block() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
write_obj(&mut oam, 0, 0, 0, 0);
write_obj(&mut oam, 1, 0, 0, 1);
fill_4bpp_tile(&mut vram, 1, 2);
set_obj_color(&mut pram, 2, 0x03E0);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert_eq!(
result.pixels[0].color, 0x03E0,
"transparent OBJ 0 should not block OBJ 1"
);
}
#[test]
fn obj_window_sets_mask_not_pixel() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
let attr0 = 2 << 10; let attr1 = 100u16; let attr2 = 1u16; write_obj(&mut oam, 0, attr0, attr1, attr2);
fill_4bpp_tile(&mut vram, 1, 1);
set_obj_color(&mut pram, 1, 0x001F);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert!(
!result.pixels[100].opaque,
"OBJ Window should not be visible"
);
assert!(result.obj_window[100], "OBJ Window should set mask bit");
}
#[test]
fn obj_2d_mapping_uses_32_tile_stride() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
let attr0 = 0; let attr1 = 1 << 14; let attr2 = 0; write_obj(&mut oam, 0, attr0, attr1, attr2);
fill_4bpp_tile(&mut vram, 32, 3);
set_obj_color(&mut pram, 3, 0x7C00);
let result = render_obj_scanline(8, &oam, &vram, &pram, false, false, 0, u32::MAX);
assert!(
result.pixels[0].opaque,
"2D mapping should find tile at stride 32"
);
assert_eq!(result.pixels[0].color, 0x7C00);
}
#[test]
fn obj_2d_mapping_8bpp_ignores_lower_bit_of_tile_id() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
for b in &mut vram[OBJ_VRAM_BASE..OBJ_VRAM_BASE + 32] {
*b = 7;
}
for b in &mut vram[OBJ_VRAM_BASE + 32..OBJ_VRAM_BASE + 64] {
*b = 9;
}
set_obj_color(&mut pram, 7, 0x03E0); set_obj_color(&mut pram, 9, 0x7C00);
let attr0 = 1 << 13; let attr1 = 0; let attr2 = 1; write_obj(&mut oam, 0, attr0, attr1, attr2);
let result = render_obj_scanline(0, &oam, &vram, &pram, false, false, 0, u32::MAX);
assert!(
result.pixels[0].opaque,
"8bpp 2D mapping with odd tile_id=1 must render using tile 0 (lower bit masked)"
);
assert_eq!(
result.pixels[0].color, 0x03E0,
"color must come from tile 0 (effective after masking tile_id=1 → 0); \
got blue means the bug is present (tile 1 used instead of tile 0)"
);
}
#[test]
fn obj_1d_mapping_uses_width_stride() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
let attr0 = 0;
let attr1 = 1 << 14; let attr2 = 0; write_obj(&mut oam, 0, attr0, attr1, attr2);
fill_4bpp_tile(&mut vram, 2, 4);
set_obj_color(&mut pram, 4, 0x03E0);
let result = render_obj_scanline(8, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert!(
result.pixels[0].opaque,
"1D mapping should find tile at width stride"
);
assert_eq!(result.pixels[0].color, 0x03E0);
}
#[test]
fn sprite_wraps_y_at_256() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
let attr0 = 252u16; write_obj(&mut oam, 0, attr0, 0, 0);
fill_4bpp_tile(&mut vram, 0, 1);
set_obj_color(&mut pram, 1, 0x001F);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert!(
result.pixels[0].opaque,
"sprite at Y=252 should wrap to scanline 0"
);
}
#[test]
fn sprite_negative_x_partially_visible() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
for i in 0..OBJ_COUNT {
write_obj(&mut oam, i, 2 << 8, 0, 0); }
let attr0 = 0;
let attr1 = 508u16; let attr2 = 1u16; write_obj(&mut oam, 0, attr0, attr1, attr2);
fill_4bpp_tile(&mut vram, 1, 1);
set_obj_color(&mut pram, 1, 0x001F);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert!(
result.pixels[0].opaque,
"partially visible sprite at negative X"
);
assert!(result.pixels[3].opaque);
assert!(!result.pixels[4].opaque);
}
#[test]
fn priority_field_is_read_from_attr2() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
let attr2 = 2 << 10;
write_obj(&mut oam, 0, 0, 0, attr2);
fill_4bpp_tile(&mut vram, 0, 1);
set_obj_color(&mut pram, 1, 0x001F);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert_eq!(result.pixels[0].priority, 2);
}
#[test]
fn large_64x64_sprite_covers_area() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
let attr0 = 0;
let attr1 = 3 << 14; let attr2 = 0;
write_obj(&mut oam, 0, attr0, attr1, attr2);
fill_4bpp_tile(&mut vram, 0, 1);
set_obj_color(&mut pram, 1, 0x001F);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert!(result.pixels[0].opaque);
assert!(result.pixels[7].opaque);
}
fn write_affine_params(oam: &mut [u8], group: usize, pa: i16, pb: i16, pc: i16, pd: i16) {
let base = group * 32;
oam[base + 6..base + 8].copy_from_slice(&pa.to_le_bytes());
oam[base + 14..base + 16].copy_from_slice(&pb.to_le_bytes());
oam[base + 22..base + 24].copy_from_slice(&pc.to_le_bytes());
oam[base + 30..base + 32].copy_from_slice(&pd.to_le_bytes());
}
#[test]
fn read_affine_params_extracts_from_oam() {
let mut oam = make_oam();
write_affine_params(&mut oam, 2, 0x0100, -0x0100, 0x0080, 0x0200);
let (pa, pb, pc, pd) = super::read_affine_params(&oam, 2);
assert_eq!(pa, 0x0100);
assert_eq!(pb, -0x0100);
assert_eq!(pc, 0x0080);
assert_eq!(pd, 0x0200);
}
#[test]
fn affine_identity_renders_same_as_regular() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
pram[0x200 + 2] = 0x1F; pram[0x200 + 3] = 0x00;
fill_4bpp_tile(&mut vram, 0, 1);
write_obj(&mut oam, 0, 0x0000, 10, 0);
let regular = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
let mut oam2 = make_oam();
write_obj(&mut oam2, 0, 0x0100, 10, 0);
write_affine_params(&mut oam2, 0, 0x0100, 0, 0, 0x0100);
let affine = render_obj_scanline(0, &oam2, &vram, &pram, true, false, 0, u32::MAX);
for x in 10..18 {
assert_eq!(
regular.pixels[x].opaque, affine.pixels[x].opaque,
"mismatch at x={x}"
);
if regular.pixels[x].opaque {
assert_eq!(regular.pixels[x].color, affine.pixels[x].color);
}
}
}
#[test]
fn affine_2x_scale_renders_larger() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
pram[0x200 + 2] = 0x1F;
pram[0x200 + 3] = 0x00;
fill_4bpp_tile(&mut vram, 0, 1);
write_obj(&mut oam, 0, 0x0300, 0, 0);
write_affine_params(&mut oam, 0, 0x0080, 0, 0, 0x0080);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
let opaque_count: usize = (0..16).filter(|&x| result.pixels[x].opaque).count();
assert!(
opaque_count >= 14,
"Expected most of 16 pixels opaque, got {opaque_count}"
);
}
#[test]
fn affine_obj_window_sets_mask() {
let mut oam = make_oam();
let mut vram = make_vram();
let pram = make_pram();
for i in 0..128 {
write_obj(&mut oam, i, 0x0200, 0, 0);
}
fill_4bpp_tile(&mut vram, 0, 1);
write_obj(&mut oam, 0, 0x0900, 0, 0);
write_affine_params(&mut oam, 0, 0x0100, 0, 0, 0x0100);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, u32::MAX);
assert!(result.obj_window[0]);
assert!(result.obj_window[7]);
assert!(!result.pixels[0].opaque);
}
#[test]
fn affine_double_size_extends_bounding_box() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
pram[0x200 + 2] = 0x1F;
pram[0x200 + 3] = 0x00;
fill_4bpp_tile(&mut vram, 0, 1);
write_obj(&mut oam, 0, 0x0300, 0, 0);
write_affine_params(&mut oam, 0, 0x0100, 0, 0, 0x0100);
let result = render_obj_scanline(12, &oam, &vram, &pram, true, false, 0, u32::MAX);
let result6 = render_obj_scanline(6, &oam, &vram, &pram, true, false, 0, u32::MAX);
let opaque_count: usize = (0..16).filter(|&x| result6.pixels[x].opaque).count();
assert!(opaque_count > 0, "Should have opaque pixels at scanline 6");
let opaque_at_12: usize = (0..16).filter(|&x| result.pixels[x].opaque).count();
assert_eq!(
opaque_at_12, 0,
"Row 12 should be transparent for identity 8x8"
);
}
#[test]
fn affine_obj_mosaic_anchors_are_applied_before_affine_transform() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
write_obj(&mut oam, 0, 0x1100, 0, 0);
write_affine_params(&mut oam, 0, 0x0100, 0x0100, 0x0000, 0x0100);
vram[OBJ_VRAM_BASE + 1] = 0x21;
vram[OBJ_VRAM_BASE + 2] = 0x03;
vram[OBJ_VRAM_BASE + 4 + 1] = 0x54;
set_obj_color(&mut pram, 1, 0x001F);
set_obj_color(&mut pram, 2, 0x03E0);
set_obj_color(&mut pram, 3, 0x7C00);
set_obj_color(&mut pram, 4, 0x7FFF);
set_obj_color(&mut pram, 5, 0x03FF);
let result = render_obj_scanline(1, &oam, &vram, &pram, true, false, 0x1100, u32::MAX);
assert_eq!(result.pixels[6].color, 0x001F);
assert_eq!(result.pixels[7].color, 0x001F);
}
#[test]
fn fetch_obj_pixel_4bpp_returns_correct_index() {
let mut vram = make_vram();
let addr = OBJ_VRAM_BASE + 2 * 4 + 3 / 2; vram[addr] = 0x50;
let idx = fetch_obj_pixel(0, 8, 3, 2, false, true, &vram);
assert_eq!(idx, 5);
}
#[test]
fn fetch_obj_pixel_8bpp_returns_correct_index() {
let mut vram = make_vram();
let addr = OBJ_VRAM_BASE + 3 * 8 + 5;
vram[addr] = 42;
let idx = fetch_obj_pixel(0, 8, 5, 3, true, true, &vram);
assert_eq!(idx, 42);
}
#[test]
fn cycle_budget_stops_rendering_when_exhausted() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
write_obj(&mut oam, 0, 0, 0, 0); write_obj(&mut oam, 1, 0, 10, 1); fill_4bpp_tile(&mut vram, 0, 1);
fill_4bpp_tile(&mut vram, 1, 2);
set_obj_color(&mut pram, 1, 0x001F); set_obj_color(&mut pram, 2, 0x03E0);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, 10);
assert!(result.pixels[0].opaque, "OBJ 0 should render within budget");
assert!(
!result.pixels[10].opaque,
"OBJ 1 should be skipped after budget exhausted"
);
}
#[test]
fn cycle_budget_does_not_count_vertically_offscreen_obj() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
write_obj(&mut oam, 0, 100, 0, 0); write_obj(&mut oam, 1, 0, 10, 1); fill_4bpp_tile(&mut vram, 0, 1);
fill_4bpp_tile(&mut vram, 1, 2);
set_obj_color(&mut pram, 1, 0x001F);
set_obj_color(&mut pram, 2, 0x03E0);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, 10);
assert!(
result.pixels[10].opaque,
"OBJ 1 should render: vertically-offscreen OBJ 0 should not consume cycle budget"
);
}
#[test]
fn cycle_budget_affine_cost_is_10_plus_2n() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
write_obj(&mut oam, 0, 0x0100, 0, 0); write_affine_params(&mut oam, 0, 0x0100, 0, 0, 0x0100); fill_4bpp_tile(&mut vram, 0, 1);
set_obj_color(&mut pram, 1, 0x001F);
let result_no = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, 25);
assert!(
!result_no.pixels[0].opaque,
"Affine OBJ should not render with budget=25 (cost=26)"
);
let result_yes = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, 26);
assert!(
result_yes.pixels[0].opaque,
"Affine OBJ should render with budget=26 (cost=26)"
);
}
#[test]
fn cycle_budget_hidden_obj_does_not_consume_cycles() {
let mut oam = make_oam();
let mut vram = make_vram();
let mut pram = make_pram();
write_obj(&mut oam, 0, 2 << 8, 0, 0); write_obj(&mut oam, 1, 0, 0, 1); fill_4bpp_tile(&mut vram, 1, 1);
set_obj_color(&mut pram, 1, 0x001F);
let result = render_obj_scanline(0, &oam, &vram, &pram, true, false, 0, 8);
assert!(
result.pixels[0].opaque,
"OBJ 1 should render: hidden OBJ 0 did not consume cycles"
);
}
}