use crate::trace_ppu;
use super::background;
use super::registers::Registers;
use super::screen_buffer::ScreenBuffer;
use super::sprites;
use super::window;
const DMG_GREY: [u8; 4] = [0xFF, 0xAA, 0x55, 0x00];
pub fn palette_lookup(palette_reg: u8, colour_index: u8) -> u8 {
let shade = (palette_reg >> (colour_index * 2)) & 0x03;
DMG_GREY[shade as usize]
}
pub fn render_scanline(
scanline: u8,
vram: &[u8; 0x2000],
oam: &[u8; 0xA0],
registers: &Registers,
window_line: &mut u8,
screen_buffer: &mut ScreenBuffer,
) {
let lcdc = registers.lcdc;
let bg_window_enabled = lcdc & 0x01 != 0;
let obj_enabled = lcdc & 0x02 != 0;
let win_enabled = lcdc & 0x20 != 0;
let sprite_indices = if obj_enabled {
sprites::scan_oam_line(scanline, oam, lcdc)
} else {
Vec::new()
};
let mut window_active = false;
for x in 0..ScreenBuffer::WIDTH {
let bg_idx = if bg_window_enabled {
background::fetch_bg_pixel(x, scanline, vram, lcdc, registers.scx, registers.scy)
} else {
0 };
let bw_idx = if win_enabled {
match window::fetch_window_pixel(
x,
scanline,
vram,
lcdc,
registers.wx,
registers.wy,
*window_line,
) {
Some(idx) => {
window_active = true;
idx
}
None => bg_idx,
}
} else {
bg_idx
};
let sprite_px = sprites::fetch_sprite_pixel(x, scanline, &sprite_indices, oam, vram, lcdc);
let (final_idx, is_sprite, sprite_pal) = if let Some(sp) = sprite_px {
if sp.bg_priority && bw_idx != 0 {
(bw_idx, false, 0u8)
} else {
(sp.colour_index, true, sp.palette)
}
} else {
(bw_idx, false, 0u8)
};
let grey = if is_sprite {
let pal = if sprite_pal == 0 {
registers.obp0
} else {
registers.obp1
};
palette_lookup(pal, final_idx)
} else {
palette_lookup(registers.bgp, final_idx)
};
screen_buffer.set_pixel(x, scanline as u32, grey, grey, grey);
}
trace_ppu!(4; "render y={} sprites={} window={}",
scanline, sprite_indices.len(), window_active);
if window_active {
*window_line = window_line.wrapping_add(1);
}
}
#[inline]
fn cgb_5bit_to_8bit(c5: u8) -> u8 {
(c5 << 3) | (c5 >> 2)
}
fn cgb_palette_lookup(palette_ram: &[u8; 64], palette_num: u8, colour_index: u8) -> (u8, u8, u8) {
let base = (palette_num as usize) * 8 + (colour_index as usize) * 2;
let lo = palette_ram[base];
let hi = palette_ram[base + 1];
let color = u16::from_le_bytes([lo, hi]);
let r5 = (color & 0x1F) as u8;
let g5 = ((color >> 5) & 0x1F) as u8;
let b5 = ((color >> 10) & 0x1F) as u8;
(
cgb_5bit_to_8bit(r5),
cgb_5bit_to_8bit(g5),
cgb_5bit_to_8bit(b5),
)
}
#[allow(clippy::too_many_arguments)]
pub fn render_scanline_cgb(
scanline: u8,
vram: &[u8; 0x2000],
vram_bank1: &[u8; 0x2000],
oam: &[u8; 0xA0],
registers: &Registers,
bg_palette_ram: &[u8; 64],
obj_palette_ram: &[u8; 64],
window_line: &mut u8,
opri_dmg_mode: bool,
screen_buffer: &mut ScreenBuffer,
) {
let lcdc = registers.lcdc;
let obj_enabled = lcdc & 0x02 != 0;
let win_enabled = lcdc & 0x20 != 0;
let master_priority = lcdc & 0x01 != 0;
let sprite_indices = if obj_enabled {
sprites::scan_oam_line(scanline, oam, lcdc)
} else {
Vec::new()
};
let mut window_active = false;
for x in 0..ScreenBuffer::WIDTH {
let bg_px = background::fetch_bg_pixel_cgb(
x,
scanline,
vram,
vram_bank1,
lcdc,
registers.scx,
registers.scy,
);
let bw_px = if win_enabled {
match window::fetch_window_pixel_cgb(
x,
scanline,
vram,
vram_bank1,
lcdc,
registers.wx,
registers.wy,
*window_line,
) {
Some(win_px) => {
window_active = true;
win_px
}
None => bg_px,
}
} else {
bg_px
};
let sprite_px = if obj_enabled {
sprites::fetch_sprite_pixel_cgb(
x,
scanline,
&sprite_indices,
oam,
vram,
vram_bank1,
lcdc,
opri_dmg_mode,
)
} else {
None
};
let (r, g, b) = if let Some(sp) = sprite_px {
let bg_wins =
bw_px.colour_index != 0 && master_priority && (bw_px.bg_priority || sp.bg_priority);
if bg_wins {
cgb_palette_lookup(bg_palette_ram, bw_px.palette_num, bw_px.colour_index)
} else {
cgb_palette_lookup(obj_palette_ram, sp.cgb_palette, sp.colour_index)
}
} else {
cgb_palette_lookup(bg_palette_ram, bw_px.palette_num, bw_px.colour_index)
};
screen_buffer.set_pixel(x, scanline as u32, r, g, b);
}
trace_ppu!(4; "render y={} sprites={} window={}",
scanline, sprite_indices.len(), window_active);
if window_active {
*window_line = window_line.wrapping_add(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_registers() -> Registers {
Registers::new()
}
fn blank_vram() -> [u8; 0x2000] {
[0u8; 0x2000]
}
fn blank_oam() -> [u8; 0xA0] {
[0u8; 0xA0]
}
#[test]
fn test_palette_index_0_maps_to_white_with_default_bgp() {
assert_eq!(palette_lookup(0xE4, 0), DMG_GREY[0]); }
#[test]
fn test_palette_index_3_maps_to_black_with_default_bgp() {
assert_eq!(palette_lookup(0xE4, 3), DMG_GREY[3]); }
#[test]
fn test_palette_inverted_bgp_maps_0_to_black() {
assert_eq!(palette_lookup(0x1B, 0), DMG_GREY[3]);
}
#[test]
fn test_palette_all_white_bgp_maps_every_index_to_white() {
for i in 0..4 {
assert_eq!(palette_lookup(0x00, i), DMG_GREY[0]);
}
}
#[test]
fn test_render_scanline_fills_160_pixels() {
let vram = blank_vram();
let oam = blank_oam();
let regs = default_registers();
let mut sb = ScreenBuffer::new();
let mut wl = 0u8;
render_scanline(0, &vram, &oam, ®s, &mut wl, &mut sb);
for x in 0..160 {
let (r, g, b) = sb.get_pixel(x, 0);
assert_eq!(r, g, "pixel ({x},0) r≠g");
assert_eq!(g, b, "pixel ({x},0) g≠b");
}
}
#[test]
fn test_render_scanline_with_inverted_bgp_paints_black() {
let vram = blank_vram();
let oam = blank_oam();
let mut regs = default_registers();
regs.bgp = 0xFF; let mut sb = ScreenBuffer::new();
let mut wl = 0u8;
render_scanline(0, &vram, &oam, ®s, &mut wl, &mut sb);
for x in 0..160 {
assert_eq!(
sb.get_pixel(x, 0),
(0x00, 0x00, 0x00),
"pixel ({x},0) should be black"
);
}
}
#[test]
fn test_render_scanline_bg_disabled_paints_white() {
let vram = blank_vram();
let oam = blank_oam();
let mut regs = default_registers();
regs.lcdc = 0x80; regs.bgp = 0xE4; let mut sb = ScreenBuffer::new();
let mut wl = 0u8;
render_scanline(0, &vram, &oam, ®s, &mut wl, &mut sb);
for x in 0..160 {
assert_eq!(
sb.get_pixel(x, 0),
(0xFF, 0xFF, 0xFF),
"pixel ({x},0) should be white when BG disabled"
);
}
}
#[test]
fn test_cgb_5bit_to_8bit_known_values() {
assert_eq!(cgb_5bit_to_8bit(0), 0);
assert_eq!(cgb_5bit_to_8bit(31), 255); assert_eq!(cgb_5bit_to_8bit(1), 8); assert_eq!(cgb_5bit_to_8bit(16), 132); }
#[test]
fn test_cgb_palette_lookup_decodes_5_5_5_le() {
let mut palette_ram = [0u8; 64];
palette_ram[2] = 0x1F; palette_ram[3] = 0x00; let (r, g, b) = cgb_palette_lookup(&palette_ram, 0, 1);
assert_eq!(r, 255, "R should be max");
assert_eq!(g, 0, "G should be 0");
assert_eq!(b, 0, "B should be 0");
}
fn blank_vram_bank1() -> [u8; 0x2000] {
[0u8; 0x2000]
}
fn blank_palette_ram() -> [u8; 64] {
[0u8; 64]
}
fn palette_ram_with_white_at(slot: usize) -> [u8; 64] {
let mut p = blank_palette_ram();
p[slot * 2] = 0xFF;
p[slot * 2 + 1] = 0x7F;
p
}
#[test]
fn test_cgb_master_priority_off_obj_always_wins() {
let mut vram = blank_vram();
let mut oam = blank_oam();
oam[0] = 16;
oam[1] = 8;
oam[2] = 0;
oam[3] = 0x80;
vram[0x0000] = 0xFF; let mut bank1_with_bgprio = blank_vram_bank1();
bank1_with_bgprio[0x1800] = 0x80;
let mut regs = default_registers();
regs.lcdc = 0x92u8;
let bg_palette = palette_ram_with_white_at(1); let obj_palette = blank_palette_ram();
let mut sb = ScreenBuffer::new();
let mut wl = 0u8;
render_scanline_cgb(
0,
&vram,
&bank1_with_bgprio,
&oam,
®s,
&bg_palette,
&obj_palette,
&mut wl,
false,
&mut sb,
);
assert_eq!(
sb.get_pixel(0, 0),
(0, 0, 0),
"OBJ (black) must win when master_priority=false"
);
}
#[test]
fn test_cgb_bg_priority_beats_obj_when_master_priority_set() {
let mut vram = blank_vram();
let mut oam = blank_oam();
oam[0] = 16;
oam[1] = 8;
oam[2] = 0;
oam[3] = 0x00; vram[0x0000] = 0xFF; let mut bank1 = blank_vram_bank1();
bank1[0x1800] = 0x80;
let mut regs = default_registers();
regs.lcdc = 0x93u8;
let bg_palette = palette_ram_with_white_at(1); let obj_palette = blank_palette_ram();
let mut sb = ScreenBuffer::new();
let mut wl = 0u8;
render_scanline_cgb(
0,
&vram,
&bank1,
&oam,
®s,
&bg_palette,
&obj_palette,
&mut wl,
false,
&mut sb,
);
assert_eq!(
sb.get_pixel(0, 0),
(255, 255, 255),
"BG (white) must win with master_priority=true and bg_priority"
);
}
}