1use crate::{FbSurface, FrameBuffer, DirtyRegion, TILE_SIZE};
9use dotzuki_engine::render::Rgba;
10use dotzuki_engine::render::{BlendMode, MapLayer};
11use dotzuki_engine::tilemap::TilemapEntry;
12use crate::tile::RgbaTile;
13use std::collections::HashMap;
14
15const TILE_PIXELS: u32 = TILE_SIZE;
16
17pub struct LayerTileCache {
22 entries: HashMap<(u16, u8), RgbaTile>,
23}
24
25impl LayerTileCache {
26 pub fn new() -> Self {
27 Self { entries: HashMap::new() }
28 }
29
30 pub fn invalidate(&mut self) {
32 self.entries.clear();
33 }
34
35 pub fn get_or_insert<F>(&mut self, tile_id: u16, pal_group: u8, tile_color: &F) -> &RgbaTile
37 where
38 F: Fn(u16, u8, u8, u8) -> Rgba,
39 {
40 let key = (tile_id, pal_group);
41 self.entries.entry(key).or_insert_with(|| {
42 let mut pixels = [[Rgba::TRANSPARENT; TILE_PIXELS as usize]; TILE_PIXELS as usize];
43 for py in 0..(TILE_PIXELS as u8) {
44 for px in 0..(TILE_PIXELS as u8) {
45 pixels[py as usize][px as usize] = tile_color(tile_id, pal_group, px, py);
46 }
47 }
48 RgbaTile { pixels }
49 })
50 }
51}
52
53impl Default for LayerTileCache {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59pub fn render_layers<F>(
80 fb: &mut impl FbSurface,
81 layers: &[MapLayer],
82 camera_x: i32,
83 camera_y: i32,
84 width: u32,
85 height: u32,
86 tile_color: F,
87) where
88 F: Fn(u16, u8, u8, u8) -> Rgba,
89{
90 render_layers_sized(fb, layers, camera_x, camera_y, width, height, TILE_PIXELS, tile_color)
91}
92
93pub fn render_layers_sized<F>(
98 fb: &mut impl FbSurface,
99 layers: &[MapLayer],
100 camera_x: i32,
101 camera_y: i32,
102 width: u32,
103 height: u32,
104 tile_size: u32,
105 tile_color: F,
106) where
107 F: Fn(u16, u8, u8, u8) -> Rgba,
108{
109 render_layers_with_cache_sized(
110 fb, layers, camera_x, camera_y, width, height, tile_size, tile_color, None,
111 )
112}
113
114pub fn render_layers_with_cache<F>(
117 fb: &mut impl FbSurface,
118 layers: &[MapLayer],
119 camera_x: i32,
120 camera_y: i32,
121 width: u32,
122 height: u32,
123 tile_color: F,
124 cache: Option<&mut LayerTileCache>,
125) where
126 F: Fn(u16, u8, u8, u8) -> Rgba,
127{
128 render_layers_with_cache_sized(
129 fb, layers, camera_x, camera_y, width, height, TILE_PIXELS, tile_color, cache,
130 )
131}
132
133pub fn render_layers_with_cache_sized<F>(
141 fb: &mut impl FbSurface,
142 layers: &[MapLayer],
143 camera_x: i32,
144 camera_y: i32,
145 width: u32,
146 height: u32,
147 tile_size: u32,
148 tile_color: F,
149 mut cache: Option<&mut LayerTileCache>,
150) where
151 F: Fn(u16, u8, u8, u8) -> Rgba,
152{
153 let mut visible: Vec<&MapLayer> = layers.iter().filter(|l| l.visible).collect();
155 visible.sort_by_key(|l| l.z_index);
156
157 if visible.is_empty() {
158 fb.clear(Rgba::TRANSPARENT);
160 return;
161 }
162
163 if visible.len() == 1 {
171 render_single_layer(fb, visible[0], camera_x, camera_y, width, height, tile_size, &tile_color, &mut cache);
172 return;
173 }
174
175 fb.clear(Rgba::TRANSPARENT);
177 let mut temp = FrameBuffer {
178 data: vec![0; (width * height * 4) as usize],
179 width,
180 height,
181 dirty_region: DirtyRegion::full(width, height),
182 };
183
184 for layer in &visible {
185 temp.clear(Rgba::TRANSPARENT);
186 render_single_layer(&mut temp, layer, camera_x, camera_y, width, height, tile_size, &tile_color, &mut cache);
187 composite_onto(fb, &temp, layer.opacity, layer.blend_mode);
188 }
189}
190
191fn render_single_layer<F>(
196 fb: &mut impl FbSurface,
197 layer: &MapLayer,
198 camera_x: i32,
199 camera_y: i32,
200 width: u32,
201 height: u32,
202 tile_size: u32,
203 tile_color: &F,
204 cache: &mut Option<&mut LayerTileCache>,
205) where
206 F: Fn(u16, u8, u8, u8) -> Rgba,
207{
208 let scroll_x = (camera_x as f32 * layer.scroll_factor.0) as i32;
209 let scroll_y = (camera_y as f32 * layer.scroll_factor.1) as i32;
210
211 let use_cache = layer.no_animation && cache.is_some() && tile_size == TILE_PIXELS;
213
214 for screen_y in 0..height {
215 let world_y = scroll_y + screen_y as i32;
216 if world_y < 0 {
217 continue;
218 }
219 let tile_y = (world_y as u32) / tile_size;
220 let pixel_y = (world_y as u32 % tile_size) as u8;
221
222 for screen_x in 0..width {
223 let world_x = scroll_x + screen_x as i32;
224 if world_x < 0 {
225 continue;
226 }
227 let tile_x = (world_x as u32) / tile_size;
228 let pixel_x = (world_x as u32 % tile_size) as u8;
229
230 if let Some(entry) = layer.tilemap.get(tile_x as u16, tile_y as u16) {
231 let color = if use_cache {
232 let c = cache.as_mut().unwrap();
233 let rgba_tile = c.get_or_insert(entry.tile_id, entry.palette_group, tile_color);
234 let (eff_px, eff_py) = effective_pixel(pixel_x, pixel_y, entry, tile_size);
235 rgba_tile.pixels[eff_py as usize][eff_px as usize]
236 } else {
237 let (eff_px, eff_py) = effective_pixel(pixel_x, pixel_y, entry, tile_size);
238 tile_color(entry.tile_id, entry.palette_group, eff_px, eff_py)
239 };
240 if color.a == 0 {
246 continue;
247 }
248 fb.set_pixel(screen_x, screen_y, color);
249 }
250 }
251 }
252}
253
254#[inline]
257fn effective_pixel(px: u8, py: u8, entry: &TilemapEntry, tile_size: u32) -> (u8, u8) {
258 let last = (tile_size - 1) as u8;
259 let eff_px = if entry.flip_h { last - px } else { px };
260 let eff_py = if entry.flip_v { last - py } else { py };
261 (eff_px, eff_py)
262}
263
264fn composite_onto(dst: &mut impl FbSurface, src: &FrameBuffer, opacity: f32, blend_mode: BlendMode) {
278 let (w, h) = (dst.width(), dst.height());
279 assert_eq!(w, src.width);
280 assert_eq!(h, src.height);
281
282 for y in 0..h {
283 for x in 0..w {
284 let off = ((y * w + x) * 4) as usize;
285 let sr = src.data[off] as f32;
286 let sg = src.data[off + 1] as f32;
287 let sb = src.data[off + 2] as f32;
288 let sa = src.data[off + 3] as f32;
289
290 let d = dst.get_pixel(x, y).unwrap_or(Rgba::TRANSPARENT);
291 let dr = d.r as f32;
292 let dg = d.g as f32;
293 let db = d.b as f32;
294 let da = d.a as f32;
295
296 let src_alpha = (sa / 255.0) * opacity;
298
299 let (out_r, out_g, out_b, out_a) = match blend_mode {
300 BlendMode::Normal => {
301 let a = src_alpha;
302 let inv_a = 1.0 - a;
303 let r = sr * a + dr * inv_a;
304 let g = sg * a + dg * inv_a;
305 let b = sb * a + db * inv_a;
306 let a_out = sa * opacity + da * (1.0 - src_alpha);
307 (r, g, b, a_out)
308 }
309 BlendMode::Additive => {
310 let r = (dr + sr * src_alpha).min(255.0);
311 let g = (dg + sg * src_alpha).min(255.0);
312 let b = (db + sb * src_alpha).min(255.0);
313 (r, g, b, 255.0)
314 }
315 BlendMode::Multiply => {
316 let inv_opacity = 1.0 - opacity;
317 let r_mul = sr * dr / 255.0;
319 let g_mul = sg * dg / 255.0;
320 let b_mul = sb * db / 255.0;
321 let r = dr * inv_opacity + r_mul * opacity;
322 let g = dg * inv_opacity + g_mul * opacity;
323 let b = db * inv_opacity + b_mul * opacity;
324 (r, g, b, 255.0)
325 }
326 };
327
328 dst.set_pixel(
329 x,
330 y,
331 Rgba::new(
332 out_r.clamp(0.0, 255.0) as u8,
333 out_g.clamp(0.0, 255.0) as u8,
334 out_b.clamp(0.0, 255.0) as u8,
335 out_a.clamp(0.0, 255.0) as u8,
336 ),
337 );
338 }
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use crate::DirtyRegion;
346 use dotzuki_engine::render::{BlendMode, MapLayer};
347 use dotzuki_engine::tilemap::{Tilemap, TilemapEntry};
348
349 fn test_tile_color(tile_id: u16, _pal: u8, _px: u8, _py: u8) -> Rgba {
356 match tile_id {
357 0 => Rgba::TRANSPARENT,
358 1 => Rgba::rgb(255, 0, 0),
359 2 => Rgba::rgb(0, 255, 0),
360 3 => Rgba::rgb(0, 0, 255),
361 _ => Rgba::rgb(255, 255, 255),
362 }
363 }
364
365 fn uniform_tilemap(w: u16, h: u16, tile_id: u16) -> Tilemap {
367 let mut tm = Tilemap::new(w, h);
368 let entry = TilemapEntry {
369 tile_id,
370 ..Default::default()
371 };
372 tm.fill_rect(0, 0, w, h, entry);
373 tm
374 }
375
376 #[test]
381 fn effective_pixel_no_flip() {
382 let e = TilemapEntry::default();
383 assert_eq!(effective_pixel(3, 5, &e, 8), (3, 5));
384 }
385
386 #[test]
387 fn effective_pixel_flip_h() {
388 let e = TilemapEntry {
389 flip_h: true,
390 ..Default::default()
391 };
392 assert_eq!(effective_pixel(0, 0, &e, 8), (7, 0));
393 assert_eq!(effective_pixel(7, 0, &e, 8), (0, 0));
394 }
395
396 #[test]
397 fn effective_pixel_flip_v() {
398 let e = TilemapEntry {
399 flip_v: true,
400 ..Default::default()
401 };
402 assert_eq!(effective_pixel(0, 0, &e, 8), (0, 7));
403 assert_eq!(effective_pixel(0, 7, &e, 8), (0, 0));
404 }
405
406 #[test]
407 fn effective_pixel_flip_both() {
408 let e = TilemapEntry {
409 flip_h: true,
410 flip_v: true,
411 ..Default::default()
412 };
413 assert_eq!(effective_pixel(0, 0, &e, 8), (7, 7));
414 }
415
416 #[test]
417 fn effective_pixel_flip_16px() {
418 let e = TilemapEntry {
420 flip_h: true,
421 flip_v: true,
422 ..Default::default()
423 };
424 assert_eq!(effective_pixel(0, 0, &e, 16), (15, 15));
425 assert_eq!(effective_pixel(15, 3, &e, 16), (0, 12));
426 }
427
428 #[test]
429 fn render_layers_sized_16px_tiles() {
430 let mut tm = Tilemap::new(2, 1);
435 tm.set(0, 0, TilemapEntry { tile_id: 2, ..Default::default() });
436 tm.set(1, 0, TilemapEntry { tile_id: 3, ..Default::default() });
437 let layer = MapLayer::new(tm, 0);
438
439 let mut fb = make_fb(32, 16, Rgba::WHITE);
440 render_layers_sized(&mut fb, &[layer], 0, 0, 32, 16, 16, test_tile_color);
441
442 assert_eq!(fb.get_pixel(4, 8), Some(Rgba::rgb(0, 255, 0)), "left half = green tile 0");
443 assert_eq!(fb.get_pixel(20, 8), Some(Rgba::rgb(0, 0, 255)), "right half = blue tile 1");
444 }
445
446 #[test]
451 fn composite_normal_fully_opaque() {
452 let mut dst = make_fb(2, 2, Rgba::rgb(0, 0, 0));
453 let src = make_fb(2, 2, Rgba::rgb(255, 0, 0));
454 composite_onto(&mut dst, &src, 1.0, BlendMode::Normal);
455 for y in 0..2 {
456 for x in 0..2 {
457 assert_eq!(dst.get_pixel(x, y), Some(Rgba::rgb(255, 0, 0)));
458 }
459 }
460 }
461
462 #[test]
463 fn composite_normal_half_opaque() {
464 let mut dst = make_fb(1, 1, Rgba::rgb(0, 0, 0));
465 let src = make_fb(1, 1, Rgba::rgb(200, 0, 0));
466 composite_onto(&mut dst, &src, 0.5, BlendMode::Normal);
467 let p = dst.get_pixel(0, 0).unwrap();
469 assert!((p.r as i32 - 100).abs() <= 1);
470 assert_eq!(p.g, 0);
471 assert_eq!(p.b, 0);
472 }
473
474 #[test]
475 fn composite_normal_transparent_src_does_not_overwrite() {
476 let mut dst = make_fb(1, 1, Rgba::rgb(100, 100, 100));
477 let src = make_fb(1, 1, Rgba::TRANSPARENT);
478 composite_onto(&mut dst, &src, 1.0, BlendMode::Normal);
479 assert_eq!(dst.get_pixel(0, 0), Some(Rgba::rgb(100, 100, 100)));
480 }
481
482 #[test]
483 fn composite_additive() {
484 let mut dst = make_fb(1, 1, Rgba::rgb(50, 30, 10));
485 let src = make_fb(1, 1, Rgba::rgb(100, 80, 60));
486 composite_onto(&mut dst, &src, 1.0, BlendMode::Additive);
487 let p = dst.get_pixel(0, 0).unwrap();
488 assert!(p.r >= 145);
490 assert!(p.g >= 105);
491 assert!(p.b >= 65);
492 }
493
494 #[test]
495 fn composite_additive_clamps_at_255() {
496 let mut dst = make_fb(1, 1, Rgba::rgb(200, 200, 200));
497 let src = make_fb(1, 1, Rgba::rgb(200, 200, 200));
498 composite_onto(&mut dst, &src, 1.0, BlendMode::Additive);
499 let p = dst.get_pixel(0, 0).unwrap();
500 assert_eq!(p.r, 255);
502 assert_eq!(p.g, 255);
503 assert_eq!(p.b, 255);
504 }
505
506 #[test]
507 fn composite_multiply() {
508 let mut dst = make_fb(1, 1, Rgba::rgb(255, 128, 64));
509 let src = make_fb(1, 1, Rgba::rgb(128, 255, 192));
510 composite_onto(&mut dst, &src, 1.0, BlendMode::Multiply);
511 let p = dst.get_pixel(0, 0).unwrap();
512 assert!((p.r as i32 - 128).abs() <= 1);
514 assert!((p.g as i32 - 128).abs() <= 1);
515 assert!((p.b as i32 - 48).abs() <= 2);
516 }
517
518 #[test]
523 fn single_layer_renders_correctly() {
524 let tm = uniform_tilemap(1, 1, 1);
526 let layer = MapLayer::new(tm, 0);
527 let mut fb = FrameBuffer {
528 data: vec![0; (8 * 8 * 4) as usize],
529 width: 8,
530 height: 8,
531 dirty_region: DirtyRegion::full(8, 8),
532 };
533
534 render_layers(&mut fb, &[layer], 0, 0, 8, 8, test_tile_color);
535
536 for y in 0..8 {
537 for x in 0..8 {
538 assert_eq!(fb.get_pixel(x, y), Some(Rgba::rgb(255, 0, 0)));
539 }
540 }
541 }
542
543 #[test]
544 fn single_layer_respects_camera_offset() {
545 let mut tm = Tilemap::new(2, 1);
547 tm.set(0, 0, TilemapEntry { tile_id: 2, ..Default::default() });
548 tm.set(1, 0, TilemapEntry { tile_id: 3, ..Default::default() });
549
550 let layer = MapLayer::new(tm, 0);
551 let mut fb = FrameBuffer {
553 data: vec![0; (8 * 8 * 4) as usize],
554 width: 8,
555 height: 8,
556 dirty_region: DirtyRegion::full(8, 8),
557 };
558
559 render_layers(&mut fb, &[layer], 8, 0, 8, 8, test_tile_color);
560
561 for y in 0..8 {
563 for x in 0..8 {
564 assert_eq!(fb.get_pixel(x, y), Some(Rgba::rgb(0, 0, 255)));
565 }
566 }
567 }
568
569 #[test]
570 fn scroll_factor_half_speed() {
571 let mut tm = Tilemap::new(2, 1);
574 tm.set(0, 0, TilemapEntry { tile_id: 1, ..Default::default() });
575 tm.set(1, 0, TilemapEntry { tile_id: 2, ..Default::default() });
576
577 let mut layer = MapLayer::new(tm, 0);
578 layer.scroll_factor = (0.5, 0.5);
579
580 let mut fb = FrameBuffer {
581 data: vec![0; (8 * 8 * 4) as usize],
582 width: 8,
583 height: 8,
584 dirty_region: DirtyRegion::full(8, 8),
585 };
586
587 render_layers(&mut fb, &[layer], 16, 0, 8, 8, test_tile_color);
588
589 for y in 0..8 {
591 for x in 0..8 {
592 assert_eq!(fb.get_pixel(x, y), Some(Rgba::rgb(0, 255, 0)));
593 }
594 }
595 }
596
597 #[test]
598 fn single_layer_transparent_tile_reveals_background() {
599 let mut tm = Tilemap::new(2, 1);
607 tm.set(0, 0, TilemapEntry { tile_id: 0, ..Default::default() }); tm.set(1, 0, TilemapEntry { tile_id: 1, ..Default::default() }); let layer = MapLayer::new(tm, 0);
610
611 let mut fb = make_fb(16, 8, Rgba::rgb(0, 0, 255)); render_layers(&mut fb, &[layer], 0, 0, 16, 8, test_tile_color);
613
614 assert_eq!(fb.get_pixel(0, 0), Some(Rgba::rgb(0, 0, 255)), "transparent tile keeps background");
616 assert_eq!(fb.get_pixel(7, 7), Some(Rgba::rgb(0, 0, 255)), "transparent tile keeps background");
617 assert_eq!(fb.get_pixel(8, 0), Some(Rgba::rgb(255, 0, 0)), "opaque tile overwrites background");
619 assert_eq!(fb.get_pixel(15, 7), Some(Rgba::rgb(255, 0, 0)), "opaque tile overwrites background");
620 }
621
622 #[test]
627 fn two_layers_composite_transparent_top() {
628 let bottom_tm = uniform_tilemap(1, 1, 2);
630 let top_tm = uniform_tilemap(1, 1, 0);
631
632 let bottom = MapLayer::new(bottom_tm, 0);
633 let top = MapLayer::new(top_tm, 1);
634
635 let mut fb = FrameBuffer {
636 data: vec![0; (8 * 8 * 4) as usize],
637 width: 8,
638 height: 8,
639 dirty_region: DirtyRegion::full(8, 8),
640 };
641
642 render_layers(&mut fb, &[bottom, top], 0, 0, 8, 8, test_tile_color);
643
644 for y in 0..8 {
646 for x in 0..8 {
647 assert_eq!(fb.get_pixel(x, y), Some(Rgba::rgb(0, 255, 0)));
648 }
649 }
650 }
651
652 #[test]
653 fn two_layers_opaque_top_covers_bottom() {
654 let bottom_tm = uniform_tilemap(1, 1, 2);
656 let top_tm = uniform_tilemap(1, 1, 1);
657
658 let bottom = MapLayer::new(bottom_tm, 0);
659 let top = MapLayer::new(top_tm, 1);
660
661 let mut fb = FrameBuffer {
662 data: vec![0; (8 * 8 * 4) as usize],
663 width: 8,
664 height: 8,
665 dirty_region: DirtyRegion::full(8, 8),
666 };
667
668 render_layers(&mut fb, &[bottom, top], 0, 0, 8, 8, test_tile_color);
669
670 for y in 0..8 {
672 for x in 0..8 {
673 assert_eq!(fb.get_pixel(x, y), Some(Rgba::rgb(255, 0, 0)));
674 }
675 }
676 }
677
678 #[test]
679 fn opacity_affects_visibility() {
680 let bottom_tm = uniform_tilemap(1, 1, 1);
682 let top_tm = uniform_tilemap(1, 1, 2);
683
684 let bottom = MapLayer::new(bottom_tm, 0);
685 let mut top = MapLayer::new(top_tm, 1);
686 top.opacity = 0.5;
687
688 let mut fb = FrameBuffer {
689 data: vec![0; (1 * 1 * 4) as usize],
690 width: 1,
691 height: 1,
692 dirty_region: DirtyRegion::full(1, 1),
693 };
694
695 render_layers(&mut fb, &[bottom, top], 0, 0, 1, 1, test_tile_color);
696
697 let p = fb.get_pixel(0, 0).unwrap();
700 let r = p.r as i32;
701 let g = p.g as i32;
702 assert!(r >= 125 && r <= 130, "r={r}");
704 assert!(g >= 125 && g <= 130, "g={g}");
706 }
707
708 #[test]
709 fn invisible_layer_skipped() {
710 let bottom_tm = uniform_tilemap(1, 1, 2);
712 let top_tm = uniform_tilemap(1, 1, 1);
713
714 let bottom = MapLayer::new(bottom_tm, 0);
715 let mut top = MapLayer::new(top_tm, 1);
716 top.visible = false;
717
718 let mut fb = FrameBuffer {
719 data: vec![0; (8 * 8 * 4) as usize],
720 width: 8,
721 height: 8,
722 dirty_region: DirtyRegion::full(8, 8),
723 };
724
725 render_layers(&mut fb, &[bottom, top], 0, 0, 8, 8, test_tile_color);
726
727 for y in 0..8 {
729 for x in 0..8 {
730 assert_eq!(fb.get_pixel(x, y), Some(Rgba::rgb(0, 255, 0)));
731 }
732 }
733 }
734
735 #[test]
736 fn empty_layers_produces_blank() {
737 let mut fb = FrameBuffer {
738 data: vec![0xFF; (8 * 8 * 4) as usize],
739 width: 8,
740 height: 8,
741 dirty_region: DirtyRegion::full(8, 8),
742 };
743
744 render_layers(&mut fb, &[], 0, 0, 8, 8, test_tile_color);
745
746 for y in 0..8 {
748 for x in 0..8 {
749 assert_eq!(fb.get_pixel(x, y), Some(Rgba::TRANSPARENT));
750 }
751 }
752 }
753
754 #[test]
755 fn z_index_sorting() {
756 let tm_a = uniform_tilemap(1, 1, 1);
758 let tm_b = uniform_tilemap(1, 1, 2);
759
760 let layer_a = MapLayer::new(tm_a, 5);
761 let layer_b = MapLayer::new(tm_b, 1);
762
763 let mut fb = FrameBuffer {
765 data: vec![0; (8 * 8 * 4) as usize],
766 width: 8,
767 height: 8,
768 dirty_region: DirtyRegion::full(8, 8),
769 };
770
771 render_layers(&mut fb, &[layer_a, layer_b], 0, 0, 8, 8, test_tile_color);
772
773 for y in 0..8 {
775 for x in 0..8 {
776 assert_eq!(fb.get_pixel(x, y), Some(Rgba::rgb(255, 0, 0)));
777 }
778 }
779 }
780
781 #[test]
782 fn scroll_factor_vertical() {
783 let mut tm = Tilemap::new(1, 2);
785 tm.set(0, 0, TilemapEntry { tile_id: 1, ..Default::default() });
786 tm.set(0, 1, TilemapEntry { tile_id: 2, ..Default::default() });
787
788 let layer = MapLayer::new(tm, 0);
789
790 let mut fb = FrameBuffer {
791 data: vec![0; (8 * 8 * 4) as usize],
792 width: 8,
793 height: 8,
794 dirty_region: DirtyRegion::full(8, 8),
795 };
796
797 render_layers(&mut fb, &[layer], 0, 8, 8, 8, test_tile_color);
798
799 for y in 0..8 {
801 for x in 0..8 {
802 assert_eq!(fb.get_pixel(x, y), Some(Rgba::rgb(0, 255, 0)));
803 }
804 }
805 }
806
807 #[test]
808 fn single_layer_optimization_bypasses_temp_buffer() {
809 let tm = uniform_tilemap(2, 2, 3);
813 let layer = MapLayer::new(tm, 0);
814
815 let mut fb = FrameBuffer {
816 data: vec![0xFF; (16 * 16 * 4) as usize],
817 width: 16,
818 height: 16,
819 dirty_region: DirtyRegion::full(16, 16),
820 };
821
822 render_layers(&mut fb, &[layer], 0, 0, 16, 16, test_tile_color);
823
824 for y in 0..16 {
825 for x in 0..16 {
826 assert_eq!(fb.get_pixel(x, y), Some(Rgba::rgb(0, 0, 255)));
827 }
828 }
829 }
830
831 #[test]
836 fn dirty_region_empty_is_not_present() {
837 let d = DirtyRegion::empty();
838 assert!(!d.present);
839 assert!(!d.contains_pixel(0, 0));
840 assert!(!d.contains_pixel(100, 50));
841 }
842
843 #[test]
844 fn dirty_region_full_contains_all() {
845 let d = DirtyRegion::full(160, 144);
846 assert!(d.present);
847 assert!(d.contains_pixel(0, 0));
848 assert!(d.contains_pixel(159, 143));
849 assert!(!d.contains_pixel(160, 0));
850 assert!(!d.contains_pixel(0, 144));
851 }
852
853 #[test]
854 fn dirty_region_union_combines() {
855 let a = DirtyRegion::new(0, 0, 10, 10);
856 let b = DirtyRegion::new(5, 5, 20, 20);
857 let u = a.union(&b);
858 assert!(u.present);
859 assert_eq!(u.x, 0);
860 assert_eq!(u.y, 0);
861 assert_eq!(u.width, 25);
862 assert_eq!(u.height, 25);
863 }
864
865 #[test]
866 fn dirty_region_union_with_empty() {
867 let a = DirtyRegion::new(0, 0, 10, 10);
868 let empty = DirtyRegion::empty();
869 assert_eq!(a.union(&empty), a);
870 assert_eq!(empty.union(&a), a);
871 }
872
873 #[test]
874 fn frame_buffer_dirty_mark_and_check() {
875 let mut fb = make_fb(20, 10, Rgba::WHITE);
876 fb.clear_dirty();
877 assert!(!fb.is_dirty_pixel(0, 0));
879 assert!(!fb.is_dirty_pixel(10, 5));
880
881 fb.mark_dirty_rect(5, 3, 10, 4);
883 assert!(fb.is_dirty_pixel(5, 3));
884 assert!(fb.is_dirty_pixel(14, 6));
885 assert!(!fb.is_dirty_pixel(4, 3));
886 assert!(!fb.is_dirty_pixel(0, 0));
887
888 fb.mark_all_dirty();
890 assert!(fb.is_dirty_pixel(0, 0));
891 assert!(fb.is_dirty_pixel(19, 9));
892 }
893
894 #[test]
895 fn frame_buffer_mark_dirty_tile() {
896 let mut fb = make_fb(32, 24, Rgba::WHITE);
897 fb.clear_dirty();
898 fb.mark_dirty_tile(2, 3);
899 assert!(fb.is_dirty_pixel(16, 24));
901 assert!(fb.is_dirty_pixel(23, 31));
902 assert!(!fb.is_dirty_pixel(15, 24));
903 assert!(!fb.is_dirty_pixel(0, 0));
904 }
905
906 #[test]
911 fn cached_tiles_render_same_as_non_cached() {
912 let mut tm = Tilemap::new(4, 4);
914 let tiles = [1u16, 2, 3, 1, 2, 1, 3, 2, 3, 1, 2, 1, 1, 3, 2, 3];
915 for y in 0..4u16 {
916 for x in 0..4u16 {
917 tm.set(x, y, TilemapEntry { tile_id: tiles[(y * 4 + x) as usize], ..Default::default() });
918 }
919 }
920
921 let mut layer = MapLayer::new(tm, 0);
922 layer.no_animation = true;
923
924 let mut fb_uncached = make_fb(32, 32, Rgba::WHITE);
926 render_layers(&mut fb_uncached, &[layer.clone()], 0, 0, 32, 32, test_tile_color);
927
928 let mut fb_cached = make_fb(32, 32, Rgba::WHITE);
930 let mut cache = LayerTileCache::new();
931 render_layers_with_cache(&mut fb_cached, &[layer], 0, 0, 32, 32, test_tile_color, Some(&mut cache));
932
933 for y in 0..32 {
935 for x in 0..32 {
936 assert_eq!(fb_uncached.get_pixel(x, y), fb_cached.get_pixel(x, y),
937 "pixel ({},{}) differs", x, y);
938 }
939 }
940 }
941
942 #[test]
943 fn cache_invalidates_and_rebuilds() {
944 let tm = uniform_tilemap(1, 1, 1);
945 let mut layer = MapLayer::new(tm, 0);
946 layer.no_animation = true;
947
948 let mut cache = LayerTileCache::new();
949 let mut fb = make_fb(8, 8, Rgba::WHITE);
950 render_layers_with_cache(&mut fb, &[layer.clone()], 0, 0, 8, 8, test_tile_color, Some(&mut cache));
951
952 assert!(!cache.entries.is_empty());
954
955 cache.invalidate();
957 assert!(cache.entries.is_empty());
958
959 let mut fb2 = make_fb(8, 8, Rgba::WHITE);
961 render_layers_with_cache(&mut fb2, &[layer], 0, 0, 8, 8, test_tile_color, Some(&mut cache));
962 assert!(!cache.entries.is_empty());
963 }
964
965 #[test]
966 fn render_layers_ignores_dirty_region_and_always_redraws() {
967 let tm = uniform_tilemap(2, 2, 1);
970 let layer = MapLayer::new(tm, 0);
971
972 let mut fb = make_fb(16, 16, Rgba::WHITE);
973 fb.clear_dirty(); render_layers(&mut fb, &[layer], 0, 0, 16, 16, test_tile_color);
976
977 for y in 0..16 {
979 for x in 0..16 {
980 assert_eq!(fb.get_pixel(x, y), Some(Rgba::rgb(255, 0, 0)),
981 "pixel ({},{}) should be red after full redraw", x, y);
982 }
983 }
984 }
985
986 #[test]
987 fn layer_without_no_animation_does_not_use_cache() {
988 let tm = uniform_tilemap(1, 1, 1);
989 let mut layer = MapLayer::new(tm, 0);
990 layer.no_animation = false;
991
992 let mut cache = LayerTileCache::new();
993 let mut fb = make_fb(8, 8, Rgba::WHITE);
994 render_layers_with_cache(&mut fb, &[layer], 0, 0, 8, 8, test_tile_color, Some(&mut cache));
995
996 assert!(cache.entries.is_empty());
998 }
999
1000 fn make_fb(w: u32, h: u32, color: Rgba) -> FrameBuffer {
1005 let mut fb = FrameBuffer {
1006 data: vec![0; (w * h * 4) as usize],
1007 width: w,
1008 height: h,
1009 dirty_region: DirtyRegion::full(w, h),
1010 };
1011 fb.clear(color);
1012 fb
1013 }
1014}