1use core::fmt::Debug;
2
3use embedded_graphics_core::{
4 Pixel,
5 draw_target::DrawTarget,
6 pixelcolor::{Rgb565, RgbColor},
7 prelude::{OriginDimensions, Point},
8};
9
10use crate::{
11 DrawPrimitive,
12 command_buffer::{CommandBuffer, RenderCommand},
13 draw::{DitherConfig, FogConfig, draw_zbuffered_with_effects},
14 error::{BudgetKind, RenderError},
15 retro::{PaletteMode, ScreenTint, SkyConfig, StippleMode},
16};
17
18pub struct FrameCtx<'a> {
19 pub zbuffer: &'a mut [crate::ZDepth],
20 pub width: usize,
21 pub height: usize,
22}
23
24impl<'a> FrameCtx<'a> {
25 pub fn validate(&self) -> Result<(), RenderError> {
26 let expected = self.width * self.height;
27 if self.zbuffer.len() != expected {
28 return Err(RenderError::OutOfBudget(BudgetKind::ZBufferLength {
29 expected,
30 got: self.zbuffer.len(),
31 }));
32 }
33 Ok(())
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct DirtyRegion {
39 pub x: usize,
40 pub y: usize,
41 pub width: usize,
42 pub height: usize,
43}
44
45impl DirtyRegion {
46 fn from_bounds(min_x: i32, min_y: i32, max_x: i32, max_y: i32) -> Option<Self> {
47 if max_x < min_x || max_y < min_y {
48 return None;
49 }
50 Some(Self {
51 x: min_x as usize,
52 y: min_y as usize,
53 width: (max_x - min_x + 1) as usize,
54 height: (max_y - min_y + 1) as usize,
55 })
56 }
57}
58
59#[inline(always)]
60fn primitive_bounds(primitive: &DrawPrimitive) -> (i32, i32, i32, i32) {
61 primitive.bounds()
62}
63
64fn clamp_bounds_to_frame(
65 min_x: i32,
66 min_y: i32,
67 max_x: i32,
68 max_y: i32,
69 width: usize,
70 height: usize,
71) -> Option<(i32, i32, i32, i32)> {
72 let w = width as i32;
73 let h = height as i32;
74 let clamped_min_x = min_x.clamp(0, w.saturating_sub(1));
75 let clamped_min_y = min_y.clamp(0, h.saturating_sub(1));
76 let clamped_max_x = max_x.clamp(0, w.saturating_sub(1));
77 let clamped_max_y = max_y.clamp(0, h.saturating_sub(1));
78 if clamped_max_x < clamped_min_x || clamped_max_y < clamped_min_y {
79 return None;
80 }
81 Some((clamped_min_x, clamped_min_y, clamped_max_x, clamped_max_y))
82}
83
84#[inline]
85fn apply_post(color: Rgb565, tint: Option<ScreenTint>, palette_mode: PaletteMode) -> Rgb565 {
86 let tinted = if let Some(t) = tint {
87 t.apply(color)
88 } else {
89 color
90 };
91 palette_mode.apply(tinted)
92}
93
94fn tint_primitive(
95 primitive: &DrawPrimitive,
96 tint: Option<ScreenTint>,
97 palette_mode: PaletteMode,
98) -> DrawPrimitive {
99 match primitive.clone() {
100 DrawPrimitive::ColoredPoint(p, color) => {
101 DrawPrimitive::ColoredPoint(p, apply_post(color, tint, palette_mode))
102 }
103 DrawPrimitive::Line(points, color) => {
104 DrawPrimitive::Line(points, apply_post(color, tint, palette_mode))
105 }
106 DrawPrimitive::ColoredTriangle(points, color) => {
107 DrawPrimitive::ColoredTriangle(points, apply_post(color, tint, palette_mode))
108 }
109 DrawPrimitive::ColoredTriangleWithDepth {
110 points,
111 depths,
112 color,
113 } => DrawPrimitive::ColoredTriangleWithDepth {
114 points,
115 depths,
116 color: apply_post(color, tint, palette_mode),
117 },
118 DrawPrimitive::TranslucentTriangleWithDepth {
119 points,
120 depths,
121 color,
122 alpha,
123 } => DrawPrimitive::TranslucentTriangleWithDepth {
124 points,
125 depths,
126 color: apply_post(color, tint, palette_mode),
127 alpha,
128 },
129 #[cfg(feature = "lighting")]
130 DrawPrimitive::GouraudTriangle { points, colors } => DrawPrimitive::GouraudTriangle {
131 points,
132 colors: [
133 apply_post(colors[0], tint, palette_mode),
134 apply_post(colors[1], tint, palette_mode),
135 apply_post(colors[2], tint, palette_mode),
136 ],
137 },
138 #[cfg(feature = "lighting")]
139 DrawPrimitive::GouraudTriangleWithDepth {
140 points,
141 depths,
142 colors,
143 } => DrawPrimitive::GouraudTriangleWithDepth {
144 points,
145 depths,
146 colors: [
147 apply_post(colors[0], tint, palette_mode),
148 apply_post(colors[1], tint, palette_mode),
149 apply_post(colors[2], tint, palette_mode),
150 ],
151 },
152 #[cfg(feature = "textured")]
153 DrawPrimitive::LightmappedTriangle {
154 points,
155 depths,
156 ws,
157 surface_uvs,
158 lm_uvs,
159 texture_id,
160 lightmap_id,
161 brightness,
162 dynamic_tint,
163 } => DrawPrimitive::LightmappedTriangle {
164 points,
165 depths,
166 ws,
167 surface_uvs,
168 lm_uvs,
169 texture_id,
170 lightmap_id,
171 brightness,
172 dynamic_tint: apply_post(dynamic_tint, tint, palette_mode),
173 },
174 #[cfg(feature = "textured")]
175 DrawPrimitive::TexturedGouraudTriangleWithDepth {
176 points,
177 depths,
178 ws,
179 uvs,
180 colors,
181 texture_id,
182 } => DrawPrimitive::TexturedGouraudTriangleWithDepth {
183 points,
184 depths,
185 ws,
186 uvs,
187 colors: [
188 apply_post(colors[0], tint, palette_mode),
189 apply_post(colors[1], tint, palette_mode),
190 apply_post(colors[2], tint, palette_mode),
191 ],
192 texture_id,
193 },
194 #[cfg(feature = "textured")]
195 other => other,
196 }
197}
198
199#[inline]
200fn blend_rgb565(a: Rgb565, b: Rgb565, t_q8: u16) -> Rgb565 {
201 let inv = 255u16.saturating_sub(t_q8);
202 let r = ((a.r() as u16 * inv + b.r() as u16 * t_q8) / 255) as u8;
203 let g = ((a.g() as u16 * inv + b.g() as u16 * t_q8) / 255) as u8;
204 let bch = ((a.b() as u16 * inv + b.b() as u16 * t_q8) / 255) as u8;
205 Rgb565::new(r, g, bch)
206}
207
208#[inline]
209fn stripe_on_at(x: i32, scroll: i32, stripe_w: i32) -> bool {
210 (((x + scroll).div_euclid(stripe_w)) & 1) == 0
211}
212
213fn draw_sky_background<D>(
214 fb: &mut D,
215 width: usize,
216 height: usize,
217 sky: SkyConfig,
218 camera_dir: [f32; 3],
219 screen_tint: Option<ScreenTint>,
220 palette_mode: PaletteMode,
221) -> Result<(), RenderError>
222where
223 D: DrawTarget<Color = Rgb565> + OriginDimensions,
224 D::Error: Debug,
225{
226 let w = width as i32;
227 let h = height as i32;
228 if w <= 0 || h <= 0 {
229 return Ok(());
230 }
231
232 let horizon = (h as f32 * (0.5 + camera_dir[1].clamp(-1.0, 1.0) * 0.25)) as i32;
233 let stripe_w = sky.stripe_width.max(1) as i32;
234 let scroll = (camera_dir[0] * 128.0) as i32;
235 let stripe_fade_span = (h / 6).max(1);
236
237 for y in 0..h {
238 let dy = (y - horizon + h / 2).clamp(0, h);
239 let t_q8 = ((dy as i64 * 255) / h.max(1) as i64) as u16;
240 let base = blend_rgb565(sky.top_color, sky.bottom_color, t_q8);
241 let below_horizon = (y - horizon).max(0);
242 let stripe_strength = if below_horizon == 0 {
243 sky.stripe_strength as u16
244 } else if below_horizon >= stripe_fade_span {
245 0
246 } else {
247 let rem = stripe_fade_span - below_horizon;
248 ((sky.stripe_strength as i32 * rem) / stripe_fade_span) as u16
249 };
250 for x in 0..w {
251 let stripe_on = stripe_on_at(x, scroll, stripe_w);
252 let mut color = if stripe_on && stripe_strength > 0 {
253 blend_rgb565(base, sky.stripe_color, stripe_strength)
254 } else {
255 base
256 };
257 color = apply_post(color, screen_tint, palette_mode);
258 fb.draw_iter([Pixel(Point::new(x, y), color)])
259 .map_err(|_| RenderError::InvalidInput("draw target rejected sky write"))?;
260 }
261 }
262 Ok(())
263}
264
265#[cfg(test)]
266mod tests {
267 use super::stripe_on_at;
268
269 #[test]
270 fn stripe_phase_is_periodic_with_negative_scroll() {
271 let stripe_w = 10;
272 let scroll = -3;
273 for x in -40..40 {
274 assert_eq!(
275 stripe_on_at(x, scroll, stripe_w),
276 stripe_on_at(x + stripe_w * 2, scroll, stripe_w)
277 );
278 }
279 }
280
281 #[test]
282 fn stripe_runs_do_not_exceed_width() {
283 let stripe_w = 8;
284 let scroll = -5;
285 let mut max_run = 0usize;
286 let mut run = 0usize;
287 let mut prev = stripe_on_at(-64, scroll, stripe_w);
288 for x in -63..=64 {
289 let cur = stripe_on_at(x, scroll, stripe_w);
290 if cur == prev {
291 run += 1;
292 } else {
293 max_run = max_run.max(run);
294 run = 1;
295 prev = cur;
296 }
297 }
298 max_run = max_run.max(run);
299 assert!(max_run <= stripe_w as usize);
300 }
301}
302
303pub fn execute_commands<D, const MAX: usize>(
304 fb: &mut D,
305 frame: &mut FrameCtx<'_>,
306 cmd: &CommandBuffer<MAX>,
307 fog: Option<&FogConfig>,
308) -> Result<(), RenderError>
309where
310 D: DrawTarget<Color = Rgb565> + OriginDimensions,
311 D::Error: Debug,
312{
313 let _ = execute_commands_with_dirty_region_effects(
314 fb,
315 frame,
316 cmd,
317 fog,
318 None,
319 None,
320 StippleMode::Off,
321 PaletteMode::Off,
322 None,
323 [0.0, 0.0, -1.0],
324 )?;
325 Ok(())
326}
327
328pub fn execute_commands_with_dirty_region<D, const MAX: usize>(
329 fb: &mut D,
330 frame: &mut FrameCtx<'_>,
331 cmd: &CommandBuffer<MAX>,
332 fog: Option<&FogConfig>,
333) -> Result<Option<DirtyRegion>, RenderError>
334where
335 D: DrawTarget<Color = Rgb565> + OriginDimensions,
336 D::Error: Debug,
337{
338 execute_commands_with_dirty_region_effects(
339 fb,
340 frame,
341 cmd,
342 fog,
343 None,
344 None,
345 StippleMode::Off,
346 PaletteMode::Off,
347 None,
348 [0.0, 0.0, -1.0],
349 )
350}
351
352pub fn execute_commands_with_dirty_region_effects<D, const MAX: usize>(
353 fb: &mut D,
354 frame: &mut FrameCtx<'_>,
355 cmd: &CommandBuffer<MAX>,
356 fog: Option<&FogConfig>,
357 dither: Option<&DitherConfig>,
358 screen_tint: Option<ScreenTint>,
359 _stipple_mode: StippleMode,
360 palette_mode: PaletteMode,
361 sky: Option<SkyConfig>,
362 camera_dir: [f32; 3],
363) -> Result<Option<DirtyRegion>, RenderError>
364where
365 D: DrawTarget<Color = Rgb565> + OriginDimensions,
366 D::Error: Debug,
367{
368 frame.validate()?;
369 let mut dirty_bounds: Option<(i32, i32, i32, i32)> = None;
370 if let Some(sky_cfg) = sky {
371 draw_sky_background(
372 fb,
373 frame.width,
374 frame.height,
375 sky_cfg,
376 camera_dir,
377 screen_tint,
378 palette_mode,
379 )?;
380 dirty_bounds = Some((
381 0,
382 0,
383 frame.width.saturating_sub(1) as i32,
384 frame.height.saturating_sub(1) as i32,
385 ));
386 }
387
388 for c in cmd.iter() {
389 match c {
390 RenderCommand::ClearColor(color) => {
391 let w = frame.width as i32;
392 let h = frame.height as i32;
393 let clear_color = apply_post(*color, screen_tint, palette_mode);
394 for y in 0..h {
395 for x in 0..w {
396 fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
397 .map_err(|_| {
398 RenderError::InvalidInput("draw target rejected clear write")
399 })?;
400 }
401 }
402 }
403 RenderCommand::ClearDepth(value) => {
404 crate::clear_zbuffer(frame.zbuffer, *value);
405 }
406 RenderCommand::Draw(primitive) => {
407 let prim = tint_primitive(primitive, screen_tint, palette_mode);
408 draw_zbuffered_with_effects(prim, fb, frame.zbuffer, frame.width, fog, dither);
409 let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
410 if let Some((min_x, min_y, max_x, max_y)) =
411 clamp_bounds_to_frame(min_x, min_y, max_x, max_y, frame.width, frame.height)
412 {
413 dirty_bounds = Some(match dirty_bounds {
414 Some((cx0, cy0, cx1, cy1)) => (
415 cx0.min(min_x),
416 cy0.min(min_y),
417 cx1.max(max_x),
418 cy1.max(max_y),
419 ),
420 None => (min_x, min_y, max_x, max_y),
421 });
422 }
423 }
424 }
425 }
426
427 let region = dirty_bounds.and_then(|(x0, y0, x1, y1)| DirtyRegion::from_bounds(x0, y0, x1, y1));
428 Ok(region)
429}
430
431#[allow(clippy::too_many_arguments)]
440#[cfg(feature = "textured")]
441pub fn execute_commands_with_dirty_region_effects_textured<D, const MAX: usize, const N: usize>(
442 fb: &mut D,
443 frame: &mut FrameCtx<'_>,
444 cmd: &CommandBuffer<MAX>,
445 texture_manager: &crate::texture::TextureManager<N>,
446 fog: Option<&FogConfig>,
447 dither: Option<&DitherConfig>,
448 screen_tint: Option<ScreenTint>,
449 stipple_mode: StippleMode,
450 palette_mode: PaletteMode,
451 sky: Option<SkyConfig>,
452 camera_dir: [f32; 3],
453) -> Result<Option<DirtyRegion>, RenderError>
454where
455 D: DrawTarget<Color = Rgb565> + OriginDimensions,
456 D::Error: Debug,
457{
458 use crate::draw::{draw_zbuffered_lightmapped_mapped, draw_zbuffered_with_textures_mapped};
459 use crate::retro::TextureMapping;
460
461 frame.validate()?;
462 let mut dirty_bounds: Option<(i32, i32, i32, i32)> = None;
463 if let Some(sky_cfg) = sky {
464 draw_sky_background(
465 fb,
466 frame.width,
467 frame.height,
468 sky_cfg,
469 camera_dir,
470 screen_tint,
471 palette_mode,
472 )?;
473 dirty_bounds = Some((
474 0,
475 0,
476 frame.width.saturating_sub(1) as i32,
477 frame.height.saturating_sub(1) as i32,
478 ));
479 }
480
481 for c in cmd.iter() {
482 match c {
483 RenderCommand::ClearColor(color) => {
484 let w = frame.width as i32;
485 let h = frame.height as i32;
486 let clear_color = apply_post(*color, screen_tint, palette_mode);
487 for y in 0..h {
488 for x in 0..w {
489 fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
490 .map_err(|_| {
491 RenderError::InvalidInput("draw target rejected clear write")
492 })?;
493 }
494 }
495 }
496 RenderCommand::ClearDepth(value) => {
497 crate::clear_zbuffer(frame.zbuffer, *value);
498 }
499 RenderCommand::Draw(primitive) => {
500 match primitive {
501 #[cfg(feature = "textured")]
502 DrawPrimitive::LightmappedTriangle {
503 points,
504 depths,
505 ws,
506 surface_uvs,
507 lm_uvs,
508 texture_id,
509 lightmap_id,
510 brightness,
511 dynamic_tint,
512 } => {
513 draw_zbuffered_lightmapped_mapped(
514 *points,
515 *depths,
516 *ws,
517 *surface_uvs,
518 *lm_uvs,
519 *texture_id,
520 *lightmap_id,
521 *brightness,
522 *dynamic_tint,
523 fog,
524 texture_manager,
525 fb,
526 frame.zbuffer,
527 frame.width,
528 TextureMapping::PerspectiveCorrect,
529 stipple_mode,
530 screen_tint,
531 palette_mode,
532 );
533 }
534 #[cfg(feature = "textured")]
535 DrawPrimitive::TexturedTriangle { .. }
536 | DrawPrimitive::TexturedTriangleWithDepth { .. }
537 | DrawPrimitive::TexturedGouraudTriangleWithDepth { .. } => {
538 draw_zbuffered_with_textures_mapped(
539 primitive.clone(),
540 fb,
541 frame.zbuffer,
542 frame.width,
543 texture_manager,
544 fog,
545 dither,
546 TextureMapping::PerspectiveCorrect,
547 stipple_mode,
548 screen_tint,
549 palette_mode,
550 );
551 }
552 _ => {
553 let prim = tint_primitive(primitive, screen_tint, palette_mode);
554 draw_zbuffered_with_effects(
555 prim,
556 fb,
557 frame.zbuffer,
558 frame.width,
559 fog,
560 dither,
561 );
562 }
563 }
564 let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
565 if let Some((min_x, min_y, max_x, max_y)) =
566 clamp_bounds_to_frame(min_x, min_y, max_x, max_y, frame.width, frame.height)
567 {
568 dirty_bounds = Some(match dirty_bounds {
569 Some((cx0, cy0, cx1, cy1)) => (
570 cx0.min(min_x),
571 cy0.min(min_y),
572 cx1.max(max_x),
573 cy1.max(max_y),
574 ),
575 None => (min_x, min_y, max_x, max_y),
576 });
577 }
578 }
579 }
580 }
581
582 let region = dirty_bounds.and_then(|(x0, y0, x1, y1)| DirtyRegion::from_bounds(x0, y0, x1, y1));
583 Ok(region)
584}
585
586pub fn execute_commands_tiled<D, const MAX: usize, const BIN_CAP: usize>(
587 fb: &mut D,
588 frame: &mut FrameCtx<'_>,
589 cmd: &CommandBuffer<MAX>,
590 tile: crate::tilebin::TileConfig,
591 fog: Option<&FogConfig>,
592) -> Result<crate::tilebin::TileBinStats, RenderError>
593where
594 D: DrawTarget<Color = Rgb565> + OriginDimensions,
595 D::Error: Debug,
596{
597 execute_commands_tiled_effects::<D, MAX, BIN_CAP>(
598 fb,
599 frame,
600 cmd,
601 tile,
602 fog,
603 None,
604 None,
605 StippleMode::Off,
606 PaletteMode::Off,
607 None,
608 [0.0, 0.0, -1.0],
609 )
610}
611
612pub fn execute_commands_tiled_effects<D, const MAX: usize, const BIN_CAP: usize>(
613 fb: &mut D,
614 frame: &mut FrameCtx<'_>,
615 cmd: &CommandBuffer<MAX>,
616 tile: crate::tilebin::TileConfig,
617 fog: Option<&FogConfig>,
618 dither: Option<&DitherConfig>,
619 screen_tint: Option<ScreenTint>,
620 _stipple_mode: StippleMode,
621 palette_mode: PaletteMode,
622 sky: Option<SkyConfig>,
623 camera_dir: [f32; 3],
624) -> Result<crate::tilebin::TileBinStats, RenderError>
625where
626 D: DrawTarget<Color = Rgb565> + OriginDimensions,
627 D::Error: Debug,
628{
629 frame.validate()?;
630 if let Some(sky_cfg) = sky {
631 draw_sky_background(
632 fb,
633 frame.width,
634 frame.height,
635 sky_cfg,
636 camera_dir,
637 screen_tint,
638 palette_mode,
639 )?;
640 }
641 let (bins, stats) =
642 crate::tilebin::build_bins::<MAX, BIN_CAP>(cmd, frame.width, frame.height, tile)?;
643 let mut executed_draw = [false; MAX];
644
645 for command in cmd.iter() {
646 match command {
647 RenderCommand::ClearColor(color) => {
648 let w = frame.width as i32;
649 let h = frame.height as i32;
650 let clear_color = apply_post(*color, screen_tint, palette_mode);
651 for y in 0..h {
652 for x in 0..w {
653 fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
654 .map_err(|_| {
655 RenderError::InvalidInput("draw target rejected clear write")
656 })?;
657 }
658 }
659 }
660 RenderCommand::ClearDepth(value) => crate::clear_zbuffer(frame.zbuffer, *value),
661 RenderCommand::Draw(_) => {}
662 }
663 }
664
665 for bin in bins.iter() {
666 for idx in bin.iter().copied() {
667 if idx >= MAX || executed_draw[idx] {
668 continue;
669 }
670 let Some(RenderCommand::Draw(primitive)) = cmd.get(idx) else {
671 continue;
672 };
673 let prim = tint_primitive(primitive, screen_tint, palette_mode);
674 draw_zbuffered_with_effects(prim, fb, frame.zbuffer, frame.width, fog, dither);
675 executed_draw[idx] = true;
676 }
677 }
678
679 Ok(stats)
680}
681
682#[cfg(feature = "aa")]
684pub fn execute_commands_2xssaa<D, const MAX: usize>(
685 fb: &mut D,
686 frame: &mut FrameCtx<'_>,
687 cmd_buf: &CommandBuffer<MAX>,
688) -> Result<(), RenderError>
689where
690 D: DrawTarget<Color = Rgb565> + crate::draw::ReadPixel,
691 <D as DrawTarget>::Error: Debug,
692{
693 frame.validate()?;
694 for c in cmd_buf.iter() {
695 if let RenderCommand::Draw(primitive) = c {
696 crate::draw::draw_zbuffered_2xssaa(primitive.clone(), fb, frame.zbuffer, frame.width);
697 }
698 }
699 Ok(())
700}