1use std::marker::PhantomData;
44
45use crate::palette::{ColorIndex, GbColor, GbaColor, Palette, GRAYSCALE_PALETTE};
46use dotzuki_engine::render::Rgba;
47use dotzuki_engine::render_config::RenderConfig;
48
49pub const SCREEN_WIDTH: usize = 160;
55pub const SCREEN_HEIGHT: usize = 144;
57
58pub const fn index_bits<C: ColorIndex>() -> usize {
63 let bits = C::MAX.ilog2();
64 if bits < 1 {
65 1
66 } else {
67 bits as usize
68 }
69}
70
71const fn groups_per_row(width: usize) -> usize {
76 (width + 7) / 8
77}
78
79pub const fn packed_len<C: ColorIndex>(width: usize, height: usize) -> usize {
84 height * groups_per_row(width) * index_bits::<C>()
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct IndexedFrameBuffer<C: ColorIndex = GbColor> {
100 data: Vec<u8>,
102 width: usize,
104 height: usize,
106 #[doc(hidden)]
107 _phantom: PhantomData<C>,
108}
109
110impl<C: ColorIndex> IndexedFrameBuffer<C> {
111 pub fn new(width: usize, height: usize, clear: C) -> Self {
113 let mut fb = Self {
114 data: vec![0; packed_len::<C>(width, height)],
115 width,
116 height,
117 _phantom: PhantomData,
118 };
119 fb.clear(clear);
120 fb
121 }
122
123 #[inline]
125 pub const fn width(&self) -> usize {
126 self.width
127 }
128
129 #[inline]
131 pub const fn height(&self) -> usize {
132 self.height
133 }
134
135 #[inline]
137 pub fn len(&self) -> usize {
138 self.width * self.height
139 }
140
141 #[inline]
143 pub fn is_empty(&self) -> bool {
144 self.width == 0 || self.height == 0
145 }
146
147 pub fn clear(&mut self, color: C) {
149 let value = color.to_index();
150 let bits = index_bits::<C>();
151 for (i, byte) in self.data.iter_mut().enumerate() {
155 let plane = i % bits;
156 *byte = if (value >> plane) & 1 == 1 { 0xFF } else { 0x00 };
157 }
158 }
159
160 pub fn set_pixel(&mut self, x: u32, y: u32, color: C) -> bool {
162 if x >= self.width as u32 || y >= self.height as u32 {
163 return false;
164 }
165 let value = color.to_index();
166 let bits = index_bits::<C>();
167 let group = (x as usize) / 8;
168 let bit = 7 - ((x as usize) % 8);
169 let base = ((y as usize) * groups_per_row(self.width) + group) * bits;
170 for plane in 0..bits {
171 let plane_bit = ((value >> plane) & 1) as u8;
172 let byte = &mut self.data[base + plane];
173 *byte = (*byte & !(1 << bit)) | (plane_bit << bit);
174 }
175 true
176 }
177
178 pub fn get_pixel(&self, x: u32, y: u32) -> Option<C> {
180 if x >= self.width as u32 || y >= self.height as u32 {
181 return None;
182 }
183 let bits = index_bits::<C>();
184 let group = (x as usize) / 8;
185 let bit = 7 - ((x as usize) % 8);
186 let base = ((y as usize) * groups_per_row(self.width) + group) * bits;
187 let mut value = 0usize;
188 for plane in 0..bits {
189 if (self.data[base + plane] >> bit) & 1 == 1 {
190 value |= 1 << plane;
191 }
192 }
193 Some(C::from_u8(value as u8))
194 }
195
196 pub fn fill_rect(&mut self, x: u32, y: u32, rect_width: u32, rect_height: u32, color: C) {
199 let x_start = (x as usize).min(self.width);
200 let y_start = (y as usize).min(self.height);
201 let x_end = (x.saturating_add(rect_width) as usize).min(self.width);
202 let y_end = (y.saturating_add(rect_height) as usize).min(self.height);
203 for row in y_start..y_end {
204 for col in x_start..x_end {
205 self.set_pixel(col as u32, row as u32, color);
206 }
207 }
208 }
209
210 pub fn to_rgba(&self, palette: &Palette<C>, out: &mut [u8]) -> bool {
217 let need = self.width * self.height * 4;
218 if out.len() < need {
219 return false;
220 }
221 let mut base = 0;
222 for y in 0..self.height {
223 for x in 0..self.width {
224 let index = self
225 .get_pixel(x as u32, y as u32)
226 .expect("pixel in bounds");
227 out[base..base + 4].copy_from_slice(&palette.color(index).to_array());
228 base += 4;
229 }
230 }
231 true
232 }
233
234 #[inline]
239 pub fn packed(&self) -> &[u8] {
240 &self.data
241 }
242
243 #[inline]
247 pub fn packed_mut(&mut self) -> &mut [u8] {
248 &mut self.data
249 }
250}
251
252impl<C: ColorIndex> Default for IndexedFrameBuffer<C> {
255 fn default() -> Self {
256 Self::new(SCREEN_WIDTH, SCREEN_HEIGHT, C::from_u8(0))
257 }
258}
259
260pub fn quantize<C: ColorIndex>(palette: &Palette<C>, color: Rgba) -> C {
271 let mut best = C::from_u8(0);
272 let mut best_dist = u32::MAX;
273 for i in 0..palette.count as usize {
274 let entry = palette.colors[i];
275 let dr = entry.r as i32 - color.r as i32;
276 let dg = entry.g as i32 - color.g as i32;
277 let db = entry.b as i32 - color.b as i32;
278 let da = entry.a as i32 - color.a as i32;
279 let dist = (dr * dr + dg * dg + db * db + da * da) as u32;
280 if dist < best_dist {
281 best_dist = dist;
282 best = C::from_u8(i as u8);
283 }
284 }
285 best
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::palette::{GbaColor, GRAYSCALE_PALETTE, GRAYSCALE_SPRITE_PALETTE};
292 use crate::tile::Tile;
293
294 #[test]
295 fn storage_sizes() {
296 assert_eq!(packed_len::<GbColor>(SCREEN_WIDTH, SCREEN_HEIGHT), 5760);
298 assert_eq!(packed_len::<GbColor>(160, 144), 5760);
299 assert_eq!(packed_len::<GbaColor>(160, 144), 11520);
301 assert_eq!(index_bits::<GbColor>(), 2);
302 assert_eq!(index_bits::<GbaColor>(), 4);
303 let fb = IndexedFrameBuffer::<GbColor>::new(160, 144, GbColor::White);
305 assert_eq!(fb.packed().len(), 5760);
306 let gba = IndexedFrameBuffer::<GbaColor>::new(160, 144, GbaColor(0));
307 assert_eq!(gba.packed().len(), 11520);
308 }
309
310 #[test]
311 fn default_is_screen_sized_cleared() {
312 let fb = IndexedFrameBuffer::<GbColor>::default();
313 assert_eq!(fb.width(), SCREEN_WIDTH);
314 assert_eq!(fb.height(), SCREEN_HEIGHT);
315 assert_eq!(fb.len(), 160 * 144);
316 assert_eq!(fb.get_pixel(0, 0), Some(GbColor::White));
317 assert_eq!(fb.get_pixel(159, 143), Some(GbColor::White));
318 let gba = IndexedFrameBuffer::<GbaColor>::default();
319 assert_eq!(gba.get_pixel(159, 143), Some(GbaColor(0)));
320 }
321
322 #[test]
323 fn packing_round_trip_gb() {
324 let mut fb = IndexedFrameBuffer::<GbColor>::new(16, 8, GbColor::White);
325 let pattern = [
326 GbColor::White,
327 GbColor::LightGray,
328 GbColor::DarkGray,
329 GbColor::Black,
330 ];
331 for y in 0..8u32 {
332 for x in 0..16u32 {
333 fb.set_pixel(x, y, pattern[((x + y) as usize) % 4]);
334 }
335 }
336 for y in 0..8u32 {
337 for x in 0..16u32 {
338 assert_eq!(
339 fb.get_pixel(x, y),
340 Some(pattern[((x + y) as usize) % 4]),
341 "mismatch at ({x}, {y})"
342 );
343 }
344 }
345 }
346
347 #[test]
348 fn packing_round_trip_gba() {
349 let mut fb = IndexedFrameBuffer::<GbaColor>::new(8, 4, GbaColor(0));
350 for y in 0..4u32 {
351 for x in 0..8u32 {
352 fb.set_pixel(x, y, GbaColor(((x * 3 + y * 5) % 16) as u8));
353 }
354 }
355 for y in 0..4u32 {
356 for x in 0..8u32 {
357 assert_eq!(
358 fb.get_pixel(x, y),
359 Some(GbaColor(((x * 3 + y * 5) % 16) as u8))
360 );
361 }
362 }
363 }
364
365 #[test]
366 fn packing_round_trip_non_multiple_of_8() {
367 let mut fb = IndexedFrameBuffer::<GbColor>::new(10, 7, GbColor::White);
370 for y in 0..7u32 {
371 for x in 0..10u32 {
372 fb.set_pixel(x, y, GbColor::from_u8(((x + y) % 4) as u8));
373 }
374 }
375 for y in 0..7u32 {
376 for x in 0..10u32 {
377 assert_eq!(fb.get_pixel(x, y), Some(GbColor::from_u8(((x + y) % 4) as u8)));
378 }
379 }
380 assert_eq!(fb.get_pixel(10, 0), None);
383 assert_eq!(fb.get_pixel(0, 7), None);
384 }
385
386 #[test]
387 fn packed_layout_is_gb_vram_bitplanes() {
388 let mut fb = IndexedFrameBuffer::<GbColor>::new(8, 1, GbColor::White);
391 let row = [1u8, 0, 3, 0, 2, 0, 1, 0];
392 for (x, &v) in row.iter().enumerate() {
393 fb.set_pixel(x as u32, 0, GbColor::from_u8(v));
394 }
395 assert_eq!(fb.packed(), &[0xA2, 0x28]);
396 }
397
398 #[test]
399 fn packed_data_feeds_tile_decoder() {
400 let mut fb = IndexedFrameBuffer::<GbColor>::new(8, 8, GbColor::White);
403 for y in 0..8u32 {
404 for x in 0..8u32 {
405 fb.set_pixel(x, y, GbColor::from_u8(((x * y) % 4) as u8));
406 }
407 }
408 let tile = Tile::from_2bpp(fb.packed());
409 for y in 0..8 {
410 for x in 0..8 {
411 assert_eq!(tile.pixels[y][x], ((x * y) % 4) as u8);
412 }
413 }
414 }
415
416 #[test]
417 fn bounds_are_checked() {
418 let mut fb = IndexedFrameBuffer::<GbColor>::new(8, 4, GbColor::White);
419 assert!(fb.set_pixel(7, 3, GbColor::Black));
420 assert!(!fb.set_pixel(8, 0, GbColor::Black));
421 assert!(!fb.set_pixel(0, 4, GbColor::Black));
422 assert!(!fb.set_pixel(u32::MAX, 0, GbColor::Black));
423 assert_eq!(fb.get_pixel(8, 0), None);
424 assert_eq!(fb.get_pixel(0, 4), None);
425 assert_eq!(fb.get_pixel(7, 3), Some(GbColor::Black));
426 }
427
428 #[test]
429 fn clear_fills_every_pixel() {
430 let mut fb = IndexedFrameBuffer::<GbColor>::new(10, 7, GbColor::Black);
431 fb.fill_rect(0, 0, 10, 7, GbColor::LightGray);
433 assert_eq!(fb.get_pixel(5, 3), Some(GbColor::LightGray));
434 fb.clear(GbColor::Black);
435 for y in 0..7u32 {
436 for x in 0..10u32 {
437 assert_eq!(fb.get_pixel(x, y), Some(GbColor::Black));
438 }
439 }
440 assert_eq!(fb.packed(), &[0xFF; packed_len::<GbColor>(10, 7)]);
441 }
442
443 #[test]
444 fn fill_rect_clamps_to_bounds() {
445 let mut fb = IndexedFrameBuffer::<GbColor>::new(8, 8, GbColor::White);
446 fb.fill_rect(4, 4, 100, 100, GbColor::Black);
448 assert_eq!(fb.get_pixel(3, 3), Some(GbColor::White));
449 assert_eq!(fb.get_pixel(4, 3), Some(GbColor::White));
450 assert_eq!(fb.get_pixel(3, 4), Some(GbColor::White));
451 assert_eq!(fb.get_pixel(4, 4), Some(GbColor::Black));
452 assert_eq!(fb.get_pixel(7, 7), Some(GbColor::Black));
453 fb.fill_rect(8, 8, 4, 4, GbColor::DarkGray);
455 assert_eq!(fb.get_pixel(7, 7), Some(GbColor::Black));
456 }
457
458 #[test]
459 fn to_rgba_applies_palette() {
460 let mut fb = IndexedFrameBuffer::<GbColor>::new(4, 2, GbColor::White);
461 fb.set_pixel(0, 0, GbColor::Black);
462 fb.set_pixel(3, 1, GbColor::DarkGray);
463 let pal = GRAYSCALE_PALETTE;
464 let mut out = [0u8; 4 * 2 * 4];
465 assert!(fb.to_rgba(&pal, &mut out));
466 assert_eq!(&out[0..4], &Rgba::rgb(0x00, 0x00, 0x00).to_array());
467 assert_eq!(&out[1 * 4..2 * 4], &Rgba::rgb(0xFF, 0xFF, 0xFF).to_array());
468 assert_eq!(&out[(3 + 1 * 4) * 4..(3 + 1 * 4) * 4 + 4], &Rgba::rgb(0x55, 0x55, 0x55).to_array());
469 }
470
471 #[test]
472 fn to_rgba_rejects_short_slice() {
473 let fb = IndexedFrameBuffer::<GbColor>::new(4, 2, GbColor::White);
474 let mut out = [0u8; 4 * 2 * 4 - 1];
475 assert!(!fb.to_rgba(&GRAYSCALE_PALETTE, &mut out));
476 assert_eq!(out, [0u8; 4 * 2 * 4 - 1]); }
478
479 #[test]
480 fn quantize_exact_match() {
481 let pal = GRAYSCALE_PALETTE;
482 assert_eq!(quantize(&pal, Rgba::rgb(0xFF, 0xFF, 0xFF)), GbColor::White);
483 assert_eq!(quantize(&pal, Rgba::rgb(0xAA, 0xAA, 0xAA)), GbColor::LightGray);
484 assert_eq!(quantize(&pal, Rgba::rgb(0x55, 0x55, 0x55)), GbColor::DarkGray);
485 assert_eq!(quantize(&pal, Rgba::rgb(0x00, 0x00, 0x00)), GbColor::Black);
486 }
487
488 #[test]
489 fn quantize_picks_nearest() {
490 let pal = GRAYSCALE_PALETTE;
492 assert_eq!(quantize(&pal, Rgba::rgb(200, 200, 200)), GbColor::LightGray);
493 assert_eq!(quantize(&pal, Rgba::rgb(0x7F, 0x7F, 0x7F)), GbColor::DarkGray);
495 assert_eq!(quantize(&pal, Rgba::rgb(30, 30, 30)), GbColor::Black);
497 }
498
499 #[test]
500 fn quantize_alpha_aware() {
501 let pal = GRAYSCALE_SPRITE_PALETTE;
504 assert_eq!(pal.colors[0], Rgba::TRANSPARENT);
505 assert_eq!(quantize(&pal, Rgba::rgb(0x00, 0x00, 0x00)), GbColor::Black);
506 assert_eq!(quantize(&pal, Rgba::TRANSPARENT), GbColor::White);
508 }
509
510 #[test]
511 fn quantize_gba_palette() {
512 let mut colors = [Rgba::BLACK; 16];
513 colors[0] = Rgba::rgb(0xFF, 0x00, 0x00);
514 colors[1] = Rgba::rgb(0x00, 0xFF, 0x00);
515 colors[2] = Rgba::rgb(0x00, 0x00, 0xFF);
516 let pal = Palette::<GbaColor>::from_gba_palette(colors);
517 assert_eq!(quantize(&pal, Rgba::rgb(0xFF, 0x00, 0x00)), GbaColor(0));
518 assert_eq!(quantize(&pal, Rgba::rgb(0x00, 0xFF, 0x00)), GbaColor(1));
519 assert_eq!(quantize(&pal, Rgba::rgb(0x00, 0x00, 0xFF)), GbaColor(2));
520 assert_eq!(quantize(&pal, Rgba::rgb(0xC0, 0x40, 0x00)), GbaColor(0));
522 }
523}
524
525pub trait DefaultPalette: ColorIndex {
532 fn default_palette() -> Palette<Self>;
534}
535
536impl DefaultPalette for GbColor {
537 fn default_palette() -> Palette<Self> {
538 GRAYSCALE_PALETTE
541 }
542}
543
544impl DefaultPalette for GbaColor {
545 fn default_palette() -> Palette<Self> {
546 let mut colors = [Rgba::BLACK; 16];
547 for i in 0..16 {
548 let v = (255 - i * 17) as u8;
549 colors[i] = Rgba::rgb(v, v, v);
550 }
551 Palette::<GbaColor>::from_gba_palette(colors)
552 }
553}
554
555pub trait FbSurface: Sized {
569 fn new_screen(width: u32, height: u32) -> Self;
571 fn width(&self) -> u32;
573 fn height(&self) -> u32;
575 fn set_pixel(&mut self, x: u32, y: u32, color: Rgba) -> bool;
577 fn get_pixel(&self, x: u32, y: u32) -> Option<Rgba>;
579 fn clear(&mut self, color: Rgba);
581 fn fill_rect(&mut self, x: u32, y: u32, rect_width: u32, rect_height: u32, color: Rgba);
583 fn pixel_rgba(&self, x: u32, y: u32) -> Rgba {
585 self.get_pixel(x, y).unwrap_or(Rgba::TRANSPARENT)
586 }
587 fn present_into(&self, out: &mut [u8]);
590}
591
592impl FbSurface for dotzuki_engine::render::FrameBuffer {
593 fn new_screen(width: u32, height: u32) -> Self {
594 Self::new(RenderConfig::new(width, height), Rgba::BLACK)
595 }
596 fn width(&self) -> u32 {
597 self.width
598 }
599 fn height(&self) -> u32 {
600 self.height
601 }
602 fn set_pixel(&mut self, x: u32, y: u32, color: Rgba) -> bool {
603 Self::set_pixel(self, x, y, color)
604 }
605 fn get_pixel(&self, x: u32, y: u32) -> Option<Rgba> {
606 Self::get_pixel(self, x, y)
607 }
608 fn clear(&mut self, color: Rgba) {
609 Self::clear(self, color)
610 }
611 fn fill_rect(&mut self, x: u32, y: u32, rect_width: u32, rect_height: u32, color: Rgba) {
612 Self::fill_rect(self, x, y, rect_width, rect_height, color)
613 }
614 fn present_into(&self, out: &mut [u8]) {
615 assert!(out.len() >= self.data.len(), "present buffer too small");
616 out[..self.data.len()].copy_from_slice(&self.data);
617 }
618}
619
620#[derive(Debug, Clone, PartialEq, Eq)]
639pub struct RgbaIndexedFrameBuffer<C: ColorIndex = GbColor> {
640 buffer: IndexedFrameBuffer<C>,
642 base: Palette<C>,
644 pub palette: Palette<C>,
646}
647
648impl<C: ColorIndex> RgbaIndexedFrameBuffer<C> {
649 pub fn with_palette(config: RenderConfig, clear: Rgba, base: Palette<C>) -> Self {
652 let mut fb = Self {
653 buffer: IndexedFrameBuffer::new(
654 config.screen_width as usize,
655 config.screen_height as usize,
656 C::from_u8(0),
657 ),
658 palette: base,
659 base,
660 };
661 fb.clear(clear);
662 fb
663 }
664
665 #[inline]
667 pub fn display_palette(&self) -> &Palette<C> {
668 &self.palette
669 }
670
671 pub fn set_palette(&mut self, palette: Palette<C>) {
673 self.palette = palette;
674 }
675
676 pub fn reset_palette(&mut self) {
678 self.palette = self.base;
679 }
680
681 pub fn remap_shades(&mut self, map: &[u8]) {
685 let count = self.palette.count as usize;
686 for i in 0..count {
687 let mapped = map.get(i).copied().unwrap_or(i as u8) as usize % count;
688 self.palette.colors[i] = self.base.colors[mapped];
689 }
690 self.palette.count = self.base.count;
691 }
692
693 pub fn scale_shades(&mut self, scale: f32) {
697 let scale = scale.clamp(0.0, 1.0);
698 for i in 0..self.palette.count as usize {
699 let c = self.base.colors[i];
700 self.palette.colors[i] = Rgba::new(
701 (c.r as f32 * scale) as u8,
702 (c.g as f32 * scale) as u8,
703 (c.b as f32 * scale) as u8,
704 c.a,
705 );
706 }
707 }
708
709 #[inline]
711 pub fn indexed(&self) -> &IndexedFrameBuffer<C> {
712 &self.buffer
713 }
714
715 #[inline]
717 pub fn indexed_mut(&mut self) -> &mut IndexedFrameBuffer<C> {
718 &mut self.buffer
719 }
720
721 #[inline]
723 pub fn packed(&self) -> &[u8] {
724 self.buffer.packed()
725 }
726
727 #[inline]
729 pub fn packed_mut(&mut self) -> &mut [u8] {
730 self.buffer.packed_mut()
731 }
732
733 pub fn to_rgba(&self, out: &mut [u8]) -> bool {
737 self.buffer.to_rgba(&self.palette, out)
738 }
739
740 pub fn copy_from(&mut self, other: &Self) {
743 self.buffer.packed_mut().copy_from_slice(other.buffer.packed());
744 self.palette = other.palette;
745 self.base = other.base;
746 }
747
748 pub fn set_pixel_index(&mut self, x: u32, y: u32, color: C) -> bool {
750 self.buffer.set_pixel(x, y, color)
751 }
752
753 pub fn get_index(&self, x: u32, y: u32) -> Option<C> {
755 self.buffer.get_pixel(x, y)
756 }
757
758 pub fn clear_index(&mut self, color: C) {
760 self.buffer.clear(color);
761 }
762
763 #[inline]
765 pub fn len(&self) -> usize {
766 self.buffer.len()
767 }
768
769 #[inline]
771 pub fn width(&self) -> u32 {
772 self.buffer.width() as u32
773 }
774
775 #[inline]
777 pub fn height(&self) -> u32 {
778 self.buffer.height() as u32
779 }
780
781 #[inline]
783 pub fn is_empty(&self) -> bool {
784 self.buffer.is_empty()
785 }
786
787 pub fn set_pixel(&mut self, x: u32, y: u32, color: Rgba) -> bool {
790 let index = quantize(&self.base, color);
791 self.buffer.set_pixel(x, y, index)
792 }
793
794 pub fn get_pixel(&self, x: u32, y: u32) -> Option<Rgba> {
797 self.buffer.get_pixel(x, y).map(|i| self.palette.color(i))
798 }
799
800 pub fn clear(&mut self, color: Rgba) {
810 let index = quantize(&self.base, color);
811 self.buffer.clear(index);
812 self.palette = self.base;
813 }
814
815 pub fn fill_rect(&mut self, x: u32, y: u32, rect_width: u32, rect_height: u32, color: Rgba) {
818 let index = quantize(&self.base, color);
819 self.buffer.fill_rect(x, y, rect_width, rect_height, index);
820 }
821
822 pub fn blit_row(&mut self, x: u32, y: u32, src: &[u8], count: u32) -> bool {
826 if y >= self.height() || x >= self.width() {
827 return false;
828 }
829 let actual_count = count.min(self.width() - x) as usize;
830 let src_bytes = actual_count * 4;
831 if src.len() < src_bytes {
832 return false;
833 }
834 for i in 0..actual_count {
835 let off = i * 4;
836 let c = Rgba::new(src[off], src[off + 1], src[off + 2], src[off + 3]);
837 self.buffer
838 .set_pixel(x + i as u32, y, quantize(&self.base, c));
839 }
840 true
841 }
842
843 #[cfg(any(feature = "gpu", feature = "image-assets"))]
845 pub fn save_png(&self, path: &std::path::Path) -> std::io::Result<()> {
846 use image::{ImageBuffer, Rgba as ImgRgba};
847 let w = self.width() as u32;
848 let h = self.height() as u32;
849 let mut rgba = vec![0u8; (w * h * 4) as usize];
850 self.to_rgba(&mut rgba);
851 let img: ImageBuffer<ImgRgba<u8>, _> =
852 ImageBuffer::from_raw(w, h, rgba).expect("framebuffer size mismatch");
853 img.save(path)
854 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
855 }
856}
857
858impl<C: ColorIndex + DefaultPalette> RgbaIndexedFrameBuffer<C> {
859 pub fn new(config: RenderConfig, clear: Rgba) -> Self {
862 Self::with_palette(config, clear, C::default_palette())
863 }
864}
865
866impl RgbaIndexedFrameBuffer<GbColor> {
867 pub fn apply_bgp(&mut self, bgp: u8) {
871 let mut remapped = [0u8; 4];
872 for i in 0..4 {
873 remapped[i] = (bgp >> (2 * i)) & 3;
874 }
875 self.remap_shades(&remapped);
876 }
877}
878
879impl<C: ColorIndex + DefaultPalette> FbSurface for RgbaIndexedFrameBuffer<C> {
880 fn new_screen(width: u32, height: u32) -> Self {
881 Self::new(RenderConfig::new(width, height), Rgba::BLACK)
882 }
883 fn width(&self) -> u32 {
884 self.buffer.width() as u32
885 }
886 fn height(&self) -> u32 {
887 self.buffer.height() as u32
888 }
889 fn set_pixel(&mut self, x: u32, y: u32, color: Rgba) -> bool {
890 self.set_pixel(x, y, color)
891 }
892 fn get_pixel(&self, x: u32, y: u32) -> Option<Rgba> {
893 self.get_pixel(x, y)
894 }
895 fn clear(&mut self, color: Rgba) {
896 self.clear(color)
897 }
898 fn fill_rect(&mut self, x: u32, y: u32, rect_width: u32, rect_height: u32, color: Rgba) {
899 self.fill_rect(x, y, rect_width, rect_height, color)
900 }
901 fn present_into(&self, out: &mut [u8]) {
902 assert!(out.len() >= self.len() * 4, "present buffer too small");
903 self.to_rgba(out);
904 }
905}
906
907#[cfg(test)]
908mod facade_tests {
909 use super::*;
910 use crate::palette::GRAYSCALE_SPRITE_PALETTE;
911
912 fn fb() -> RgbaIndexedFrameBuffer<GbColor> {
913 RgbaIndexedFrameBuffer::new(RenderConfig::new(160, 144), Rgba::WHITE)
914 }
915
916 #[test]
917 fn storage_is_packed() {
918 let fb = fb();
919 assert_eq!(fb.len(), 160 * 144);
920 assert_eq!(fb.packed().len(), 5760);
921 assert_eq!(fb.packed().len(), packed_len::<GbColor>(160, 144));
922 }
923
924 #[test]
925 fn grayscale_round_trips_exactly() {
926 let mut fb = fb();
927 let colors = [
928 Rgba::WHITE,
929 Rgba::rgb(0xAA, 0xAA, 0xAA),
930 Rgba::rgb(0x55, 0x55, 0x55),
931 Rgba::BLACK,
932 ];
933 for (i, &c) in colors.iter().enumerate() {
934 assert!(fb.set_pixel(i as u32, 0, c));
935 }
936 for (i, &c) in colors.iter().enumerate() {
937 assert_eq!(fb.get_pixel(i as u32, 0), Some(c));
938 assert_eq!(
939 fb.get_index(i as u32, 0),
940 Some(GbColor::from_u8(i as u8))
941 );
942 }
943 }
944
945 #[test]
946 fn near_grays_quantize_to_nearest_shade() {
947 let mut fb = fb();
948 fb.set_pixel(0, 0, Rgba::rgb(0xC0, 0xC0, 0xC0));
950 fb.set_pixel(1, 0, Rgba::rgb(0x80, 0x80, 0x80));
951 fb.set_pixel(2, 0, Rgba::rgb(0x40, 0x40, 0x40));
952 assert_eq!(fb.get_index(0, 0), Some(GbColor::LightGray));
953 assert_eq!(fb.get_index(1, 0), Some(GbColor::LightGray));
954 assert_eq!(fb.get_index(2, 0), Some(GbColor::DarkGray));
955 }
956
957 #[test]
958 fn transparent_writes_pick_nearest_opaque_shade() {
959 let mut fb = fb();
964 fb.set_pixel(3, 3, Rgba::TRANSPARENT);
965 assert_eq!(fb.get_index(3, 3), Some(GbColor::Black));
966 }
967
968 #[test]
969 fn bounds_checked_rgba_facade() {
970 let mut fb = fb();
971 assert!(fb.set_pixel(159, 143, Rgba::BLACK));
972 assert!(!fb.set_pixel(160, 0, Rgba::BLACK));
973 assert!(!fb.set_pixel(0, 144, Rgba::BLACK));
974 assert_eq!(fb.get_pixel(160, 0), None);
975 assert_eq!(fb.pixel_rgba(160, 0), Rgba::TRANSPARENT);
976 }
977
978 #[test]
979 fn clear_and_fill_quantize() {
980 let mut fb = fb();
981 fb.fill_rect(0, 0, 100, 100, Rgba::rgb(0x55, 0x55, 0x55));
982 assert_eq!(fb.get_index(50, 50), Some(GbColor::DarkGray));
983 fb.clear(Rgba::BLACK);
984 assert_eq!(fb.get_index(0, 0), Some(GbColor::Black));
985 assert_eq!(fb.get_index(159, 143), Some(GbColor::Black));
986 }
987
988 #[test]
989 fn blit_row_quantizes_each_pixel() {
990 let mut fb = fb();
991 let row = [255u8, 255, 255, 255, 0, 0, 0, 0];
992 assert!(fb.blit_row(0, 0, &row, 2));
993 assert_eq!(fb.get_index(0, 0), Some(GbColor::White));
994 assert_eq!(fb.get_index(1, 0), Some(GbColor::Black));
995 assert!(!fb.blit_row(160, 0, &row, 2));
996 assert!(!fb.blit_row(0, 0, &row, 3)); }
998
999 #[test]
1000 fn remap_shades_inverts() {
1001 let mut fb = fb();
1002 fb.fill_rect(0, 0, 8, 8, Rgba::BLACK);
1003 fb.remap_shades(&[3, 2, 1, 0]);
1005 assert_eq!(fb.get_pixel(0, 0), Some(Rgba::WHITE));
1006 assert_eq!(fb.get_index(0, 0), Some(GbColor::Black));
1008 fb.reset_palette();
1009 assert_eq!(fb.get_pixel(0, 0), Some(Rgba::BLACK));
1010 }
1011
1012 #[test]
1013 fn apply_bgp_fade_to_black() {
1014 let mut fb = fb();
1015 fb.set_pixel(0, 0, Rgba::WHITE);
1016 fb.apply_bgp(0b11111111);
1018 assert_eq!(fb.get_pixel(0, 0), Some(Rgba::BLACK));
1019 assert_eq!(fb.get_index(0, 0), Some(GbColor::White));
1020 }
1021
1022 #[test]
1023 fn scale_shades_dims_display() {
1024 let mut fb = fb();
1025 fb.fill_rect(0, 0, 8, 8, Rgba::WHITE);
1026 fb.scale_shades(0.5);
1027 assert_eq!(fb.get_pixel(0, 0), Some(Rgba::rgb(127, 127, 127)));
1028 fb.scale_shades(0.0);
1029 assert_eq!(fb.get_pixel(0, 0), Some(Rgba::BLACK));
1030 }
1031
1032 #[test]
1033 fn palette_swap_does_not_touch_draws() {
1034 let mut fb = fb();
1035 fb.set_pixel(4, 4, Rgba::rgb(0x55, 0x55, 0x55));
1036 fb.apply_bgp(0b11100100); fb.set_pixel(5, 4, Rgba::rgb(0xAA, 0xAA, 0xAA));
1039 fb.reset_palette();
1040 assert_eq!(fb.get_pixel(5, 4), Some(Rgba::rgb(0xAA, 0xAA, 0xAA)));
1041 }
1042
1043 #[test]
1044 fn copy_from_copies_pixels_and_palette() {
1045 let mut src = fb();
1046 src.fill_rect(0, 0, 16, 16, Rgba::BLACK);
1047 src.apply_bgp(0b00000000); let mut dst = fb();
1049 dst.copy_from(&src);
1050 assert_eq!(dst.get_index(8, 8), Some(GbColor::Black));
1051 assert_eq!(dst.get_pixel(8, 8), Some(Rgba::WHITE));
1052 assert_eq!(dst.packed(), src.packed());
1053 }
1054
1055 #[test]
1056 fn clear_resets_display_palette() {
1057 let mut fb = fb();
1058 fb.apply_bgp(0b00000000); assert_eq!(fb.get_pixel(0, 0), Some(Rgba::WHITE));
1060 fb.clear(Rgba::BLACK);
1061 assert_eq!(fb.get_pixel(0, 0), Some(Rgba::BLACK));
1063 fb.set_pixel(1, 0, Rgba::WHITE);
1064 assert_eq!(fb.get_pixel(1, 0), Some(Rgba::WHITE));
1065 }
1066
1067 #[test]
1068 fn to_rgba_uses_display_palette() {
1069 let mut fb = RgbaIndexedFrameBuffer::<GbColor>::new(RenderConfig::new(2, 1), Rgba::WHITE);
1070 fb.set_pixel(0, 0, Rgba::WHITE);
1071 fb.apply_bgp(0b00000000); let mut out = [0u8; 8];
1073 assert!(fb.to_rgba(&mut out));
1074 assert_eq!(&out[0..4], &[0xFF, 0xFF, 0xFF, 0xFF]);
1075 }
1076
1077 #[test]
1078 fn fb_surface_present_and_pixels() {
1079 let mut fb = RgbaIndexedFrameBuffer::<GbColor>::new_screen(4, 2);
1080 fb.set_pixel(1, 1, Rgba::WHITE);
1081 assert_eq!(fb.width(), 4);
1082 assert_eq!(fb.height(), 2);
1083 assert_eq!(fb.pixel_rgba(1, 1), Rgba::WHITE);
1084 assert_eq!(fb.pixel_rgba(0, 0), Rgba::BLACK);
1085 let mut out = [0u8; 4 * 2 * 4];
1086 fb.present_into(&mut out);
1087 assert_eq!(&out[5 * 4..6 * 4], &[0xFF, 0xFF, 0xFF, 0xFF]);
1088 }
1089
1090 #[test]
1091 fn sprite_palette_quantization_matches_draw_palette() {
1092 let mut fb = fb();
1098 for (i, &c) in GRAYSCALE_SPRITE_PALETTE.colors[..4].iter().enumerate() {
1099 fb.set_pixel(i as u32, 0, c);
1100 let expected = if i == 0 { GbColor::Black } else { GbColor::from_u8(i as u8) };
1101 assert_eq!(fb.get_index(i as u32, 0), Some(expected));
1102 }
1103 }
1104}