use crate::{Error, Execution, PixelShader, ProgramShader, Sampler};
use iridia::{InterpMode, SystemValue, Varying};
fn error(detail: impl Into<String>) -> Error {
Error {
bytecode: "draw",
at: 0,
detail: detail.into(),
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Viewport {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub min_depth: f32,
pub max_depth: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cull {
None,
Front,
Back,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Compare {
Never,
Less,
Equal,
LessEqual,
Greater,
NotEqual,
GreaterEqual,
Always,
}
impl Compare {
fn passes(self, pixel: f32, stored: f32) -> bool {
match self {
Compare::Never => false,
Compare::Less => pixel < stored,
Compare::Equal => pixel == stored,
Compare::LessEqual => pixel <= stored,
Compare::Greater => pixel > stored,
Compare::NotEqual => pixel != stored,
Compare::GreaterEqual => pixel >= stored,
Compare::Always => true,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DepthState {
pub compare: Compare,
pub write: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rasterizer {
pub viewport: Viewport,
pub cull: Cull,
pub front_counter_clockwise: bool,
pub depth: Option<DepthState>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Surface {
pub width: u32,
pub height: u32,
pub targets: Vec<Option<Vec<[u32; 4]>>>,
pub depth: Option<Vec<f32>>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DrawStats {
pub triangles: usize,
pub culled: usize,
pub clipped_away: usize,
pub not_a_number: usize,
pub shaded: usize,
pub discarded: usize,
pub depth_failed: usize,
pub written: usize,
}
#[derive(Clone, Debug)]
enum Source {
Varying {
from_register: usize,
from_lanes: Vec<u8>,
to_register: usize,
to_lanes: Vec<u8>,
mode: InterpMode,
},
Position {
register: usize,
lanes: Vec<u8>,
},
FrontFace {
register: usize,
lane: u8,
},
}
fn lanes_of(mask: u8) -> Vec<u8> {
(0..4).filter(|l| mask >> l & 1 != 0).collect()
}
fn same_label(a: &Varying, b: &Varying) -> bool {
a.label.index == b.label.index && a.label.name.eq_ignore_ascii_case(&b.label.name)
}
#[derive(Debug)]
pub struct DrawShader<'a> {
vertex: &'a ProgramShader,
pixel: &'a ProgramShader,
sources: Vec<Source>,
}
#[derive(Clone, Debug)]
struct Shaded {
position: [f32; 4],
outputs: Vec<[u32; 4]>,
}
#[derive(Clone, Debug)]
struct ClipVertex {
clip: [f32; 4],
weights: [f64; 3],
}
fn screen_weights(clip: [f64; 3], w: [f64; 3]) -> [f64; 3] {
let scaled = [clip[0] * w[0], clip[1] * w[1], clip[2] * w[2]];
let sum = scaled[0] + scaled[1] + scaled[2];
scaled.map(|x| x / sum)
}
impl<'a> DrawShader<'a> {
pub fn new(vertex: &'a ProgramShader, pixel: &'a ProgramShader) -> Result<Self, Error> {
let mut sources = Vec::new();
for v in pixel.interface().inputs.iter().filter(|v| v.used) {
let Some(sig) = v.provenance.d3d else {
return Err(error(format!(
"pixel input {}{} has no register",
v.label.name, v.label.index
)));
};
let to_register = sig.register as usize;
let to_lanes = lanes_of(sig.mask);
match v.system {
Some(SystemValue::FragCoord) => {
sources.push(Source::Position {
register: to_register,
lanes: to_lanes,
});
continue;
}
Some(SystemValue::FrontFacing) => {
let Some(&lane) = to_lanes.first() else {
return Err(error(format!(
"pixel input {}{} is the facing, and its signature mask names no lane",
v.label.name, v.label.index
)));
};
sources.push(Source::FrontFace {
register: to_register,
lane,
});
continue;
}
Some(other) => {
return Err(error(format!(
"pixel input {}{} is the system value {other:?}, which a draw does not supply",
v.label.name, v.label.index
)));
}
None => {}
}
let out = vertex
.interface()
.outputs
.iter()
.find(|o| o.system.is_none() && same_label(o, v))
.ok_or_else(|| {
error(format!(
"pixel input {}{} has no vertex output of its semantic",
v.label.name, v.label.index
))
})?;
let from = out.provenance.d3d.ok_or_else(|| {
error(format!(
"vertex output {}{} has no register",
out.label.name, out.label.index
))
})?;
let mode = match v.interp {
Some(i) => i.mode,
None if v.value.scalar == iridia::ScalarType::Float => InterpMode::Perspective,
None => InterpMode::Flat,
};
sources.push(Source::Varying {
from_register: from.register as usize,
from_lanes: lanes_of(from.mask),
to_register,
to_lanes,
mode,
});
}
Ok(Self {
vertex,
pixel,
sources,
})
}
#[allow(clippy::too_many_arguments)]
pub fn draw_indexed(
&self,
vertices: &[Vec<[u32; 4]>],
indices: &[u32],
vertex_buffers: &[(u32, &[[u32; 4]])],
pixel_buffers: &[(u32, &[[u32; 4]])],
sampler: &mut dyn Sampler,
state: &Rasterizer,
surface: &mut Surface,
) -> Result<DrawStats, Error> {
if !indices.len().is_multiple_of(3) {
return Err(error(format!(
"{} indices are not whole triangles",
indices.len()
)));
}
let n = (surface.width as usize).saturating_mul(surface.height as usize);
if surface.targets.iter().flatten().any(|t| t.len() != n)
|| surface.depth.as_ref().is_some_and(|d| d.len() != n)
{
return Err(error("a target or the depth buffer is not width x height"));
}
if state.depth.is_some() && surface.depth.is_none() {
return Err(error("the state tests depth and the surface has no depth buffer"));
}
let mut shaded: Vec<Option<Shaded>> = vec![None; vertices.len()];
let mut stats = DrawStats::default();
for triangle in indices.as_chunks::<3>().0 {
stats.triangles = stats.triangles.saturating_add(1);
let mut drawn = [0usize; 3];
for (slot, &i) in drawn.iter_mut().zip(triangle) {
let i = i as usize;
let input = vertices.get(i).ok_or_else(|| {
error(format!(
"index {i} names a vertex past the {} given",
vertices.len()
))
})?;
let entry = shaded
.get_mut(i)
.ok_or_else(|| error(format!("vertex {i} has no room for its shading")))?;
if entry.is_none() {
let (e, position) = self.vertex.execute_vertex(input, vertex_buffers, sampler)?;
*entry = Some(Shaded {
position: position.map(f32::from_bits),
outputs: e.outputs,
});
}
*slot = i;
}
let [first, second, third] = drawn;
let (Some(Some(first)), Some(Some(second)), Some(Some(third))) =
(shaded.get(first), shaded.get(second), shaded.get(third))
else {
return Err(error("a vertex of a triangle was not shaded"));
};
let original = [first, second, third];
let corners = original
.iter()
.enumerate()
.map(|(k, vertex)| {
let mut weights = [0.; 3];
if let Some(weight) = weights.get_mut(k) {
*weight = 1.;
}
ClipVertex {
clip: vertex.position,
weights,
}
})
.collect();
let polygon = clip(corners);
if polygon.len() < 3 {
stats.clipped_away = stats.clipped_away.saturating_add(1);
continue;
}
if polygon.iter().any(|v| v.clip.iter().any(|c| c.is_nan())) {
stats.not_a_number = stats.not_a_number.saturating_add(1);
continue;
}
if let Some((fan, rest)) = polygon.split_first() {
for pair in rest.windows(2) {
if let [second, third] = pair {
let tri = [fan, second, third];
self.raster(tri, original, pixel_buffers, sampler, state, surface, &mut stats)?;
}
}
}
}
Ok(stats)
}
#[allow(clippy::too_many_arguments)]
fn raster(
&self,
tri: [&ClipVertex; 3],
original: [&Shaded; 3],
buffers: &[(u32, &[[u32; 4]])],
sampler: &mut dyn Sampler,
state: &Rasterizer,
surface: &mut Surface,
stats: &mut DrawStats,
) -> Result<(), Error> {
let vp = state.viewport;
let Some(setup) = Setup::new(tri, &vp) else {
return Ok(());
};
let front = setup.clockwise != state.front_counter_clockwise;
if (state.cull == Cull::Front && front) || (state.cull == Cull::Back && !front) {
stats.culled = stats.culled.saturating_add(1);
return Ok(());
}
let (w, h) = (i64::from(surface.width), i64::from(surface.height));
let Some(([min_x, max_x], [min_y, max_y])) = setup.bounds(w, h) else {
return Ok(());
};
let qx0 = min_x & !1;
let qy0 = min_y & !1;
let mut lane_inputs: [Vec<[u32; 4]>; 4] = Default::default();
for qy in (qy0..max_y).step_by(2) {
for qx in (qx0..max_x).step_by(2) {
let (qx1, qy1) = (qx.saturating_add(1), qy.saturating_add(1));
let lanes = [(qx, qy), (qx1, qy), (qx, qy1), (qx1, qy1)];
let mut covered = [false; 4];
let mut depths = [0f32; 4];
for (((&(px, py), covered), depth), lane_input) in lanes
.iter()
.zip(covered.iter_mut())
.zip(depths.iter_mut())
.zip(lane_inputs.iter_mut())
{
let (b, inside) = setup.weights(px, py);
*covered = inside && px < w && py < h;
let [b0, b1, b2] = b;
let [s0, s1, s2] = setup.screen;
let z = b0 * s0[2] + b1 * s1[2] + b2 * s2[2];
let z = crate::narrow(z)
.clamp(vp.min_depth.min(vp.max_depth), vp.max_depth.max(vp.min_depth));
*depth = z;
self.inputs_at(
tri,
original,
b,
&setup.inv_w,
[px as f32 + 0.5, py as f32 + 0.5, z],
front,
lane_input,
);
}
if !covered.iter().any(|c| *c) {
continue;
}
let [input0, input1, input2, input3] = &lane_inputs;
let inputs = [
input0.as_slice(),
input1.as_slice(),
input2.as_slice(),
input3.as_slice(),
];
let quad: [Execution; 4] = self.pixel.execute_quad(inputs, buffers, sampler)?;
for (((e, &covered), &(px, py)), &depth) in quad.iter().zip(&covered).zip(&lanes).zip(&depths)
{
if !covered {
continue;
}
stats.shaded = stats.shaded.saturating_add(1);
if e.discarded {
stats.discarded = stats.discarded.saturating_add(1);
continue;
}
let at = usize::try_from(py.saturating_mul(w).saturating_add(px))
.map_err(|_| error(format!("pixel ({px}, {py}) is outside the surface")))?;
if let (Some(test), Some(buffer)) = (state.depth, surface.depth.as_mut()) {
let stored = buffer.get_mut(at).ok_or_else(|| {
error(format!("pixel ({px}, {py}) is outside the depth buffer"))
})?;
if !test.compare.passes(depth, *stored) {
stats.depth_failed = stats.depth_failed.saturating_add(1);
continue;
}
if test.write {
*stored = depth;
}
}
for (register, target) in surface.targets.iter_mut().enumerate() {
let Some(target) = target else { continue };
let mask = e.output_masks.get(register).copied().unwrap_or(0);
let value = e.outputs.get(register).copied().unwrap_or([0; 4]);
let texel = target.get_mut(at).ok_or_else(|| {
error(format!("pixel ({px}, {py}) is outside target {register}"))
})?;
for (lane, (stored, &written)) in texel.iter_mut().zip(&value).enumerate() {
if mask >> lane & 1 != 0 {
*stored = written;
}
}
}
stats.written = stats.written.saturating_add(1);
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn inputs_at(
&self,
tri: [&ClipVertex; 3],
original: [&Shaded; 3],
b: [f64; 3],
inv_w: &[f64; 3],
position: [f32; 3],
front: bool,
into: &mut Vec<[u32; 4]>,
) {
into.clear();
into.resize(self.pixel.input_count(), [0; 4]);
let pw: [f64; 3] = [b[0] * inv_w[0], b[1] * inv_w[1], b[2] * inv_w[2]];
let sum = pw[0] + pw[1] + pw[2];
let persp = pw.map(|x| x / sum);
let corner_w = original.map(|v| f64::from(v.position[3]));
let (mut on_persp, mut on_linear) = ([0f64; 3], [0f64; 3]);
for (k, v) in tri.iter().enumerate() {
let screen = screen_weights(v.weights, corner_w);
let (persp_k, b_k) = (
persp.get(k).copied().unwrap_or(0.),
b.get(k).copied().unwrap_or(0.),
);
for (((persp_sum, linear_sum), &weight), &on_screen) in on_persp
.iter_mut()
.zip(on_linear.iter_mut())
.zip(&v.weights)
.zip(&screen)
{
*persp_sum += persp_k * weight;
*linear_sum += b_k * on_screen;
}
}
for s in &self.sources {
match s {
Source::Position { register, lanes } => {
let [x, y, z] = position;
let value = [x, y, z, crate::narrow(1. / sum)];
for (lane, value) in lanes.iter().zip(value) {
put(into, *register, *lane as usize, value.to_bits());
}
}
Source::FrontFace { register, lane } => {
put(into, *register, *lane as usize, if front { u32::MAX } else { 0 });
}
Source::Varying {
from_register,
from_lanes,
to_register,
to_lanes,
mode,
} => {
for (k, to) in to_lanes.iter().enumerate() {
let Some(from) = from_lanes.get(k) else { continue };
let word = |v: &Shaded| {
v.outputs
.get(*from_register)
.and_then(|r| r.get(*from as usize))
.copied()
.unwrap_or(0)
};
let blend = |weights: [f64; 3]| {
let v: f64 = weights
.iter()
.zip(original)
.map(|(weight, vertex)| weight * f64::from(f32::from_bits(word(vertex))))
.sum();
crate::narrow(v).to_bits()
};
let [provoking, _, _] = original;
let word = match mode {
InterpMode::Flat => word(provoking),
InterpMode::Perspective => blend(on_persp),
InterpMode::Linear => blend(on_linear),
};
put(into, *to_register, *to as usize, word);
}
}
}
}
}
}
fn put(into: &mut [[u32; 4]], register: usize, lane: usize, word: u32) {
if let Some(slot) = into.get_mut(register).and_then(|r| r.get_mut(lane)) {
*slot = word;
}
}
const FIXED: [i64; 2] = [-(1 << 23), (1 << 23) - 1];
#[allow(clippy::cast_possible_truncation)] fn fixed(x: f64) -> i64 {
((x * 256.).round_ties_even() as i64).clamp(FIXED[0], FIXED[1])
}
#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] fn edge(a: [i64; 2], b: [i64; 2], p: [i64; 2]) -> i64 {
(b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
}
#[derive(Debug)]
struct Setup {
screen: [[f64; 3]; 3],
inv_w: [f64; 3],
fixed: [[i64; 2]; 3],
clockwise: bool,
area: i64,
edges: [(usize, usize); 3],
top_left: [bool; 3],
}
impl Setup {
#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] fn new(tri: [&ClipVertex; 3], vp: &Viewport) -> Option<Self> {
let mut screen = [[0f64; 3]; 3];
let mut inv_w = [0f64; 3];
let mut fixed_at = [[0i64; 2]; 3];
for (k, v) in tri.iter().enumerate() {
let [cx, cy, cz, w] = v.clip.map(f64::from);
let (nx, ny, nz) = (cx / w, cy / w, cz / w);
let x = f64::from(vp.x) + (nx + 1.) * 0.5 * f64::from(vp.width);
let y = f64::from(vp.y) + (1. - ny) * 0.5 * f64::from(vp.height);
let z = f64::from(vp.min_depth) + nz * f64::from(vp.max_depth - vp.min_depth);
screen[k] = [x, y, z];
inv_w[k] = 1. / w;
fixed_at[k] = [fixed(x), fixed(y)];
}
let area = edge(fixed_at[0], fixed_at[1], fixed_at[2]);
if area == 0 {
return None;
}
let clockwise = area > 0;
let order: [usize; 3] = if clockwise { [0, 1, 2] } else { [0, 2, 1] };
let edges: [(usize, usize); 3] = [(order[0], order[1]), (order[1], order[2]), (order[2], order[0])];
let top_left: [bool; 3] = edges.map(|(a, b)| {
let dx = fixed_at[b][0] - fixed_at[a][0];
let dy = fixed_at[b][1] - fixed_at[a][1];
(dy == 0 && dx > 0) || dy < 0
});
Some(Self {
screen,
inv_w,
fixed: fixed_at,
clockwise,
area: area.abs(),
edges,
top_left,
})
}
#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] fn bounds(&self, w: i64, h: i64) -> Option<([i64; 2], [i64; 2])> {
let along = |axis: usize, size: i64| {
let low = self.fixed.iter().map(|p| p[axis]).min().unwrap_or(0) >> 8;
let high = (self.fixed.iter().map(|p| p[axis]).max().unwrap_or(0) >> 8) + 1;
[low.clamp(0, size), high.clamp(0, size)]
};
let (x, y) = (along(0, w), along(1, h));
(x[0] < x[1] && y[0] < y[1]).then_some((x, y))
}
#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] fn weights(&self, px: i64, py: i64) -> ([f64; 3], bool) {
let p = [px * 256 + 128, py * 256 + 128];
let mut inside = true;
let mut b = [0f64; 3];
for (e, &(a, c)) in self.edges.iter().enumerate() {
let value = edge(self.fixed[a], self.fixed[c], p);
if value < 0 || (value == 0 && !self.top_left[e]) {
inside = false;
}
let opposite = 3 - a - c;
b[opposite] = value as f64 / self.area as f64;
}
(b, inside)
}
}
#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] fn clip(triangle: Vec<ClipVertex>) -> Vec<ClipVertex> {
let w = |c: &[f32; 4]| f64::from(c[3]);
type Plane<'a> = &'a dyn Fn(&[f32; 4]) -> f64;
let planes: [Plane; 6] = [
&|c| f64::from(c[2]),
&|c| w(c) - f64::from(c[2]),
&|c| w(c) + f64::from(c[0]),
&|c| w(c) - f64::from(c[0]),
&|c| w(c) + f64::from(c[1]),
&|c| w(c) - f64::from(c[1]),
];
let mut polygon = triangle;
for plane in planes {
if polygon.iter().all(|v| plane(&v.clip) >= 0.) {
continue;
}
let mut out = Vec::new();
for i in 0..polygon.len() {
let a = &polygon[i];
let b = &polygon[(i + 1) % polygon.len()];
let (da, db) = (plane(&a.clip), plane(&b.clip));
if da >= 0. {
out.push(a.clone());
}
if (da >= 0.) != (db >= 0.) {
let t = da / (da - db);
out.push(blend(a, b, t));
}
}
polygon = out;
if polygon.len() < 3 {
return polygon;
}
}
if polygon.iter().any(|v| v.clip[3] == 0.) {
return Vec::new();
}
polygon
}
#[allow(clippy::indexing_slicing, clippy::cast_possible_truncation)] fn blend(a: &ClipVertex, b: &ClipVertex, t: f64) -> ClipVertex {
let clip = [0, 1, 2, 3]
.map(|k| (f64::from(a.clip[k]) + (f64::from(b.clip[k]) - f64::from(a.clip[k])) * t) as f32);
let weights = [0, 1, 2].map(|i| a.weights[i] + (b.weights[i] - a.weights[i]) * t);
ClipVertex { clip, weights }
}
#[cfg(test)]
mod tests {
use super::*;
const SIZE: i64 = 16;
const VIEWPORT: Viewport = Viewport {
x: 0.,
y: 0.,
width: SIZE as f32,
height: SIZE as f32,
min_depth: 0.,
max_depth: 1.,
};
fn at(x: f32, y: f32, z: f32, w: f32) -> [f32; 4] {
let s = SIZE as f32;
[(x / s * 2. - 1.) * w, (1. - y / s * 2.) * w, z * w, w]
}
fn clipped(corners: [[f32; 4]; 3]) -> Vec<ClipVertex> {
let mut k = 0;
let triangle = corners.map(|clip| {
let mut weights = [0.; 3];
weights[k] = 1.;
k += 1;
ClipVertex { clip, weights }
});
clip(triangle.to_vec())
}
fn on_screen(c: [f64; 4]) -> [f64; 2] {
[
(c[0] / c[3] + 1.) * 0.5 * SIZE as f64,
(1. - c[1] / c[3]) * 0.5 * SIZE as f64,
]
}
fn coverage(polygon: &[ClipVertex]) -> Vec<u32> {
let mut count = vec![0; (SIZE * SIZE) as usize];
for k in 1..polygon.len().saturating_sub(1) {
let Some(setup) = Setup::new([&polygon[0], &polygon[k], &polygon[k + 1]], &VIEWPORT) else {
continue;
};
let Some(([x0, x1], [y0, y1])) = setup.bounds(SIZE, SIZE) else {
continue;
};
for py in y0..y1 {
for px in x0..x1 {
if setup.weights(px, py).1 {
count[(py * SIZE + px) as usize] += 1;
}
}
}
}
count
}
#[test]
fn a_vertex_near_the_camera_plane_is_clipped_to_the_viewport() {
let a = at(2.25, 8., 0.5, 1.);
let b = at(8.25, 14., 0.5, 1.);
let c = [1e3, 2e2, 0., 1e-7];
let polygon = clipped([a, b, c]);
let expected: Vec<u32> = (0..SIZE * SIZE)
.map(|i| {
let (x, y) = ((i % SIZE) as f64 + 0.5, (i / SIZE) as f64 + 0.5);
u32::from(x + 5. * y > 42.25 && x + 5. * y < 78.25 && x - y > -5.75)
})
.collect();
assert_eq!(coverage(&polygon), expected);
for v in &polygon {
for screen in on_screen(v.clip.map(f64::from)) {
assert!(
(-1e-3..=SIZE as f64 + 1e-3).contains(&screen),
"{screen} past the viewport"
);
}
}
}
#[test]
fn a_cut_vertex_holds_its_weights_in_clip_space_and_on_screen() {
let corners = [
at(2.25, 8., 0.5, 1.),
at(8.25, 14., 0.5, 1.),
[1e3, 2e2, 0., 1e-7],
];
let polygon = clipped(corners);
let corner_w = corners.map(|c| f64::from(c[3]));
let mut cut = 0;
for v in &polygon {
let in_clip: [f64; 4] =
std::array::from_fn(|k| (0..3).map(|i| v.weights[i] * f64::from(corners[i][k])).sum());
for (k, (blended, kept)) in in_clip.iter().zip(v.clip).enumerate() {
let kept = f64::from(kept);
assert!(
(blended - kept).abs() <= 1e-6 * kept.abs().max(1.),
"clip lane {k}: {blended} from the weights, {kept} kept"
);
}
let screen = screen_weights(v.weights, corner_w);
let far = on_screen(corners[2].map(f64::from));
let near = [0, 1].map(|i| on_screen(corners[i].map(f64::from)));
let at = on_screen(v.clip.map(f64::from));
for axis in 0..2 {
let blended = screen[0] * near[0][axis] + screen[1] * near[1][axis] + screen[2] * far[axis];
assert!(
(blended - at[axis]).abs() <= 1e-3,
"screen axis {axis}: {blended} from the weights, {} kept",
at[axis]
);
}
if v.weights[2] > 0. && v.weights[2] < 1. {
cut += 1;
assert!(
v.weights[2] < 1e-2 && screen[2] < 1e-6 * v.weights[2],
"{:?} {screen:?}",
v.weights
);
}
}
assert_eq!(cut, 2, "{polygon:?}");
}
#[test]
fn a_vertex_at_a_tiny_w_inside_the_viewport_is_kept() {
let near = [1e-8, -1e-8, 0., 1e-7];
let polygon = clipped([at(2., 2., 0.5, 1.), at(14., 3., 0.5, 1.), near]);
let kept: Vec<[u32; 4]> = polygon.iter().map(|v| v.clip.map(f32::to_bits)).collect();
assert!(kept.contains(&near.map(f32::to_bits)), "{polygon:?}");
assert!(coverage(&polygon).iter().any(|&n| n > 0));
}
#[test]
fn a_triangle_through_the_clip_space_origin_draws_nothing() {
let a = at(4., 4., 0.5, 1.);
let b = at(12., 6., 0.5, 1.);
let c = [0, 1, 2, 3].map(|k| -(a[k] + b[k]) / 2.);
assert!(clipped([a, b, c]).is_empty());
}
#[test]
fn a_triangle_inside_the_viewport_is_not_clipped() {
let corners = [
at(1., 14., 0.5, 1.),
at(8., 1.5, 0.5, 3.),
at(15., 13., 0.25, 0.5),
];
let polygon = clipped(corners);
let kept: Vec<[u32; 4]> = polygon.iter().map(|v| v.clip.map(f32::to_bits)).collect();
assert_eq!(kept, corners.map(|c| c.map(f32::to_bits)));
}
#[test]
fn an_infinite_position_is_clipped_away_or_not_a_number() {
let polygon = clipped([
at(2., 2., 0.5, 1.),
[f32::INFINITY, 0., 0.5, 1.],
at(2., 12., 0.5, 1.),
]);
assert!(polygon.len() < 3 || polygon.iter().any(|v| v.clip.iter().any(|c| c.is_nan())));
}
#[test]
fn snapping_rounds_to_the_nearest_a_tie_to_even_and_saturates() {
assert_eq!(fixed(100.25), 25664);
assert_eq!(fixed(1. / 1024.), 0);
assert_eq!(fixed(3. / 1024.), 1);
assert_eq!(fixed(1152.5 / 256.), 1152);
assert_eq!(fixed(1153.5 / 256.), 1154);
assert_eq!(fixed(-1152.5 / 256.), -1152);
assert_eq!(fixed(1. / 512.), 0);
assert_eq!(fixed(1e30), FIXED[1]);
assert_eq!(fixed(-1e30), FIXED[0]);
assert_eq!(fixed(f64::INFINITY), FIXED[1]);
assert_eq!(fixed(f64::NAN), 0);
}
}