use std::marker::PhantomData;
use crate::palette::{ColorIndex, GbColor, GbaColor, Palette, GRAYSCALE_PALETTE};
use dotzuki_engine::render::Rgba;
use dotzuki_engine::render_config::RenderConfig;
pub const SCREEN_WIDTH: usize = 160;
pub const SCREEN_HEIGHT: usize = 144;
pub const fn index_bits<C: ColorIndex>() -> usize {
let bits = C::MAX.ilog2();
if bits < 1 {
1
} else {
bits as usize
}
}
const fn groups_per_row(width: usize) -> usize {
(width + 7) / 8
}
pub const fn packed_len<C: ColorIndex>(width: usize, height: usize) -> usize {
height * groups_per_row(width) * index_bits::<C>()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexedFrameBuffer<C: ColorIndex = GbColor> {
data: Vec<u8>,
width: usize,
height: usize,
#[doc(hidden)]
_phantom: PhantomData<C>,
}
impl<C: ColorIndex> IndexedFrameBuffer<C> {
pub fn new(width: usize, height: usize, clear: C) -> Self {
let mut fb = Self {
data: vec![0; packed_len::<C>(width, height)],
width,
height,
_phantom: PhantomData,
};
fb.clear(clear);
fb
}
#[inline]
pub const fn width(&self) -> usize {
self.width
}
#[inline]
pub const fn height(&self) -> usize {
self.height
}
#[inline]
pub fn len(&self) -> usize {
self.width * self.height
}
#[inline]
pub fn is_empty(&self) -> bool {
self.width == 0 || self.height == 0
}
pub fn clear(&mut self, color: C) {
let value = color.to_index();
let bits = index_bits::<C>();
for (i, byte) in self.data.iter_mut().enumerate() {
let plane = i % bits;
*byte = if (value >> plane) & 1 == 1 { 0xFF } else { 0x00 };
}
}
pub fn set_pixel(&mut self, x: u32, y: u32, color: C) -> bool {
if x >= self.width as u32 || y >= self.height as u32 {
return false;
}
let value = color.to_index();
let bits = index_bits::<C>();
let group = (x as usize) / 8;
let bit = 7 - ((x as usize) % 8);
let base = ((y as usize) * groups_per_row(self.width) + group) * bits;
for plane in 0..bits {
let plane_bit = ((value >> plane) & 1) as u8;
let byte = &mut self.data[base + plane];
*byte = (*byte & !(1 << bit)) | (plane_bit << bit);
}
true
}
pub fn get_pixel(&self, x: u32, y: u32) -> Option<C> {
if x >= self.width as u32 || y >= self.height as u32 {
return None;
}
let bits = index_bits::<C>();
let group = (x as usize) / 8;
let bit = 7 - ((x as usize) % 8);
let base = ((y as usize) * groups_per_row(self.width) + group) * bits;
let mut value = 0usize;
for plane in 0..bits {
if (self.data[base + plane] >> bit) & 1 == 1 {
value |= 1 << plane;
}
}
Some(C::from_u8(value as u8))
}
pub fn fill_rect(&mut self, x: u32, y: u32, rect_width: u32, rect_height: u32, color: C) {
let x_start = (x as usize).min(self.width);
let y_start = (y as usize).min(self.height);
let x_end = (x.saturating_add(rect_width) as usize).min(self.width);
let y_end = (y.saturating_add(rect_height) as usize).min(self.height);
for row in y_start..y_end {
for col in x_start..x_end {
self.set_pixel(col as u32, row as u32, color);
}
}
}
pub fn to_rgba(&self, palette: &Palette<C>, out: &mut [u8]) -> bool {
let need = self.width * self.height * 4;
if out.len() < need {
return false;
}
let mut base = 0;
for y in 0..self.height {
for x in 0..self.width {
let index = self
.get_pixel(x as u32, y as u32)
.expect("pixel in bounds");
out[base..base + 4].copy_from_slice(&palette.color(index).to_array());
base += 4;
}
}
true
}
#[inline]
pub fn packed(&self) -> &[u8] {
&self.data
}
#[inline]
pub fn packed_mut(&mut self) -> &mut [u8] {
&mut self.data
}
}
impl<C: ColorIndex> Default for IndexedFrameBuffer<C> {
fn default() -> Self {
Self::new(SCREEN_WIDTH, SCREEN_HEIGHT, C::from_u8(0))
}
}
pub fn quantize<C: ColorIndex>(palette: &Palette<C>, color: Rgba) -> C {
let mut best = C::from_u8(0);
let mut best_dist = u32::MAX;
for i in 0..palette.count as usize {
let entry = palette.colors[i];
let dr = entry.r as i32 - color.r as i32;
let dg = entry.g as i32 - color.g as i32;
let db = entry.b as i32 - color.b as i32;
let da = entry.a as i32 - color.a as i32;
let dist = (dr * dr + dg * dg + db * db + da * da) as u32;
if dist < best_dist {
best_dist = dist;
best = C::from_u8(i as u8);
}
}
best
}
#[cfg(test)]
mod tests {
use super::*;
use crate::palette::{GbaColor, GRAYSCALE_PALETTE, GRAYSCALE_SPRITE_PALETTE};
use crate::tile::Tile;
#[test]
fn storage_sizes() {
assert_eq!(packed_len::<GbColor>(SCREEN_WIDTH, SCREEN_HEIGHT), 5760);
assert_eq!(packed_len::<GbColor>(160, 144), 5760);
assert_eq!(packed_len::<GbaColor>(160, 144), 11520);
assert_eq!(index_bits::<GbColor>(), 2);
assert_eq!(index_bits::<GbaColor>(), 4);
let fb = IndexedFrameBuffer::<GbColor>::new(160, 144, GbColor::White);
assert_eq!(fb.packed().len(), 5760);
let gba = IndexedFrameBuffer::<GbaColor>::new(160, 144, GbaColor(0));
assert_eq!(gba.packed().len(), 11520);
}
#[test]
fn default_is_screen_sized_cleared() {
let fb = IndexedFrameBuffer::<GbColor>::default();
assert_eq!(fb.width(), SCREEN_WIDTH);
assert_eq!(fb.height(), SCREEN_HEIGHT);
assert_eq!(fb.len(), 160 * 144);
assert_eq!(fb.get_pixel(0, 0), Some(GbColor::White));
assert_eq!(fb.get_pixel(159, 143), Some(GbColor::White));
let gba = IndexedFrameBuffer::<GbaColor>::default();
assert_eq!(gba.get_pixel(159, 143), Some(GbaColor(0)));
}
#[test]
fn packing_round_trip_gb() {
let mut fb = IndexedFrameBuffer::<GbColor>::new(16, 8, GbColor::White);
let pattern = [
GbColor::White,
GbColor::LightGray,
GbColor::DarkGray,
GbColor::Black,
];
for y in 0..8u32 {
for x in 0..16u32 {
fb.set_pixel(x, y, pattern[((x + y) as usize) % 4]);
}
}
for y in 0..8u32 {
for x in 0..16u32 {
assert_eq!(
fb.get_pixel(x, y),
Some(pattern[((x + y) as usize) % 4]),
"mismatch at ({x}, {y})"
);
}
}
}
#[test]
fn packing_round_trip_gba() {
let mut fb = IndexedFrameBuffer::<GbaColor>::new(8, 4, GbaColor(0));
for y in 0..4u32 {
for x in 0..8u32 {
fb.set_pixel(x, y, GbaColor(((x * 3 + y * 5) % 16) as u8));
}
}
for y in 0..4u32 {
for x in 0..8u32 {
assert_eq!(
fb.get_pixel(x, y),
Some(GbaColor(((x * 3 + y * 5) % 16) as u8))
);
}
}
}
#[test]
fn packing_round_trip_non_multiple_of_8() {
let mut fb = IndexedFrameBuffer::<GbColor>::new(10, 7, GbColor::White);
for y in 0..7u32 {
for x in 0..10u32 {
fb.set_pixel(x, y, GbColor::from_u8(((x + y) % 4) as u8));
}
}
for y in 0..7u32 {
for x in 0..10u32 {
assert_eq!(fb.get_pixel(x, y), Some(GbColor::from_u8(((x + y) % 4) as u8)));
}
}
assert_eq!(fb.get_pixel(10, 0), None);
assert_eq!(fb.get_pixel(0, 7), None);
}
#[test]
fn packed_layout_is_gb_vram_bitplanes() {
let mut fb = IndexedFrameBuffer::<GbColor>::new(8, 1, GbColor::White);
let row = [1u8, 0, 3, 0, 2, 0, 1, 0];
for (x, &v) in row.iter().enumerate() {
fb.set_pixel(x as u32, 0, GbColor::from_u8(v));
}
assert_eq!(fb.packed(), &[0xA2, 0x28]);
}
#[test]
fn packed_data_feeds_tile_decoder() {
let mut fb = IndexedFrameBuffer::<GbColor>::new(8, 8, GbColor::White);
for y in 0..8u32 {
for x in 0..8u32 {
fb.set_pixel(x, y, GbColor::from_u8(((x * y) % 4) as u8));
}
}
let tile = Tile::from_2bpp(fb.packed());
for y in 0..8 {
for x in 0..8 {
assert_eq!(tile.pixels[y][x], ((x * y) % 4) as u8);
}
}
}
#[test]
fn bounds_are_checked() {
let mut fb = IndexedFrameBuffer::<GbColor>::new(8, 4, GbColor::White);
assert!(fb.set_pixel(7, 3, GbColor::Black));
assert!(!fb.set_pixel(8, 0, GbColor::Black));
assert!(!fb.set_pixel(0, 4, GbColor::Black));
assert!(!fb.set_pixel(u32::MAX, 0, GbColor::Black));
assert_eq!(fb.get_pixel(8, 0), None);
assert_eq!(fb.get_pixel(0, 4), None);
assert_eq!(fb.get_pixel(7, 3), Some(GbColor::Black));
}
#[test]
fn clear_fills_every_pixel() {
let mut fb = IndexedFrameBuffer::<GbColor>::new(10, 7, GbColor::Black);
fb.fill_rect(0, 0, 10, 7, GbColor::LightGray);
assert_eq!(fb.get_pixel(5, 3), Some(GbColor::LightGray));
fb.clear(GbColor::Black);
for y in 0..7u32 {
for x in 0..10u32 {
assert_eq!(fb.get_pixel(x, y), Some(GbColor::Black));
}
}
assert_eq!(fb.packed(), &[0xFF; packed_len::<GbColor>(10, 7)]);
}
#[test]
fn fill_rect_clamps_to_bounds() {
let mut fb = IndexedFrameBuffer::<GbColor>::new(8, 8, GbColor::White);
fb.fill_rect(4, 4, 100, 100, GbColor::Black);
assert_eq!(fb.get_pixel(3, 3), Some(GbColor::White));
assert_eq!(fb.get_pixel(4, 3), Some(GbColor::White));
assert_eq!(fb.get_pixel(3, 4), Some(GbColor::White));
assert_eq!(fb.get_pixel(4, 4), Some(GbColor::Black));
assert_eq!(fb.get_pixel(7, 7), Some(GbColor::Black));
fb.fill_rect(8, 8, 4, 4, GbColor::DarkGray);
assert_eq!(fb.get_pixel(7, 7), Some(GbColor::Black));
}
#[test]
fn to_rgba_applies_palette() {
let mut fb = IndexedFrameBuffer::<GbColor>::new(4, 2, GbColor::White);
fb.set_pixel(0, 0, GbColor::Black);
fb.set_pixel(3, 1, GbColor::DarkGray);
let pal = GRAYSCALE_PALETTE;
let mut out = [0u8; 4 * 2 * 4];
assert!(fb.to_rgba(&pal, &mut out));
assert_eq!(&out[0..4], &Rgba::rgb(0x00, 0x00, 0x00).to_array());
assert_eq!(&out[1 * 4..2 * 4], &Rgba::rgb(0xFF, 0xFF, 0xFF).to_array());
assert_eq!(&out[(3 + 1 * 4) * 4..(3 + 1 * 4) * 4 + 4], &Rgba::rgb(0x55, 0x55, 0x55).to_array());
}
#[test]
fn to_rgba_rejects_short_slice() {
let fb = IndexedFrameBuffer::<GbColor>::new(4, 2, GbColor::White);
let mut out = [0u8; 4 * 2 * 4 - 1];
assert!(!fb.to_rgba(&GRAYSCALE_PALETTE, &mut out));
assert_eq!(out, [0u8; 4 * 2 * 4 - 1]); }
#[test]
fn quantize_exact_match() {
let pal = GRAYSCALE_PALETTE;
assert_eq!(quantize(&pal, Rgba::rgb(0xFF, 0xFF, 0xFF)), GbColor::White);
assert_eq!(quantize(&pal, Rgba::rgb(0xAA, 0xAA, 0xAA)), GbColor::LightGray);
assert_eq!(quantize(&pal, Rgba::rgb(0x55, 0x55, 0x55)), GbColor::DarkGray);
assert_eq!(quantize(&pal, Rgba::rgb(0x00, 0x00, 0x00)), GbColor::Black);
}
#[test]
fn quantize_picks_nearest() {
let pal = GRAYSCALE_PALETTE;
assert_eq!(quantize(&pal, Rgba::rgb(200, 200, 200)), GbColor::LightGray);
assert_eq!(quantize(&pal, Rgba::rgb(0x7F, 0x7F, 0x7F)), GbColor::DarkGray);
assert_eq!(quantize(&pal, Rgba::rgb(30, 30, 30)), GbColor::Black);
}
#[test]
fn quantize_alpha_aware() {
let pal = GRAYSCALE_SPRITE_PALETTE;
assert_eq!(pal.colors[0], Rgba::TRANSPARENT);
assert_eq!(quantize(&pal, Rgba::rgb(0x00, 0x00, 0x00)), GbColor::Black);
assert_eq!(quantize(&pal, Rgba::TRANSPARENT), GbColor::White);
}
#[test]
fn quantize_gba_palette() {
let mut colors = [Rgba::BLACK; 16];
colors[0] = Rgba::rgb(0xFF, 0x00, 0x00);
colors[1] = Rgba::rgb(0x00, 0xFF, 0x00);
colors[2] = Rgba::rgb(0x00, 0x00, 0xFF);
let pal = Palette::<GbaColor>::from_gba_palette(colors);
assert_eq!(quantize(&pal, Rgba::rgb(0xFF, 0x00, 0x00)), GbaColor(0));
assert_eq!(quantize(&pal, Rgba::rgb(0x00, 0xFF, 0x00)), GbaColor(1));
assert_eq!(quantize(&pal, Rgba::rgb(0x00, 0x00, 0xFF)), GbaColor(2));
assert_eq!(quantize(&pal, Rgba::rgb(0xC0, 0x40, 0x00)), GbaColor(0));
}
}
pub trait DefaultPalette: ColorIndex {
fn default_palette() -> Palette<Self>;
}
impl DefaultPalette for GbColor {
fn default_palette() -> Palette<Self> {
GRAYSCALE_PALETTE
}
}
impl DefaultPalette for GbaColor {
fn default_palette() -> Palette<Self> {
let mut colors = [Rgba::BLACK; 16];
for i in 0..16 {
let v = (255 - i * 17) as u8;
colors[i] = Rgba::rgb(v, v, v);
}
Palette::<GbaColor>::from_gba_palette(colors)
}
}
pub trait FbSurface: Sized {
fn new_screen(width: u32, height: u32) -> Self;
fn width(&self) -> u32;
fn height(&self) -> u32;
fn set_pixel(&mut self, x: u32, y: u32, color: Rgba) -> bool;
fn get_pixel(&self, x: u32, y: u32) -> Option<Rgba>;
fn clear(&mut self, color: Rgba);
fn fill_rect(&mut self, x: u32, y: u32, rect_width: u32, rect_height: u32, color: Rgba);
fn pixel_rgba(&self, x: u32, y: u32) -> Rgba {
self.get_pixel(x, y).unwrap_or(Rgba::TRANSPARENT)
}
fn present_into(&self, out: &mut [u8]);
}
impl FbSurface for dotzuki_engine::render::FrameBuffer {
fn new_screen(width: u32, height: u32) -> Self {
Self::new(RenderConfig::new(width, height), Rgba::BLACK)
}
fn width(&self) -> u32 {
self.width
}
fn height(&self) -> u32 {
self.height
}
fn set_pixel(&mut self, x: u32, y: u32, color: Rgba) -> bool {
Self::set_pixel(self, x, y, color)
}
fn get_pixel(&self, x: u32, y: u32) -> Option<Rgba> {
Self::get_pixel(self, x, y)
}
fn clear(&mut self, color: Rgba) {
Self::clear(self, color)
}
fn fill_rect(&mut self, x: u32, y: u32, rect_width: u32, rect_height: u32, color: Rgba) {
Self::fill_rect(self, x, y, rect_width, rect_height, color)
}
fn present_into(&self, out: &mut [u8]) {
assert!(out.len() >= self.data.len(), "present buffer too small");
out[..self.data.len()].copy_from_slice(&self.data);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RgbaIndexedFrameBuffer<C: ColorIndex = GbColor> {
buffer: IndexedFrameBuffer<C>,
base: Palette<C>,
pub palette: Palette<C>,
}
impl<C: ColorIndex> RgbaIndexedFrameBuffer<C> {
pub fn with_palette(config: RenderConfig, clear: Rgba, base: Palette<C>) -> Self {
let mut fb = Self {
buffer: IndexedFrameBuffer::new(
config.screen_width as usize,
config.screen_height as usize,
C::from_u8(0),
),
palette: base,
base,
};
fb.clear(clear);
fb
}
#[inline]
pub fn display_palette(&self) -> &Palette<C> {
&self.palette
}
pub fn set_palette(&mut self, palette: Palette<C>) {
self.palette = palette;
}
pub fn reset_palette(&mut self) {
self.palette = self.base;
}
pub fn remap_shades(&mut self, map: &[u8]) {
let count = self.palette.count as usize;
for i in 0..count {
let mapped = map.get(i).copied().unwrap_or(i as u8) as usize % count;
self.palette.colors[i] = self.base.colors[mapped];
}
self.palette.count = self.base.count;
}
pub fn scale_shades(&mut self, scale: f32) {
let scale = scale.clamp(0.0, 1.0);
for i in 0..self.palette.count as usize {
let c = self.base.colors[i];
self.palette.colors[i] = Rgba::new(
(c.r as f32 * scale) as u8,
(c.g as f32 * scale) as u8,
(c.b as f32 * scale) as u8,
c.a,
);
}
}
#[inline]
pub fn indexed(&self) -> &IndexedFrameBuffer<C> {
&self.buffer
}
#[inline]
pub fn indexed_mut(&mut self) -> &mut IndexedFrameBuffer<C> {
&mut self.buffer
}
#[inline]
pub fn packed(&self) -> &[u8] {
self.buffer.packed()
}
#[inline]
pub fn packed_mut(&mut self) -> &mut [u8] {
self.buffer.packed_mut()
}
pub fn to_rgba(&self, out: &mut [u8]) -> bool {
self.buffer.to_rgba(&self.palette, out)
}
pub fn copy_from(&mut self, other: &Self) {
self.buffer.packed_mut().copy_from_slice(other.buffer.packed());
self.palette = other.palette;
self.base = other.base;
}
pub fn set_pixel_index(&mut self, x: u32, y: u32, color: C) -> bool {
self.buffer.set_pixel(x, y, color)
}
pub fn get_index(&self, x: u32, y: u32) -> Option<C> {
self.buffer.get_pixel(x, y)
}
pub fn clear_index(&mut self, color: C) {
self.buffer.clear(color);
}
#[inline]
pub fn len(&self) -> usize {
self.buffer.len()
}
#[inline]
pub fn width(&self) -> u32 {
self.buffer.width() as u32
}
#[inline]
pub fn height(&self) -> u32 {
self.buffer.height() as u32
}
#[inline]
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
pub fn set_pixel(&mut self, x: u32, y: u32, color: Rgba) -> bool {
let index = quantize(&self.base, color);
self.buffer.set_pixel(x, y, index)
}
pub fn get_pixel(&self, x: u32, y: u32) -> Option<Rgba> {
self.buffer.get_pixel(x, y).map(|i| self.palette.color(i))
}
pub fn clear(&mut self, color: Rgba) {
let index = quantize(&self.base, color);
self.buffer.clear(index);
self.palette = self.base;
}
pub fn fill_rect(&mut self, x: u32, y: u32, rect_width: u32, rect_height: u32, color: Rgba) {
let index = quantize(&self.base, color);
self.buffer.fill_rect(x, y, rect_width, rect_height, index);
}
pub fn blit_row(&mut self, x: u32, y: u32, src: &[u8], count: u32) -> bool {
if y >= self.height() || x >= self.width() {
return false;
}
let actual_count = count.min(self.width() - x) as usize;
let src_bytes = actual_count * 4;
if src.len() < src_bytes {
return false;
}
for i in 0..actual_count {
let off = i * 4;
let c = Rgba::new(src[off], src[off + 1], src[off + 2], src[off + 3]);
self.buffer
.set_pixel(x + i as u32, y, quantize(&self.base, c));
}
true
}
#[cfg(any(feature = "gpu", feature = "image-assets"))]
pub fn save_png(&self, path: &std::path::Path) -> std::io::Result<()> {
use image::{ImageBuffer, Rgba as ImgRgba};
let w = self.width() as u32;
let h = self.height() as u32;
let mut rgba = vec![0u8; (w * h * 4) as usize];
self.to_rgba(&mut rgba);
let img: ImageBuffer<ImgRgba<u8>, _> =
ImageBuffer::from_raw(w, h, rgba).expect("framebuffer size mismatch");
img.save(path)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
}
}
impl<C: ColorIndex + DefaultPalette> RgbaIndexedFrameBuffer<C> {
pub fn new(config: RenderConfig, clear: Rgba) -> Self {
Self::with_palette(config, clear, C::default_palette())
}
}
impl RgbaIndexedFrameBuffer<GbColor> {
pub fn apply_bgp(&mut self, bgp: u8) {
let mut remapped = [0u8; 4];
for i in 0..4 {
remapped[i] = (bgp >> (2 * i)) & 3;
}
self.remap_shades(&remapped);
}
}
impl<C: ColorIndex + DefaultPalette> FbSurface for RgbaIndexedFrameBuffer<C> {
fn new_screen(width: u32, height: u32) -> Self {
Self::new(RenderConfig::new(width, height), Rgba::BLACK)
}
fn width(&self) -> u32 {
self.buffer.width() as u32
}
fn height(&self) -> u32 {
self.buffer.height() as u32
}
fn set_pixel(&mut self, x: u32, y: u32, color: Rgba) -> bool {
self.set_pixel(x, y, color)
}
fn get_pixel(&self, x: u32, y: u32) -> Option<Rgba> {
self.get_pixel(x, y)
}
fn clear(&mut self, color: Rgba) {
self.clear(color)
}
fn fill_rect(&mut self, x: u32, y: u32, rect_width: u32, rect_height: u32, color: Rgba) {
self.fill_rect(x, y, rect_width, rect_height, color)
}
fn present_into(&self, out: &mut [u8]) {
assert!(out.len() >= self.len() * 4, "present buffer too small");
self.to_rgba(out);
}
}
#[cfg(test)]
mod facade_tests {
use super::*;
use crate::palette::GRAYSCALE_SPRITE_PALETTE;
fn fb() -> RgbaIndexedFrameBuffer<GbColor> {
RgbaIndexedFrameBuffer::new(RenderConfig::new(160, 144), Rgba::WHITE)
}
#[test]
fn storage_is_packed() {
let fb = fb();
assert_eq!(fb.len(), 160 * 144);
assert_eq!(fb.packed().len(), 5760);
assert_eq!(fb.packed().len(), packed_len::<GbColor>(160, 144));
}
#[test]
fn grayscale_round_trips_exactly() {
let mut fb = fb();
let colors = [
Rgba::WHITE,
Rgba::rgb(0xAA, 0xAA, 0xAA),
Rgba::rgb(0x55, 0x55, 0x55),
Rgba::BLACK,
];
for (i, &c) in colors.iter().enumerate() {
assert!(fb.set_pixel(i as u32, 0, c));
}
for (i, &c) in colors.iter().enumerate() {
assert_eq!(fb.get_pixel(i as u32, 0), Some(c));
assert_eq!(
fb.get_index(i as u32, 0),
Some(GbColor::from_u8(i as u8))
);
}
}
#[test]
fn near_grays_quantize_to_nearest_shade() {
let mut fb = fb();
fb.set_pixel(0, 0, Rgba::rgb(0xC0, 0xC0, 0xC0));
fb.set_pixel(1, 0, Rgba::rgb(0x80, 0x80, 0x80));
fb.set_pixel(2, 0, Rgba::rgb(0x40, 0x40, 0x40));
assert_eq!(fb.get_index(0, 0), Some(GbColor::LightGray));
assert_eq!(fb.get_index(1, 0), Some(GbColor::LightGray));
assert_eq!(fb.get_index(2, 0), Some(GbColor::DarkGray));
}
#[test]
fn transparent_writes_pick_nearest_opaque_shade() {
let mut fb = fb();
fb.set_pixel(3, 3, Rgba::TRANSPARENT);
assert_eq!(fb.get_index(3, 3), Some(GbColor::Black));
}
#[test]
fn bounds_checked_rgba_facade() {
let mut fb = fb();
assert!(fb.set_pixel(159, 143, Rgba::BLACK));
assert!(!fb.set_pixel(160, 0, Rgba::BLACK));
assert!(!fb.set_pixel(0, 144, Rgba::BLACK));
assert_eq!(fb.get_pixel(160, 0), None);
assert_eq!(fb.pixel_rgba(160, 0), Rgba::TRANSPARENT);
}
#[test]
fn clear_and_fill_quantize() {
let mut fb = fb();
fb.fill_rect(0, 0, 100, 100, Rgba::rgb(0x55, 0x55, 0x55));
assert_eq!(fb.get_index(50, 50), Some(GbColor::DarkGray));
fb.clear(Rgba::BLACK);
assert_eq!(fb.get_index(0, 0), Some(GbColor::Black));
assert_eq!(fb.get_index(159, 143), Some(GbColor::Black));
}
#[test]
fn blit_row_quantizes_each_pixel() {
let mut fb = fb();
let row = [255u8, 255, 255, 255, 0, 0, 0, 0];
assert!(fb.blit_row(0, 0, &row, 2));
assert_eq!(fb.get_index(0, 0), Some(GbColor::White));
assert_eq!(fb.get_index(1, 0), Some(GbColor::Black));
assert!(!fb.blit_row(160, 0, &row, 2));
assert!(!fb.blit_row(0, 0, &row, 3)); }
#[test]
fn remap_shades_inverts() {
let mut fb = fb();
fb.fill_rect(0, 0, 8, 8, Rgba::BLACK);
fb.remap_shades(&[3, 2, 1, 0]);
assert_eq!(fb.get_pixel(0, 0), Some(Rgba::WHITE));
assert_eq!(fb.get_index(0, 0), Some(GbColor::Black));
fb.reset_palette();
assert_eq!(fb.get_pixel(0, 0), Some(Rgba::BLACK));
}
#[test]
fn apply_bgp_fade_to_black() {
let mut fb = fb();
fb.set_pixel(0, 0, Rgba::WHITE);
fb.apply_bgp(0b11111111);
assert_eq!(fb.get_pixel(0, 0), Some(Rgba::BLACK));
assert_eq!(fb.get_index(0, 0), Some(GbColor::White));
}
#[test]
fn scale_shades_dims_display() {
let mut fb = fb();
fb.fill_rect(0, 0, 8, 8, Rgba::WHITE);
fb.scale_shades(0.5);
assert_eq!(fb.get_pixel(0, 0), Some(Rgba::rgb(127, 127, 127)));
fb.scale_shades(0.0);
assert_eq!(fb.get_pixel(0, 0), Some(Rgba::BLACK));
}
#[test]
fn palette_swap_does_not_touch_draws() {
let mut fb = fb();
fb.set_pixel(4, 4, Rgba::rgb(0x55, 0x55, 0x55));
fb.apply_bgp(0b11100100); fb.set_pixel(5, 4, Rgba::rgb(0xAA, 0xAA, 0xAA));
fb.reset_palette();
assert_eq!(fb.get_pixel(5, 4), Some(Rgba::rgb(0xAA, 0xAA, 0xAA)));
}
#[test]
fn copy_from_copies_pixels_and_palette() {
let mut src = fb();
src.fill_rect(0, 0, 16, 16, Rgba::BLACK);
src.apply_bgp(0b00000000); let mut dst = fb();
dst.copy_from(&src);
assert_eq!(dst.get_index(8, 8), Some(GbColor::Black));
assert_eq!(dst.get_pixel(8, 8), Some(Rgba::WHITE));
assert_eq!(dst.packed(), src.packed());
}
#[test]
fn clear_resets_display_palette() {
let mut fb = fb();
fb.apply_bgp(0b00000000); assert_eq!(fb.get_pixel(0, 0), Some(Rgba::WHITE));
fb.clear(Rgba::BLACK);
assert_eq!(fb.get_pixel(0, 0), Some(Rgba::BLACK));
fb.set_pixel(1, 0, Rgba::WHITE);
assert_eq!(fb.get_pixel(1, 0), Some(Rgba::WHITE));
}
#[test]
fn to_rgba_uses_display_palette() {
let mut fb = RgbaIndexedFrameBuffer::<GbColor>::new(RenderConfig::new(2, 1), Rgba::WHITE);
fb.set_pixel(0, 0, Rgba::WHITE);
fb.apply_bgp(0b00000000); let mut out = [0u8; 8];
assert!(fb.to_rgba(&mut out));
assert_eq!(&out[0..4], &[0xFF, 0xFF, 0xFF, 0xFF]);
}
#[test]
fn fb_surface_present_and_pixels() {
let mut fb = RgbaIndexedFrameBuffer::<GbColor>::new_screen(4, 2);
fb.set_pixel(1, 1, Rgba::WHITE);
assert_eq!(fb.width(), 4);
assert_eq!(fb.height(), 2);
assert_eq!(fb.pixel_rgba(1, 1), Rgba::WHITE);
assert_eq!(fb.pixel_rgba(0, 0), Rgba::BLACK);
let mut out = [0u8; 4 * 2 * 4];
fb.present_into(&mut out);
assert_eq!(&out[5 * 4..6 * 4], &[0xFF, 0xFF, 0xFF, 0xFF]);
}
#[test]
fn sprite_palette_quantization_matches_draw_palette() {
let mut fb = fb();
for (i, &c) in GRAYSCALE_SPRITE_PALETTE.colors[..4].iter().enumerate() {
fb.set_pixel(i as u32, 0, c);
let expected = if i == 0 { GbColor::Black } else { GbColor::from_u8(i as u8) };
assert_eq!(fb.get_index(i as u32, 0), Some(expected));
}
}
}