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 command_buffer::{CommandBuffer, RenderCommand},
12 draw::{DitherConfig, FogConfig, draw_zbuffered_with_effects},
13 error::{BudgetKind, RenderError},
14 primitive::DrawPrimitive,
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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct PickQuery {
62 pub x: i32,
63 pub y: i32,
64}
65
66impl PickQuery {
67 pub const fn new(x: i32, y: i32) -> Self {
68 Self { x, y }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct PickResult {
75 pub x: i32,
77 pub y: i32,
79 pub depth: crate::ZDepth,
81 pub command_index: usize,
83}
84
85#[inline(always)]
86fn primitive_bounds(primitive: &DrawPrimitive) -> (i32, i32, i32, i32) {
87 primitive.bounds()
88}
89
90fn clamp_bounds_to_frame(
91 min_x: i32,
92 min_y: i32,
93 max_x: i32,
94 max_y: i32,
95 width: usize,
96 height: usize,
97) -> Option<(i32, i32, i32, i32)> {
98 let w = width as i32;
99 let h = height as i32;
100 let clamped_min_x = min_x.clamp(0, w.saturating_sub(1));
101 let clamped_min_y = min_y.clamp(0, h.saturating_sub(1));
102 let clamped_max_x = max_x.clamp(0, w.saturating_sub(1));
103 let clamped_max_y = max_y.clamp(0, h.saturating_sub(1));
104 if clamped_max_x < clamped_min_x || clamped_max_y < clamped_min_y {
105 return None;
106 }
107 Some((clamped_min_x, clamped_min_y, clamped_max_x, clamped_max_y))
108}
109
110#[inline]
111fn apply_post(color: Rgb565, tint: Option<ScreenTint>, palette_mode: PaletteMode) -> Rgb565 {
112 let tinted = if let Some(t) = tint {
113 t.apply(color)
114 } else {
115 color
116 };
117 palette_mode.apply(tinted)
118}
119
120fn tint_primitive(
121 primitive: &DrawPrimitive,
122 tint: Option<ScreenTint>,
123 palette_mode: PaletteMode,
124) -> DrawPrimitive {
125 match primitive.clone() {
126 DrawPrimitive::ColoredPoint(p, color) => {
127 DrawPrimitive::ColoredPoint(p, apply_post(color, tint, palette_mode))
128 }
129 DrawPrimitive::Line(points, color) => {
130 DrawPrimitive::Line(points, apply_post(color, tint, palette_mode))
131 }
132 DrawPrimitive::ColoredTriangle(points, color) => {
133 DrawPrimitive::ColoredTriangle(points, apply_post(color, tint, palette_mode))
134 }
135 DrawPrimitive::ColoredTriangleWithDepth {
136 points,
137 depths,
138 color,
139 } => DrawPrimitive::ColoredTriangleWithDepth {
140 points,
141 depths,
142 color: apply_post(color, tint, palette_mode),
143 },
144 DrawPrimitive::TranslucentTriangleWithDepth {
145 points,
146 depths,
147 color,
148 alpha,
149 } => DrawPrimitive::TranslucentTriangleWithDepth {
150 points,
151 depths,
152 color: apply_post(color, tint, palette_mode),
153 alpha,
154 },
155 DrawPrimitive::ScreenDoorTriangleWithDepth {
156 points,
157 depths,
158 color,
159 alpha,
160 } => DrawPrimitive::ScreenDoorTriangleWithDepth {
161 points,
162 depths,
163 color: apply_post(color, tint, palette_mode),
164 alpha,
165 },
166 #[cfg(feature = "lighting")]
167 DrawPrimitive::GouraudTriangle { points, colors } => DrawPrimitive::GouraudTriangle {
168 points,
169 colors: [
170 apply_post(colors[0], tint, palette_mode),
171 apply_post(colors[1], tint, palette_mode),
172 apply_post(colors[2], tint, palette_mode),
173 ],
174 },
175 #[cfg(feature = "lighting")]
176 DrawPrimitive::GouraudTriangleWithDepth {
177 points,
178 depths,
179 colors,
180 } => DrawPrimitive::GouraudTriangleWithDepth {
181 points,
182 depths,
183 colors: [
184 apply_post(colors[0], tint, palette_mode),
185 apply_post(colors[1], tint, palette_mode),
186 apply_post(colors[2], tint, palette_mode),
187 ],
188 },
189 #[cfg(feature = "textured")]
190 DrawPrimitive::LightmappedTriangle {
191 points,
192 depths,
193 ws,
194 surface_uvs,
195 lm_uvs,
196 texture_id,
197 lightmap_id,
198 brightness,
199 dynamic_tint,
200 } => DrawPrimitive::LightmappedTriangle {
201 points,
202 depths,
203 ws,
204 surface_uvs,
205 lm_uvs,
206 texture_id,
207 lightmap_id,
208 brightness,
209 dynamic_tint: apply_post(dynamic_tint, tint, palette_mode),
210 },
211 #[cfg(feature = "textured")]
212 DrawPrimitive::TexturedGouraudTriangleWithDepth {
213 points,
214 depths,
215 ws,
216 uvs,
217 colors,
218 texture_id,
219 } => DrawPrimitive::TexturedGouraudTriangleWithDepth {
220 points,
221 depths,
222 ws,
223 uvs,
224 colors: [
225 apply_post(colors[0], tint, palette_mode),
226 apply_post(colors[1], tint, palette_mode),
227 apply_post(colors[2], tint, palette_mode),
228 ],
229 texture_id,
230 },
231 #[cfg(feature = "textured")]
232 other => other,
233 }
234}
235
236#[inline]
237fn blend_rgb565(a: Rgb565, b: Rgb565, t_q8: u16) -> Rgb565 {
238 let inv = 255u16.saturating_sub(t_q8);
239 let r = ((a.r() as u16 * inv + b.r() as u16 * t_q8) / 255) as u8;
240 let g = ((a.g() as u16 * inv + b.g() as u16 * t_q8) / 255) as u8;
241 let bch = ((a.b() as u16 * inv + b.b() as u16 * t_q8) / 255) as u8;
242 Rgb565::new(r, g, bch)
243}
244
245#[inline]
246fn stripe_on_at(x: i32, scroll: i32, stripe_w: i32) -> bool {
247 (((x + scroll).div_euclid(stripe_w)) & 1) == 0
248}
249
250fn draw_sky_background<D>(
251 fb: &mut D,
252 width: usize,
253 height: usize,
254 sky: SkyConfig,
255 camera_dir: [f32; 3],
256 screen_tint: Option<ScreenTint>,
257 palette_mode: PaletteMode,
258) -> Result<(), RenderError>
259where
260 D: DrawTarget<Color = Rgb565> + OriginDimensions,
261 D::Error: Debug,
262{
263 let w = width as i32;
264 let h = height as i32;
265 if w <= 0 || h <= 0 {
266 return Ok(());
267 }
268
269 let horizon = (h as f32 * (0.5 + camera_dir[1].clamp(-1.0, 1.0) * 0.25)) as i32;
270 let stripe_w = sky.stripe_width.max(1) as i32;
271 let scroll = (camera_dir[0] * 128.0) as i32;
272 let stripe_fade_span = (h / 6).max(1);
273
274 for y in 0..h {
275 let dy = (y - horizon + h / 2).clamp(0, h);
276 let t_q8 = ((dy as i64 * 255) / h.max(1) as i64) as u16;
277 let base = blend_rgb565(sky.top_color, sky.bottom_color, t_q8);
278 let below_horizon = (y - horizon).max(0);
279 let stripe_strength = if below_horizon == 0 {
280 sky.stripe_strength as u16
281 } else if below_horizon >= stripe_fade_span {
282 0
283 } else {
284 let rem = stripe_fade_span - below_horizon;
285 ((sky.stripe_strength as i32 * rem) / stripe_fade_span) as u16
286 };
287 for x in 0..w {
288 let stripe_on = stripe_on_at(x, scroll, stripe_w);
289 let mut color = if stripe_on && stripe_strength > 0 {
290 blend_rgb565(base, sky.stripe_color, stripe_strength)
291 } else {
292 base
293 };
294 color = apply_post(color, screen_tint, palette_mode);
295 fb.draw_iter([Pixel(Point::new(x, y), color)])
296 .map_err(|_| RenderError::InvalidInput("draw target rejected sky write"))?;
297 }
298 }
299 Ok(())
300}
301
302#[cfg(test)]
303mod tests {
304 extern crate std;
305 use super::stripe_on_at;
306
307 #[test]
308 fn stripe_phase_is_periodic_with_negative_scroll() {
309 let stripe_w = 10;
310 let scroll = -3;
311 for x in -40..40 {
312 assert_eq!(
313 stripe_on_at(x, scroll, stripe_w),
314 stripe_on_at(x + stripe_w * 2, scroll, stripe_w)
315 );
316 }
317 }
318
319 #[test]
320 fn stripe_runs_do_not_exceed_width() {
321 let stripe_w = 8;
322 let scroll = -5;
323 let mut max_run = 0usize;
324 let mut run = 0usize;
325 let mut prev = stripe_on_at(-64, scroll, stripe_w);
326 for x in -63..=64 {
327 let cur = stripe_on_at(x, scroll, stripe_w);
328 if cur == prev {
329 run += 1;
330 } else {
331 max_run = max_run.max(run);
332 run = 1;
333 prev = cur;
334 }
335 }
336 max_run = max_run.max(run);
337 assert!(max_run <= stripe_w as usize);
338 }
339
340 #[test]
341 fn test_execute_commands_with_picking() {
342 use crate::command_buffer::{CommandBuffer, RenderCommand};
343 use crate::primitive::DrawPrimitive;
344 use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
345 use embedded_graphics_framebuf::{
346 FrameBuf,
347 backends::{EndianCorrectedBuffer, EndianCorrection},
348 };
349 use nalgebra::Point2;
350
351 let backing = std::vec![Rgb565::BLACK; 64 * 64].leak();
352 let mut fb = FrameBuf::new(
353 EndianCorrectedBuffer::new(backing, EndianCorrection::ToLittleEndian),
354 64,
355 64,
356 );
357 let mut zbuf = [crate::Z_MAX_VALUE; 64 * 64];
358 let mut frame = super::FrameCtx {
359 zbuffer: &mut zbuf,
360 width: 64,
361 height: 64,
362 };
363
364 let mut cmd = CommandBuffer::<8>::new();
365 cmd.push(RenderCommand::Draw(
366 DrawPrimitive::ColoredTriangleWithDepth {
367 points: [
368 Point2::new(10, 10),
369 Point2::new(40, 10),
370 Point2::new(25, 40),
371 ],
372 depths: [10.0, 10.0, 10.0],
373 color: Rgb565::RED,
374 },
375 ))
376 .unwrap();
377
378 let queries = [super::PickQuery::new(25, 20), super::PickQuery::new(0, 0)];
379 let mut results = [None, None];
380
381 let region =
382 super::execute_commands_with_picking(&mut fb, &mut frame, &cmd, &queries, &mut results)
383 .unwrap();
384
385 assert!(region.is_some());
386 assert!(results[0].is_some());
387 let hit = results[0].unwrap();
388 assert_eq!(hit.x, 25);
389 assert_eq!(hit.y, 20);
390 assert_eq!(hit.command_index, 0);
391 assert!(results[1].is_none()); }
393
394 #[test]
395 fn test_execute_commands_variants() {
396 use crate::command_buffer::{CommandBuffer, RenderCommand};
397 use crate::primitive::DrawPrimitive;
398 use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
399 use embedded_graphics_framebuf::{
400 FrameBuf,
401 backends::{EndianCorrectedBuffer, EndianCorrection},
402 };
403 use nalgebra::Point2;
404
405 let backing = std::vec![Rgb565::BLACK; 16 * 16].leak();
406 let mut fb = FrameBuf::new(
407 EndianCorrectedBuffer::new(backing, EndianCorrection::ToLittleEndian),
408 16,
409 16,
410 );
411 let mut zbuf = [crate::Z_MAX_VALUE; 16 * 16];
412 let mut frame = super::FrameCtx {
413 zbuffer: &mut zbuf,
414 width: 16,
415 height: 16,
416 };
417
418 let mut cmd = CommandBuffer::<4>::new();
419 cmd.push(RenderCommand::ClearColor(Rgb565::BLUE)).unwrap();
420 cmd.push(RenderCommand::ClearDepth(crate::Z_MAX_VALUE))
421 .unwrap();
422 cmd.push(RenderCommand::Draw(
423 DrawPrimitive::ColoredTriangleWithDepth {
424 points: [Point2::new(2, 2), Point2::new(12, 2), Point2::new(7, 12)],
425 depths: [10.0; 3],
426 color: Rgb565::RED,
427 },
428 ))
429 .unwrap();
430
431 assert!(super::execute_commands(&mut fb, &mut frame, &cmd, None).is_ok());
432
433 let dirty =
434 super::execute_commands_with_dirty_region(&mut fb, &mut frame, &cmd, None).unwrap();
435 assert!(dirty.is_some());
436 let dirty = dirty.unwrap();
437 assert!(dirty.width >= 1);
438 assert!(dirty.height >= 1);
439
440 let region = super::execute_commands_with_dirty_region_effects(
441 &mut fb,
442 &mut frame,
443 &cmd,
444 None,
445 None,
446 None,
447 crate::retro::StippleMode::Off,
448 crate::retro::PaletteMode::Off,
449 None,
450 [0.0, 0.0, -1.0],
451 )
452 .unwrap();
453 assert!(region.is_some());
454
455 let full_region = super::execute_commands_with_dirty_region_effects(
457 &mut fb,
458 &mut frame,
459 &cmd,
460 None,
461 None,
462 None,
463 crate::retro::StippleMode::Off,
464 crate::retro::PaletteMode::Off,
465 Some(crate::retro::SkyConfig::retro_blue()),
466 [0.0, 0.0, -1.0],
467 )
468 .unwrap();
469 let full = full_region.unwrap();
470 assert_eq!(full.x, 0);
471 assert_eq!(full.y, 0);
472 assert_eq!(full.width, 16);
473 assert_eq!(full.height, 16);
474 }
475
476 #[test]
477 fn test_execute_commands_tiled_and_frame_validation() {
478 use crate::command_buffer::{CommandBuffer, RenderCommand};
479 use crate::primitive::DrawPrimitive;
480 use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
481 use embedded_graphics_framebuf::{
482 FrameBuf,
483 backends::{EndianCorrectedBuffer, EndianCorrection},
484 };
485 use nalgebra::Point2;
486
487 let backing = std::vec![Rgb565::BLACK; 16 * 16].leak();
488 let mut fb = FrameBuf::new(
489 EndianCorrectedBuffer::new(backing, EndianCorrection::ToLittleEndian),
490 16,
491 16,
492 );
493
494 let mut short_zbuf = [crate::Z_MAX_VALUE; 4];
496 let mut bad_frame = super::FrameCtx {
497 zbuffer: &mut short_zbuf,
498 width: 16,
499 height: 16,
500 };
501 let empty = CommandBuffer::<1>::new();
502 assert!(super::execute_commands(&mut fb, &mut bad_frame, &empty, None).is_err());
503
504 let mut zbuf = [crate::Z_MAX_VALUE; 16 * 16];
505 let mut frame = super::FrameCtx {
506 zbuffer: &mut zbuf,
507 width: 16,
508 height: 16,
509 };
510
511 let mut cmd = CommandBuffer::<2>::new();
512 cmd.push(RenderCommand::Draw(
513 DrawPrimitive::ColoredTriangleWithDepth {
514 points: [Point2::new(3, 3), Point2::new(13, 3), Point2::new(8, 13)],
515 depths: [5.0; 3],
516 color: Rgb565::GREEN,
517 },
518 ))
519 .unwrap();
520
521 let stats = super::execute_commands_tiled::<_, 2, 16>(
522 &mut fb,
523 &mut frame,
524 &cmd,
525 crate::tilebin::TileConfig {
526 tile_width: 8,
527 tile_height: 8,
528 },
529 None,
530 )
531 .unwrap();
532 assert!(stats.draw_commands >= 1);
533 assert!(stats.bins_used >= 1);
534 }
535}
536
537pub fn execute_commands<D, const MAX: usize>(
538 fb: &mut D,
539 frame: &mut FrameCtx<'_>,
540 cmd: &CommandBuffer<MAX>,
541 fog: Option<&FogConfig>,
542) -> Result<(), RenderError>
543where
544 D: DrawTarget<Color = Rgb565> + OriginDimensions,
545 D::Error: Debug,
546{
547 let _ = execute_commands_with_dirty_region_effects(
548 fb,
549 frame,
550 cmd,
551 fog,
552 None,
553 None,
554 StippleMode::Off,
555 PaletteMode::Off,
556 None,
557 [0.0, 0.0, -1.0],
558 )?;
559 Ok(())
560}
561
562pub fn execute_commands_with_dirty_region<D, const MAX: usize>(
563 fb: &mut D,
564 frame: &mut FrameCtx<'_>,
565 cmd: &CommandBuffer<MAX>,
566 fog: Option<&FogConfig>,
567) -> Result<Option<DirtyRegion>, RenderError>
568where
569 D: DrawTarget<Color = Rgb565> + OriginDimensions,
570 D::Error: Debug,
571{
572 execute_commands_with_dirty_region_effects(
573 fb,
574 frame,
575 cmd,
576 fog,
577 None,
578 None,
579 StippleMode::Off,
580 PaletteMode::Off,
581 None,
582 [0.0, 0.0, -1.0],
583 )
584}
585
586pub fn execute_commands_with_dirty_region_effects<D, const MAX: usize>(
587 fb: &mut D,
588 frame: &mut FrameCtx<'_>,
589 cmd: &CommandBuffer<MAX>,
590 fog: Option<&FogConfig>,
591 dither: Option<&DitherConfig>,
592 screen_tint: Option<ScreenTint>,
593 _stipple_mode: StippleMode,
594 palette_mode: PaletteMode,
595 sky: Option<SkyConfig>,
596 camera_dir: [f32; 3],
597) -> Result<Option<DirtyRegion>, RenderError>
598where
599 D: DrawTarget<Color = Rgb565> + OriginDimensions,
600 D::Error: Debug,
601{
602 frame.validate()?;
603 let mut dirty_bounds: Option<(i32, i32, i32, i32)> = None;
604 if let Some(sky_cfg) = sky {
605 draw_sky_background(
606 fb,
607 frame.width,
608 frame.height,
609 sky_cfg,
610 camera_dir,
611 screen_tint,
612 palette_mode,
613 )?;
614 dirty_bounds = Some((
615 0,
616 0,
617 frame.width.saturating_sub(1) as i32,
618 frame.height.saturating_sub(1) as i32,
619 ));
620 }
621
622 for c in cmd.iter() {
623 match c {
624 RenderCommand::ClearColor(color) => {
625 let w = frame.width as i32;
626 let h = frame.height as i32;
627 let clear_color = apply_post(*color, screen_tint, palette_mode);
628 for y in 0..h {
629 for x in 0..w {
630 fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
631 .map_err(|_| {
632 RenderError::InvalidInput("draw target rejected clear write")
633 })?;
634 }
635 }
636 }
637 RenderCommand::ClearDepth(value) => {
638 crate::clear_zbuffer(frame.zbuffer, *value);
639 }
640 RenderCommand::Draw(primitive) => {
641 let prim = tint_primitive(primitive, screen_tint, palette_mode);
642 draw_zbuffered_with_effects(prim, fb, frame.zbuffer, frame.width, fog, dither);
643 let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
644 if let Some((min_x, min_y, max_x, max_y)) =
645 clamp_bounds_to_frame(min_x, min_y, max_x, max_y, frame.width, frame.height)
646 {
647 dirty_bounds = Some(match dirty_bounds {
648 Some((cx0, cy0, cx1, cy1)) => (
649 cx0.min(min_x),
650 cy0.min(min_y),
651 cx1.max(max_x),
652 cy1.max(max_y),
653 ),
654 None => (min_x, min_y, max_x, max_y),
655 });
656 }
657 }
658 }
659 }
660
661 let region = dirty_bounds.and_then(|(x0, y0, x1, y1)| DirtyRegion::from_bounds(x0, y0, x1, y1));
662 Ok(region)
663}
664
665#[allow(clippy::too_many_arguments)]
674#[cfg(feature = "textured")]
675pub fn execute_commands_with_dirty_region_effects_textured<D, const MAX: usize, const N: usize>(
676 fb: &mut D,
677 frame: &mut FrameCtx<'_>,
678 cmd: &CommandBuffer<MAX>,
679 texture_manager: &crate::texture::TextureManager<N>,
680 fog: Option<&FogConfig>,
681 dither: Option<&DitherConfig>,
682 screen_tint: Option<ScreenTint>,
683 stipple_mode: StippleMode,
684 palette_mode: PaletteMode,
685 sky: Option<SkyConfig>,
686 camera_dir: [f32; 3],
687) -> Result<Option<DirtyRegion>, RenderError>
688where
689 D: DrawTarget<Color = Rgb565> + OriginDimensions,
690 D::Error: Debug,
691{
692 use crate::draw::{draw_zbuffered_lightmapped_mapped, draw_zbuffered_with_textures_mapped};
693 use crate::retro::TextureMapping;
694
695 frame.validate()?;
696 let mut dirty_bounds: Option<(i32, i32, i32, i32)> = None;
697 if let Some(sky_cfg) = sky {
698 draw_sky_background(
699 fb,
700 frame.width,
701 frame.height,
702 sky_cfg,
703 camera_dir,
704 screen_tint,
705 palette_mode,
706 )?;
707 dirty_bounds = Some((
708 0,
709 0,
710 frame.width.saturating_sub(1) as i32,
711 frame.height.saturating_sub(1) as i32,
712 ));
713 }
714
715 for c in cmd.iter() {
716 match c {
717 RenderCommand::ClearColor(color) => {
718 let w = frame.width as i32;
719 let h = frame.height as i32;
720 let clear_color = apply_post(*color, screen_tint, palette_mode);
721 for y in 0..h {
722 for x in 0..w {
723 fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
724 .map_err(|_| {
725 RenderError::InvalidInput("draw target rejected clear write")
726 })?;
727 }
728 }
729 }
730 RenderCommand::ClearDepth(value) => {
731 crate::clear_zbuffer(frame.zbuffer, *value);
732 }
733 RenderCommand::Draw(primitive) => {
734 match primitive {
735 #[cfg(feature = "textured")]
736 DrawPrimitive::LightmappedTriangle {
737 points,
738 depths,
739 ws,
740 surface_uvs,
741 lm_uvs,
742 texture_id,
743 lightmap_id,
744 brightness,
745 dynamic_tint,
746 } => {
747 draw_zbuffered_lightmapped_mapped(
748 *points,
749 *depths,
750 *ws,
751 *surface_uvs,
752 *lm_uvs,
753 *texture_id,
754 *lightmap_id,
755 *brightness,
756 *dynamic_tint,
757 fog,
758 texture_manager,
759 fb,
760 frame.zbuffer,
761 frame.width,
762 TextureMapping::PerspectiveCorrect,
763 stipple_mode,
764 screen_tint,
765 palette_mode,
766 );
767 }
768 #[cfg(feature = "textured")]
769 DrawPrimitive::TexturedTriangle { .. }
770 | DrawPrimitive::TexturedTriangleWithDepth { .. }
771 | DrawPrimitive::TexturedGouraudTriangleWithDepth { .. } => {
772 draw_zbuffered_with_textures_mapped(
773 primitive.clone(),
774 fb,
775 frame.zbuffer,
776 frame.width,
777 texture_manager,
778 fog,
779 dither,
780 TextureMapping::PerspectiveCorrect,
781 stipple_mode,
782 screen_tint,
783 palette_mode,
784 );
785 }
786 _ => {
787 let prim = tint_primitive(primitive, screen_tint, palette_mode);
788 draw_zbuffered_with_effects(
789 prim,
790 fb,
791 frame.zbuffer,
792 frame.width,
793 fog,
794 dither,
795 );
796 }
797 }
798 let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
799 if let Some((min_x, min_y, max_x, max_y)) =
800 clamp_bounds_to_frame(min_x, min_y, max_x, max_y, frame.width, frame.height)
801 {
802 dirty_bounds = Some(match dirty_bounds {
803 Some((cx0, cy0, cx1, cy1)) => (
804 cx0.min(min_x),
805 cy0.min(min_y),
806 cx1.max(max_x),
807 cy1.max(max_y),
808 ),
809 None => (min_x, min_y, max_x, max_y),
810 });
811 }
812 }
813 }
814 }
815
816 let region = dirty_bounds.and_then(|(x0, y0, x1, y1)| DirtyRegion::from_bounds(x0, y0, x1, y1));
817 Ok(region)
818}
819
820pub fn execute_commands_tiled<D, const MAX: usize, const BIN_CAP: usize>(
821 fb: &mut D,
822 frame: &mut FrameCtx<'_>,
823 cmd: &CommandBuffer<MAX>,
824 tile: crate::tilebin::TileConfig,
825 fog: Option<&FogConfig>,
826) -> Result<crate::tilebin::TileBinStats, RenderError>
827where
828 D: DrawTarget<Color = Rgb565> + OriginDimensions,
829 D::Error: Debug,
830{
831 execute_commands_tiled_effects::<D, MAX, BIN_CAP>(
832 fb,
833 frame,
834 cmd,
835 tile,
836 fog,
837 None,
838 None,
839 StippleMode::Off,
840 PaletteMode::Off,
841 None,
842 [0.0, 0.0, -1.0],
843 )
844}
845
846pub fn execute_commands_tiled_effects<D, const MAX: usize, const BIN_CAP: usize>(
847 fb: &mut D,
848 frame: &mut FrameCtx<'_>,
849 cmd: &CommandBuffer<MAX>,
850 tile: crate::tilebin::TileConfig,
851 fog: Option<&FogConfig>,
852 dither: Option<&DitherConfig>,
853 screen_tint: Option<ScreenTint>,
854 _stipple_mode: StippleMode,
855 palette_mode: PaletteMode,
856 sky: Option<SkyConfig>,
857 camera_dir: [f32; 3],
858) -> Result<crate::tilebin::TileBinStats, RenderError>
859where
860 D: DrawTarget<Color = Rgb565> + OriginDimensions,
861 D::Error: Debug,
862{
863 frame.validate()?;
864 if let Some(sky_cfg) = sky {
865 draw_sky_background(
866 fb,
867 frame.width,
868 frame.height,
869 sky_cfg,
870 camera_dir,
871 screen_tint,
872 palette_mode,
873 )?;
874 }
875 let (bins, stats) =
876 crate::tilebin::build_bins::<MAX, BIN_CAP>(cmd, frame.width, frame.height, tile)?;
877 let mut executed_draw = [false; MAX];
878
879 for command in cmd.iter() {
880 match command {
881 RenderCommand::ClearColor(color) => {
882 let w = frame.width as i32;
883 let h = frame.height as i32;
884 let clear_color = apply_post(*color, screen_tint, palette_mode);
885 for y in 0..h {
886 for x in 0..w {
887 fb.draw_iter([Pixel(Point::new(x, y), clear_color)])
888 .map_err(|_| {
889 RenderError::InvalidInput("draw target rejected clear write")
890 })?;
891 }
892 }
893 }
894 RenderCommand::ClearDepth(value) => crate::clear_zbuffer(frame.zbuffer, *value),
895 RenderCommand::Draw(_) => {}
896 }
897 }
898
899 for bin in bins.iter() {
900 for idx in bin.iter().copied() {
901 if idx >= MAX || executed_draw[idx] {
902 continue;
903 }
904 let Some(RenderCommand::Draw(primitive)) = cmd.get(idx) else {
905 continue;
906 };
907 let prim = tint_primitive(primitive, screen_tint, palette_mode);
908 draw_zbuffered_with_effects(prim, fb, frame.zbuffer, frame.width, fog, dither);
909 executed_draw[idx] = true;
910 }
911 }
912
913 Ok(stats)
914}
915
916#[cfg(feature = "aa")]
918pub fn execute_commands_2xssaa<D, const MAX: usize>(
919 fb: &mut D,
920 frame: &mut FrameCtx<'_>,
921 cmd_buf: &CommandBuffer<MAX>,
922) -> Result<(), RenderError>
923where
924 D: DrawTarget<Color = Rgb565> + crate::draw::ReadPixel,
925 <D as DrawTarget>::Error: Debug,
926{
927 frame.validate()?;
928 for c in cmd_buf.iter() {
929 if let RenderCommand::Draw(primitive) = c {
930 crate::draw::draw_zbuffered_2xssaa(primitive.clone(), fb, frame.zbuffer, frame.width);
931 }
932 }
933 Ok(())
934}
935
936pub fn execute_commands_with_picking<D, const MAX: usize>(
938 fb: &mut D,
939 frame: &mut FrameCtx<'_>,
940 cmd: &CommandBuffer<MAX>,
941 queries: &[PickQuery],
942 results: &mut [Option<PickResult>],
943) -> Result<Option<DirtyRegion>, RenderError>
944where
945 D: DrawTarget<Color = Rgb565>,
946 <D as DrawTarget>::Error: Debug,
947{
948 frame.validate()?;
949 let mut dirty_bounds: Option<(i32, i32, i32, i32)> = None;
950
951 for (cmd_idx, c) in cmd.iter().enumerate() {
952 match c {
953 RenderCommand::ClearColor(color) => {
954 let w = frame.width as i32;
955 let h = frame.height as i32;
956 for y in 0..h {
957 for x in 0..w {
958 fb.draw_iter([Pixel(Point::new(x, y), *color)])
959 .map_err(|_| {
960 RenderError::InvalidInput("draw target rejected clear write")
961 })?;
962 }
963 }
964 }
965 RenderCommand::ClearDepth(value) => {
966 crate::clear_zbuffer(frame.zbuffer, *value);
967 }
968 RenderCommand::Draw(primitive) => {
969 let mut prev_depths = [0 as crate::ZDepth; 16];
970 let check_count = queries.len().min(16).min(results.len());
971 for (q_i, query) in queries.iter().take(check_count).enumerate() {
972 if query.x >= 0
973 && query.x < frame.width as i32
974 && query.y >= 0
975 && query.y < frame.height as i32
976 {
977 let idx = query.y as usize * frame.width + query.x as usize;
978 prev_depths[q_i] = frame.zbuffer[idx];
979 }
980 }
981
982 crate::draw::draw_zbuffered(primitive.clone(), fb, frame.zbuffer, frame.width);
983
984 for (q_i, query) in queries.iter().take(check_count).enumerate() {
985 if query.x >= 0
986 && query.x < frame.width as i32
987 && query.y >= 0
988 && query.y < frame.height as i32
989 {
990 let idx = query.y as usize * frame.width + query.x as usize;
991 let new_depth = frame.zbuffer[idx];
992 if new_depth < prev_depths[q_i] {
993 results[q_i] = Some(PickResult {
994 x: query.x,
995 y: query.y,
996 depth: new_depth,
997 command_index: cmd_idx,
998 });
999 }
1000 }
1001 }
1002
1003 let (min_x, min_y, max_x, max_y) = primitive_bounds(primitive);
1004 if let Some((min_x, min_y, max_x, max_y)) =
1005 clamp_bounds_to_frame(min_x, min_y, max_x, max_y, frame.width, frame.height)
1006 {
1007 dirty_bounds = Some(match dirty_bounds {
1008 Some((cx0, cy0, cx1, cy1)) => (
1009 cx0.min(min_x),
1010 cy0.min(min_y),
1011 cx1.max(max_x),
1012 cy1.max(max_y),
1013 ),
1014 None => (min_x, min_y, max_x, max_y),
1015 });
1016 }
1017 }
1018 }
1019 }
1020
1021 let region = dirty_bounds.and_then(|(x0, y0, x1, y1)| DirtyRegion::from_bounds(x0, y0, x1, y1));
1022 Ok(region)
1023}