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