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