1use crate::{Error, Execution, PixelShader, ProgramShader, Sampler};
43use iridia::{InterpMode, SystemValue, Varying};
44
45fn error(detail: impl Into<String>) -> Error {
46 Error {
47 bytecode: "draw",
48 at: 0,
49 detail: detail.into(),
50 }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq)]
55pub struct Viewport {
56 pub x: f32,
58 pub y: f32,
60 pub width: f32,
62 pub height: f32,
64 pub min_depth: f32,
66 pub max_depth: f32,
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum Cull {
73 None,
75 Front,
77 Back,
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum Compare {
84 Never,
86 Less,
88 Equal,
90 LessEqual,
92 Greater,
94 NotEqual,
96 GreaterEqual,
98 Always,
100}
101
102impl Compare {
103 fn passes(self, pixel: f32, stored: f32) -> bool {
104 match self {
105 Compare::Never => false,
106 Compare::Less => pixel < stored,
107 Compare::Equal => pixel == stored,
108 Compare::LessEqual => pixel <= stored,
109 Compare::Greater => pixel > stored,
110 Compare::NotEqual => pixel != stored,
111 Compare::GreaterEqual => pixel >= stored,
112 Compare::Always => true,
113 }
114 }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct DepthState {
120 pub compare: Compare,
122 pub write: bool,
124}
125
126#[derive(Clone, Copy, Debug, PartialEq)]
128pub struct Rasterizer {
129 pub viewport: Viewport,
131 pub cull: Cull,
133 pub front_counter_clockwise: bool,
135 pub depth: Option<DepthState>,
137}
138
139#[derive(Clone, Debug, PartialEq)]
142pub struct Surface {
143 pub width: u32,
145 pub height: u32,
147 pub targets: Vec<Option<Vec<[u32; 4]>>>,
149 pub depth: Option<Vec<f32>>,
151}
152
153#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
155pub struct DrawStats {
156 pub triangles: usize,
158 pub culled: usize,
160 pub clipped_away: usize,
162 pub not_a_number: usize,
164 pub shaded: usize,
166 pub discarded: usize,
168 pub depth_failed: usize,
170 pub written: usize,
172}
173
174#[derive(Clone, Debug)]
176enum Source {
177 Varying {
179 from_register: usize,
180 from_lanes: Vec<u8>,
181 to_register: usize,
182 to_lanes: Vec<u8>,
183 mode: InterpMode,
184 },
185 Position {
186 register: usize,
187 lanes: Vec<u8>,
188 },
189 FrontFace {
190 register: usize,
191 lane: u8,
192 },
193}
194
195fn lanes_of(mask: u8) -> Vec<u8> {
196 (0..4).filter(|l| mask >> l & 1 != 0).collect()
197}
198
199fn same_label(a: &Varying, b: &Varying) -> bool {
200 a.label.index == b.label.index && a.label.name.eq_ignore_ascii_case(&b.label.name)
201}
202
203#[derive(Debug)]
205pub struct DrawShader<'a> {
206 vertex: &'a ProgramShader,
207 pixel: &'a ProgramShader,
208 sources: Vec<Source>,
209}
210
211#[derive(Clone, Debug)]
213struct Shaded {
214 position: [f32; 4],
215 outputs: Vec<[u32; 4]>,
216}
217
218#[derive(Clone, Debug)]
221struct ClipVertex {
222 clip: [f32; 4],
223 weights: [f64; 3],
224}
225
226fn screen_weights(clip: [f64; 3], w: [f64; 3]) -> [f64; 3] {
230 let scaled = [clip[0] * w[0], clip[1] * w[1], clip[2] * w[2]];
231 let sum = scaled[0] + scaled[1] + scaled[2];
232 scaled.map(|x| x / sum)
233}
234
235impl<'a> DrawShader<'a> {
236 pub fn new(vertex: &'a ProgramShader, pixel: &'a ProgramShader) -> Result<Self, Error> {
238 let mut sources = Vec::new();
239 for v in pixel.interface().inputs.iter().filter(|v| v.used) {
240 let Some(sig) = v.provenance.d3d else {
241 return Err(error(format!(
242 "pixel input {}{} has no register",
243 v.label.name, v.label.index
244 )));
245 };
246 let to_register = sig.register as usize;
247 let to_lanes = lanes_of(sig.mask);
248 match v.system {
249 Some(SystemValue::FragCoord) => {
250 sources.push(Source::Position {
251 register: to_register,
252 lanes: to_lanes,
253 });
254 continue;
255 }
256 Some(SystemValue::FrontFacing) => {
257 let Some(&lane) = to_lanes.first() else {
259 return Err(error(format!(
260 "pixel input {}{} is the facing, and its signature mask names no lane",
261 v.label.name, v.label.index
262 )));
263 };
264 sources.push(Source::FrontFace {
265 register: to_register,
266 lane,
267 });
268 continue;
269 }
270 Some(other) => {
271 return Err(error(format!(
272 "pixel input {}{} is the system value {other:?}, which a draw does not supply",
273 v.label.name, v.label.index
274 )));
275 }
276 None => {}
277 }
278 let out = vertex
279 .interface()
280 .outputs
281 .iter()
282 .find(|o| o.system.is_none() && same_label(o, v))
283 .ok_or_else(|| {
284 error(format!(
285 "pixel input {}{} has no vertex output of its semantic",
286 v.label.name, v.label.index
287 ))
288 })?;
289 let from = out.provenance.d3d.ok_or_else(|| {
290 error(format!(
291 "vertex output {}{} has no register",
292 out.label.name, out.label.index
293 ))
294 })?;
295 let mode = match v.interp {
296 Some(i) => i.mode,
297 None if v.value.scalar == iridia::ScalarType::Float => InterpMode::Perspective,
298 None => InterpMode::Flat,
299 };
300 sources.push(Source::Varying {
301 from_register: from.register as usize,
302 from_lanes: lanes_of(from.mask),
303 to_register,
304 to_lanes,
305 mode,
306 });
307 }
308 Ok(Self {
309 vertex,
310 pixel,
311 sources,
312 })
313 }
314
315 #[allow(clippy::too_many_arguments)]
317 pub fn draw_indexed(
318 &self,
319 vertices: &[Vec<[u32; 4]>],
320 indices: &[u32],
321 vertex_buffers: &[(u32, &[[u32; 4]])],
322 pixel_buffers: &[(u32, &[[u32; 4]])],
323 sampler: &mut dyn Sampler,
324 state: &Rasterizer,
325 surface: &mut Surface,
326 ) -> Result<DrawStats, Error> {
327 if !indices.len().is_multiple_of(3) {
328 return Err(error(format!(
329 "{} indices are not whole triangles",
330 indices.len()
331 )));
332 }
333 let n = (surface.width as usize).saturating_mul(surface.height as usize);
334 if surface.targets.iter().flatten().any(|t| t.len() != n)
335 || surface.depth.as_ref().is_some_and(|d| d.len() != n)
336 {
337 return Err(error("a target or the depth buffer is not width x height"));
338 }
339 if state.depth.is_some() && surface.depth.is_none() {
340 return Err(error("the state tests depth and the surface has no depth buffer"));
341 }
342 let mut shaded: Vec<Option<Shaded>> = vec![None; vertices.len()];
344 let mut stats = DrawStats::default();
345 for triangle in indices.as_chunks::<3>().0 {
346 stats.triangles = stats.triangles.saturating_add(1);
347 let mut drawn = [0usize; 3];
348 for (slot, &i) in drawn.iter_mut().zip(triangle) {
349 let i = i as usize;
350 let input = vertices.get(i).ok_or_else(|| {
351 error(format!(
352 "index {i} names a vertex past the {} given",
353 vertices.len()
354 ))
355 })?;
356 let entry = shaded
357 .get_mut(i)
358 .ok_or_else(|| error(format!("vertex {i} has no room for its shading")))?;
359 if entry.is_none() {
360 let (e, position) = self.vertex.execute_vertex(input, vertex_buffers, sampler)?;
361 *entry = Some(Shaded {
362 position: position.map(f32::from_bits),
363 outputs: e.outputs,
364 });
365 }
366 *slot = i;
367 }
368 let [first, second, third] = drawn;
369 let (Some(Some(first)), Some(Some(second)), Some(Some(third))) =
370 (shaded.get(first), shaded.get(second), shaded.get(third))
371 else {
372 return Err(error("a vertex of a triangle was not shaded"));
373 };
374 let original = [first, second, third];
375 let corners = original
376 .iter()
377 .enumerate()
378 .map(|(k, vertex)| {
379 let mut weights = [0.; 3];
380 if let Some(weight) = weights.get_mut(k) {
381 *weight = 1.;
382 }
383 ClipVertex {
384 clip: vertex.position,
385 weights,
386 }
387 })
388 .collect();
389 let polygon = clip(corners);
390 if polygon.len() < 3 {
391 stats.clipped_away = stats.clipped_away.saturating_add(1);
392 continue;
393 }
394 if polygon.iter().any(|v| v.clip.iter().any(|c| c.is_nan())) {
395 stats.not_a_number = stats.not_a_number.saturating_add(1);
396 continue;
397 }
398 if let Some((fan, rest)) = polygon.split_first() {
399 for pair in rest.windows(2) {
400 if let [second, third] = pair {
401 let tri = [fan, second, third];
402 self.raster(tri, original, pixel_buffers, sampler, state, surface, &mut stats)?;
403 }
404 }
405 }
406 }
407 Ok(stats)
408 }
409
410 #[allow(clippy::too_many_arguments)]
411 fn raster(
412 &self,
413 tri: [&ClipVertex; 3],
414 original: [&Shaded; 3],
415 buffers: &[(u32, &[[u32; 4]])],
416 sampler: &mut dyn Sampler,
417 state: &Rasterizer,
418 surface: &mut Surface,
419 stats: &mut DrawStats,
420 ) -> Result<(), Error> {
421 let vp = state.viewport;
422 let Some(setup) = Setup::new(tri, &vp) else {
423 return Ok(());
424 };
425 let front = setup.clockwise != state.front_counter_clockwise;
426 if (state.cull == Cull::Front && front) || (state.cull == Cull::Back && !front) {
427 stats.culled = stats.culled.saturating_add(1);
428 return Ok(());
429 }
430 let (w, h) = (i64::from(surface.width), i64::from(surface.height));
431 let Some(([min_x, max_x], [min_y, max_y])) = setup.bounds(w, h) else {
432 return Ok(());
433 };
434 let qx0 = min_x & !1;
436 let qy0 = min_y & !1;
437 let mut lane_inputs: [Vec<[u32; 4]>; 4] = Default::default();
438 for qy in (qy0..max_y).step_by(2) {
439 for qx in (qx0..max_x).step_by(2) {
440 let (qx1, qy1) = (qx.saturating_add(1), qy.saturating_add(1));
441 let lanes = [(qx, qy), (qx1, qy), (qx, qy1), (qx1, qy1)];
442 let mut covered = [false; 4];
443 let mut depths = [0f32; 4];
444 for (((&(px, py), covered), depth), lane_input) in lanes
445 .iter()
446 .zip(covered.iter_mut())
447 .zip(depths.iter_mut())
448 .zip(lane_inputs.iter_mut())
449 {
450 let (b, inside) = setup.weights(px, py);
451 *covered = inside && px < w && py < h;
452 let [b0, b1, b2] = b;
453 let [s0, s1, s2] = setup.screen;
454 let z = b0 * s0[2] + b1 * s1[2] + b2 * s2[2];
455 let z = crate::narrow(z)
456 .clamp(vp.min_depth.min(vp.max_depth), vp.max_depth.max(vp.min_depth));
457 *depth = z;
458 self.inputs_at(
459 tri,
460 original,
461 b,
462 &setup.inv_w,
463 [px as f32 + 0.5, py as f32 + 0.5, z],
464 front,
465 lane_input,
466 );
467 }
468 if !covered.iter().any(|c| *c) {
469 continue;
470 }
471 let [input0, input1, input2, input3] = &lane_inputs;
472 let inputs = [
473 input0.as_slice(),
474 input1.as_slice(),
475 input2.as_slice(),
476 input3.as_slice(),
477 ];
478 let quad: [Execution; 4] = self.pixel.execute_quad(inputs, buffers, sampler)?;
479 for (((e, &covered), &(px, py)), &depth) in quad.iter().zip(&covered).zip(&lanes).zip(&depths)
480 {
481 if !covered {
482 continue;
483 }
484 stats.shaded = stats.shaded.saturating_add(1);
485 if e.discarded {
486 stats.discarded = stats.discarded.saturating_add(1);
487 continue;
488 }
489 let at = usize::try_from(py.saturating_mul(w).saturating_add(px))
491 .map_err(|_| error(format!("pixel ({px}, {py}) is outside the surface")))?;
492 if let (Some(test), Some(buffer)) = (state.depth, surface.depth.as_mut()) {
493 let stored = buffer.get_mut(at).ok_or_else(|| {
494 error(format!("pixel ({px}, {py}) is outside the depth buffer"))
495 })?;
496 if !test.compare.passes(depth, *stored) {
497 stats.depth_failed = stats.depth_failed.saturating_add(1);
498 continue;
499 }
500 if test.write {
501 *stored = depth;
502 }
503 }
504 for (register, target) in surface.targets.iter_mut().enumerate() {
505 let Some(target) = target else { continue };
506 let mask = e.output_masks.get(register).copied().unwrap_or(0);
507 let value = e.outputs.get(register).copied().unwrap_or([0; 4]);
508 let texel = target.get_mut(at).ok_or_else(|| {
509 error(format!("pixel ({px}, {py}) is outside target {register}"))
510 })?;
511 for (lane, (stored, &written)) in texel.iter_mut().zip(&value).enumerate() {
512 if mask >> lane & 1 != 0 {
513 *stored = written;
514 }
515 }
516 }
517 stats.written = stats.written.saturating_add(1);
518 }
519 }
520 }
521 Ok(())
522 }
523
524 #[allow(clippy::too_many_arguments)]
527 fn inputs_at(
528 &self,
529 tri: [&ClipVertex; 3],
530 original: [&Shaded; 3],
531 b: [f64; 3],
532 inv_w: &[f64; 3],
533 position: [f32; 3],
534 front: bool,
535 into: &mut Vec<[u32; 4]>,
536 ) {
537 into.clear();
538 into.resize(self.pixel.input_count(), [0; 4]);
539 let pw: [f64; 3] = [b[0] * inv_w[0], b[1] * inv_w[1], b[2] * inv_w[2]];
541 let sum = pw[0] + pw[1] + pw[2];
542 let persp = pw.map(|x| x / sum);
543 let corner_w = original.map(|v| f64::from(v.position[3]));
546 let (mut on_persp, mut on_linear) = ([0f64; 3], [0f64; 3]);
547 for (k, v) in tri.iter().enumerate() {
548 let screen = screen_weights(v.weights, corner_w);
549 let (persp_k, b_k) = (
550 persp.get(k).copied().unwrap_or(0.),
551 b.get(k).copied().unwrap_or(0.),
552 );
553 for (((persp_sum, linear_sum), &weight), &on_screen) in on_persp
554 .iter_mut()
555 .zip(on_linear.iter_mut())
556 .zip(&v.weights)
557 .zip(&screen)
558 {
559 *persp_sum += persp_k * weight;
560 *linear_sum += b_k * on_screen;
561 }
562 }
563 for s in &self.sources {
564 match s {
565 Source::Position { register, lanes } => {
566 let [x, y, z] = position;
567 let value = [x, y, z, crate::narrow(1. / sum)];
568 for (lane, value) in lanes.iter().zip(value) {
569 put(into, *register, *lane as usize, value.to_bits());
570 }
571 }
572 Source::FrontFace { register, lane } => {
573 put(into, *register, *lane as usize, if front { u32::MAX } else { 0 });
574 }
575 Source::Varying {
576 from_register,
577 from_lanes,
578 to_register,
579 to_lanes,
580 mode,
581 } => {
582 for (k, to) in to_lanes.iter().enumerate() {
583 let Some(from) = from_lanes.get(k) else { continue };
584 let word = |v: &Shaded| {
585 v.outputs
586 .get(*from_register)
587 .and_then(|r| r.get(*from as usize))
588 .copied()
589 .unwrap_or(0)
590 };
591 let blend = |weights: [f64; 3]| {
592 let v: f64 = weights
593 .iter()
594 .zip(original)
595 .map(|(weight, vertex)| weight * f64::from(f32::from_bits(word(vertex))))
596 .sum();
597 crate::narrow(v).to_bits()
598 };
599 let [provoking, _, _] = original;
600 let word = match mode {
601 InterpMode::Flat => word(provoking),
602 InterpMode::Perspective => blend(on_persp),
603 InterpMode::Linear => blend(on_linear),
604 };
605 put(into, *to_register, *to as usize, word);
606 }
607 }
608 }
609 }
610 }
611}
612
613fn put(into: &mut [[u32; 4]], register: usize, lane: usize, word: u32) {
615 if let Some(slot) = into.get_mut(register).and_then(|r| r.get_mut(lane)) {
616 *slot = word;
617 }
618}
619
620const FIXED: [i64; 2] = [-(1 << 23), (1 << 23) - 1];
622
623#[allow(clippy::cast_possible_truncation)] fn fixed(x: f64) -> i64 {
627 ((x * 256.).round_ties_even() as i64).clamp(FIXED[0], FIXED[1])
628}
629
630#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] fn edge(a: [i64; 2], b: [i64; 2], p: [i64; 2]) -> i64 {
634 (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
635}
636
637#[derive(Debug)]
639struct Setup {
640 screen: [[f64; 3]; 3],
642 inv_w: [f64; 3],
644 fixed: [[i64; 2]; 3],
646 clockwise: bool,
648 area: i64,
650 edges: [(usize, usize); 3],
652 top_left: [bool; 3],
654}
655
656impl Setup {
657 #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] fn new(tri: [&ClipVertex; 3], vp: &Viewport) -> Option<Self> {
660 let mut screen = [[0f64; 3]; 3];
661 let mut inv_w = [0f64; 3];
662 let mut fixed_at = [[0i64; 2]; 3];
663 for (k, v) in tri.iter().enumerate() {
664 let [cx, cy, cz, w] = v.clip.map(f64::from);
665 let (nx, ny, nz) = (cx / w, cy / w, cz / w);
666 let x = f64::from(vp.x) + (nx + 1.) * 0.5 * f64::from(vp.width);
667 let y = f64::from(vp.y) + (1. - ny) * 0.5 * f64::from(vp.height);
668 let z = f64::from(vp.min_depth) + nz * f64::from(vp.max_depth - vp.min_depth);
669 screen[k] = [x, y, z];
670 inv_w[k] = 1. / w;
671 fixed_at[k] = [fixed(x), fixed(y)];
672 }
673 let area = edge(fixed_at[0], fixed_at[1], fixed_at[2]);
674 if area == 0 {
675 return None;
676 }
677 let clockwise = area > 0;
679 let order: [usize; 3] = if clockwise { [0, 1, 2] } else { [0, 2, 1] };
680 let edges: [(usize, usize); 3] = [(order[0], order[1]), (order[1], order[2]), (order[2], order[0])];
681 let top_left: [bool; 3] = edges.map(|(a, b)| {
684 let dx = fixed_at[b][0] - fixed_at[a][0];
685 let dy = fixed_at[b][1] - fixed_at[a][1];
686 (dy == 0 && dx > 0) || dy < 0
687 });
688 Some(Self {
689 screen,
690 inv_w,
691 fixed: fixed_at,
692 clockwise,
693 area: area.abs(),
694 edges,
695 top_left,
696 })
697 }
698
699 #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] fn bounds(&self, w: i64, h: i64) -> Option<([i64; 2], [i64; 2])> {
703 let along = |axis: usize, size: i64| {
704 let low = self.fixed.iter().map(|p| p[axis]).min().unwrap_or(0) >> 8;
705 let high = (self.fixed.iter().map(|p| p[axis]).max().unwrap_or(0) >> 8) + 1;
706 [low.clamp(0, size), high.clamp(0, size)]
707 };
708 let (x, y) = (along(0, w), along(1, h));
709 (x[0] < x[1] && y[0] < y[1]).then_some((x, y))
710 }
711
712 #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] fn weights(&self, px: i64, py: i64) -> ([f64; 3], bool) {
716 let p = [px * 256 + 128, py * 256 + 128];
717 let mut inside = true;
718 let mut b = [0f64; 3];
719 for (e, &(a, c)) in self.edges.iter().enumerate() {
720 let value = edge(self.fixed[a], self.fixed[c], p);
721 if value < 0 || (value == 0 && !self.top_left[e]) {
722 inside = false;
723 }
724 let opposite = 3 - a - c;
726 b[opposite] = value as f64 / self.area as f64;
727 }
728 (b, inside)
729 }
730}
731
732#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] fn clip(triangle: Vec<ClipVertex>) -> Vec<ClipVertex> {
739 let w = |c: &[f32; 4]| f64::from(c[3]);
740 type Plane<'a> = &'a dyn Fn(&[f32; 4]) -> f64;
742 let planes: [Plane; 6] = [
743 &|c| f64::from(c[2]),
744 &|c| w(c) - f64::from(c[2]),
745 &|c| w(c) + f64::from(c[0]),
746 &|c| w(c) - f64::from(c[0]),
747 &|c| w(c) + f64::from(c[1]),
748 &|c| w(c) - f64::from(c[1]),
749 ];
750 let mut polygon = triangle;
751 for plane in planes {
752 if polygon.iter().all(|v| plane(&v.clip) >= 0.) {
753 continue;
754 }
755 let mut out = Vec::new();
756 for i in 0..polygon.len() {
757 let a = &polygon[i];
758 let b = &polygon[(i + 1) % polygon.len()];
759 let (da, db) = (plane(&a.clip), plane(&b.clip));
760 if da >= 0. {
761 out.push(a.clone());
762 }
763 if (da >= 0.) != (db >= 0.) {
764 let t = da / (da - db);
765 out.push(blend(a, b, t));
766 }
767 }
768 polygon = out;
769 if polygon.len() < 3 {
770 return polygon;
771 }
772 }
773 if polygon.iter().any(|v| v.clip[3] == 0.) {
774 return Vec::new();
775 }
776 polygon
777}
778
779#[allow(clippy::indexing_slicing, clippy::cast_possible_truncation)] fn blend(a: &ClipVertex, b: &ClipVertex, t: f64) -> ClipVertex {
782 let clip = [0, 1, 2, 3]
783 .map(|k| (f64::from(a.clip[k]) + (f64::from(b.clip[k]) - f64::from(a.clip[k])) * t) as f32);
784 let weights = [0, 1, 2].map(|i| a.weights[i] + (b.weights[i] - a.weights[i]) * t);
785 ClipVertex { clip, weights }
786}
787
788#[cfg(test)]
789mod tests {
790 use super::*;
791
792 const SIZE: i64 = 16;
793 const VIEWPORT: Viewport = Viewport {
794 x: 0.,
795 y: 0.,
796 width: SIZE as f32,
797 height: SIZE as f32,
798 min_depth: 0.,
799 max_depth: 1.,
800 };
801
802 fn at(x: f32, y: f32, z: f32, w: f32) -> [f32; 4] {
804 let s = SIZE as f32;
805 [(x / s * 2. - 1.) * w, (1. - y / s * 2.) * w, z * w, w]
806 }
807
808 fn clipped(corners: [[f32; 4]; 3]) -> Vec<ClipVertex> {
809 let mut k = 0;
810 let triangle = corners.map(|clip| {
811 let mut weights = [0.; 3];
812 weights[k] = 1.;
813 k += 1;
814 ClipVertex { clip, weights }
815 });
816 clip(triangle.to_vec())
817 }
818
819 fn on_screen(c: [f64; 4]) -> [f64; 2] {
821 [
822 (c[0] / c[3] + 1.) * 0.5 * SIZE as f64,
823 (1. - c[1] / c[3]) * 0.5 * SIZE as f64,
824 ]
825 }
826
827 fn coverage(polygon: &[ClipVertex]) -> Vec<u32> {
829 let mut count = vec![0; (SIZE * SIZE) as usize];
830 for k in 1..polygon.len().saturating_sub(1) {
831 let Some(setup) = Setup::new([&polygon[0], &polygon[k], &polygon[k + 1]], &VIEWPORT) else {
832 continue;
833 };
834 let Some(([x0, x1], [y0, y1])) = setup.bounds(SIZE, SIZE) else {
835 continue;
836 };
837 for py in y0..y1 {
838 for px in x0..x1 {
839 if setup.weights(px, py).1 {
840 count[(py * SIZE + px) as usize] += 1;
841 }
842 }
843 }
844 }
845 count
846 }
847
848 #[test]
849 fn a_vertex_near_the_camera_plane_is_clipped_to_the_viewport() {
850 let a = at(2.25, 8., 0.5, 1.);
856 let b = at(8.25, 14., 0.5, 1.);
857 let c = [1e3, 2e2, 0., 1e-7];
858 let polygon = clipped([a, b, c]);
859 let expected: Vec<u32> = (0..SIZE * SIZE)
860 .map(|i| {
861 let (x, y) = ((i % SIZE) as f64 + 0.5, (i / SIZE) as f64 + 0.5);
862 u32::from(x + 5. * y > 42.25 && x + 5. * y < 78.25 && x - y > -5.75)
863 })
864 .collect();
865 assert_eq!(coverage(&polygon), expected);
866 for v in &polygon {
867 for screen in on_screen(v.clip.map(f64::from)) {
868 assert!(
869 (-1e-3..=SIZE as f64 + 1e-3).contains(&screen),
870 "{screen} past the viewport"
871 );
872 }
873 }
874 }
875
876 #[test]
882 fn a_cut_vertex_holds_its_weights_in_clip_space_and_on_screen() {
883 let corners = [
884 at(2.25, 8., 0.5, 1.),
885 at(8.25, 14., 0.5, 1.),
886 [1e3, 2e2, 0., 1e-7],
887 ];
888 let polygon = clipped(corners);
889 let corner_w = corners.map(|c| f64::from(c[3]));
890 let mut cut = 0;
891 for v in &polygon {
892 let in_clip: [f64; 4] =
893 std::array::from_fn(|k| (0..3).map(|i| v.weights[i] * f64::from(corners[i][k])).sum());
894 for (k, (blended, kept)) in in_clip.iter().zip(v.clip).enumerate() {
895 let kept = f64::from(kept);
896 assert!(
897 (blended - kept).abs() <= 1e-6 * kept.abs().max(1.),
898 "clip lane {k}: {blended} from the weights, {kept} kept"
899 );
900 }
901 let screen = screen_weights(v.weights, corner_w);
902 let far = on_screen(corners[2].map(f64::from));
903 let near = [0, 1].map(|i| on_screen(corners[i].map(f64::from)));
904 let at = on_screen(v.clip.map(f64::from));
905 for axis in 0..2 {
906 let blended = screen[0] * near[0][axis] + screen[1] * near[1][axis] + screen[2] * far[axis];
907 assert!(
908 (blended - at[axis]).abs() <= 1e-3,
909 "screen axis {axis}: {blended} from the weights, {} kept",
910 at[axis]
911 );
912 }
913 if v.weights[2] > 0. && v.weights[2] < 1. {
914 cut += 1;
915 assert!(
916 v.weights[2] < 1e-2 && screen[2] < 1e-6 * v.weights[2],
917 "{:?} {screen:?}",
918 v.weights
919 );
920 }
921 }
922 assert_eq!(cut, 2, "{polygon:?}");
923 }
924
925 #[test]
928 fn a_vertex_at_a_tiny_w_inside_the_viewport_is_kept() {
929 let near = [1e-8, -1e-8, 0., 1e-7];
930 let polygon = clipped([at(2., 2., 0.5, 1.), at(14., 3., 0.5, 1.), near]);
931 let kept: Vec<[u32; 4]> = polygon.iter().map(|v| v.clip.map(f32::to_bits)).collect();
932 assert!(kept.contains(&near.map(f32::to_bits)), "{polygon:?}");
933 assert!(coverage(&polygon).iter().any(|&n| n > 0));
934 }
935
936 #[test]
939 fn a_triangle_through_the_clip_space_origin_draws_nothing() {
940 let a = at(4., 4., 0.5, 1.);
941 let b = at(12., 6., 0.5, 1.);
942 let c = [0, 1, 2, 3].map(|k| -(a[k] + b[k]) / 2.);
943 assert!(clipped([a, b, c]).is_empty());
944 }
945
946 #[test]
947 fn a_triangle_inside_the_viewport_is_not_clipped() {
948 let corners = [
949 at(1., 14., 0.5, 1.),
950 at(8., 1.5, 0.5, 3.),
951 at(15., 13., 0.25, 0.5),
952 ];
953 let polygon = clipped(corners);
954 let kept: Vec<[u32; 4]> = polygon.iter().map(|v| v.clip.map(f32::to_bits)).collect();
955 assert_eq!(kept, corners.map(|c| c.map(f32::to_bits)));
956 }
957
958 #[test]
959 fn an_infinite_position_is_clipped_away_or_not_a_number() {
960 let polygon = clipped([
961 at(2., 2., 0.5, 1.),
962 [f32::INFINITY, 0., 0.5, 1.],
963 at(2., 12., 0.5, 1.),
964 ]);
965 assert!(polygon.len() < 3 || polygon.iter().any(|v| v.clip.iter().any(|c| c.is_nan())));
966 }
967
968 #[test]
969 fn snapping_rounds_to_the_nearest_a_tie_to_even_and_saturates() {
970 assert_eq!(fixed(100.25), 25664);
971 assert_eq!(fixed(1. / 1024.), 0);
972 assert_eq!(fixed(3. / 1024.), 1);
973 assert_eq!(fixed(1152.5 / 256.), 1152);
975 assert_eq!(fixed(1153.5 / 256.), 1154);
976 assert_eq!(fixed(-1152.5 / 256.), -1152);
977 assert_eq!(fixed(1. / 512.), 0);
978 assert_eq!(fixed(1e30), FIXED[1]);
979 assert_eq!(fixed(-1e30), FIXED[0]);
980 assert_eq!(fixed(f64::INFINITY), FIXED[1]);
981 assert_eq!(fixed(f64::NAN), 0);
982 }
983}