Skip to main content

iridia_exec/
draw.rs

1//! A draw on the CPU, as Direct3D draws an indexed triangle list: the vertex program for each vertex, each triangle
2//! clipped, mapped through the viewport and rasterized, the pixel program for each pixel it covers, the depth test,
3//! and the render targets written. It is for re-running a draw recorded from a running game on the programs the record
4//! names, so its pixels can be set against the ones the game wrote.
5//!
6//! The rules are the Direct3D 11 functional specification's (which Direct3D 12 keeps), held to Microsoft's WARP
7//! rasterizer by the `exec_warp_draw` tests:
8//!
9//! - **Linking.** A pixel program input reads the vertex program output of the same semantic and index (names compared
10//!   without case), component by component; `SV_Position` is the pixel's position, `SV_IsFrontFace` the triangle's
11//!   facing. An input no output carries is an error.
12//! - **Clipping.** A triangle is clipped to `0 <= z <= w` (depth clipping) and to `-w <= x <= w`, `-w <= y <= w`: the
13//!   viewport, section 15.4 of the D3D11.3 specification's clip volume. 15.4 allows a guard band ("or arbitrarily
14//!   wider range if implementation uses a guard band"); WARP uses none (a triangle cut 16.25 pixels past the viewport
15//!   instead of at it puts the pixels it draws up to 1e-4 off WARP's), so a vertex clipping makes on a side of the
16//!   viewport is snapped like any other. A vertex near the camera plane, whose screen position is far outside, is
17//!   snapped only after clipping brings it in. Those planes leave every vertex `w >= |x|, |y|, z >= 0`, so a division
18//!   by `w` is finite but at the clip-space origin, which a clipped polygon reaches only when its triangle's vertices
19//!   are linearly dependent: its plane holds the camera and it has no area on screen, so it draws nothing. A vertex
20//!   clipping makes holds its weights on the triangle's three vertices in clip space, and its
21//!   outputs are the triangle's by the interpolation of the input they feed: perspective-correct ones by those weights,
22//!   the screen-linear ones (`noperspective`) by the weights of its screen position (a clip-space weight times the
23//!   vertex's w, normalised), and flat ones the provoking vertex's. A triangle with a NaN position after clipping draws
24//!   nothing (15.4); one with an infinity becomes NaN where clipping cuts it, which 15.4 allows ("may or may not result
25//!   in a discarded primitive").
26//! - **Viewport.** `x = X + (x/w + 1) / 2 * W`, `y = Y + (1 - y/w) / 2 * H` (y down), `z = MinDepth + z/w * (MaxDepth -
27//!   MinDepth)`.
28//! - **Rasterization.** Positions are snapped to 1/256 of a pixel (16.8 fixed point), as D3D's 8 bits of sub-pixel
29//!   precision (15.16), to the nearest with a tie to even and saturating at the format's range (3.2.4.1). A pixel is
30//!   drawn when its centre is inside the triangle, or on an edge that is a top or a left edge. The facing is the sign
31//!   of the snapped area: with y down a clockwise triangle is front facing unless the state says counter-clockwise;
32//!   culling drops the facing it names; a zero-area triangle draws nothing.
33//! - **Interpolation.** At the pixel centre, from the snapped positions: perspective-correct (the default), linear in
34//!   screen space (`noperspective`) or the provoking vertex's value (`nointerpolation`, integers; the triangle's first
35//!   vertex). Centroid and sample interpolation are the centre's (one sample a pixel). Depth is linear in screen space
36//!   and clamped to the viewport's depth range.
37//! - **Pixels.** In 2 x 2 quads aligned to even pixels, uncovered lanes run as helpers (their inputs extrapolated) so
38//!   derivatives and implicit levels of detail are the quad's; a helper writes nothing. A discarded pixel writes
39//!   nothing. The depth test runs after the pixel program (its result is the same: a pixel that fails writes
40//!   nothing); a pixel that passes writes every target lane the program writes and, when the state writes depth, its
41//!   depth. Triangles are drawn in index order, so a later one overwrites an earlier one where both pass.
42use 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/// The viewport: where clip space lands in the targets.
54#[derive(Clone, Copy, Debug, PartialEq)]
55pub struct Viewport {
56    /// Left edge of the viewport, in pixels.
57    pub x: f32,
58    /// Top edge of the viewport, in pixels (y runs down).
59    pub y: f32,
60    /// Width in pixels.
61    pub width: f32,
62    /// Height in pixels.
63    pub height: f32,
64    /// The depth that clip-space z/w = 0 maps to.
65    pub min_depth: f32,
66    /// The depth that clip-space z/w = 1 maps to.
67    pub max_depth: f32,
68}
69
70/// Which facing culling drops.
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum Cull {
73    /// No triangle is culled.
74    None,
75    /// Front-facing triangles are culled.
76    Front,
77    /// Back-facing triangles are culled.
78    Back,
79}
80
81/// A depth comparison: the pixel's depth (left) against the depth buffer's (right).
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum Compare {
84    /// Never passes.
85    Never,
86    /// Passes where the pixel's depth is less.
87    Less,
88    /// Passes where equal.
89    Equal,
90    /// Passes where less or equal.
91    LessEqual,
92    /// Passes where greater.
93    Greater,
94    /// Passes where not equal.
95    NotEqual,
96    /// Passes where greater or equal.
97    GreaterEqual,
98    /// Always passes.
99    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/// The depth test and whether a pixel that passes writes its depth.
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct DepthState {
120    /// The comparison of the pixel's depth with the depth buffer's.
121    pub compare: Compare,
122    /// Whether a pixel that passes writes its depth.
123    pub write: bool,
124}
125
126/// The fixed-function state of a draw.
127#[derive(Clone, Copy, Debug, PartialEq)]
128pub struct Rasterizer {
129    /// Where clip space lands in the targets.
130    pub viewport: Viewport,
131    /// Which facing is dropped.
132    pub cull: Cull,
133    /// Whether a counter-clockwise triangle (y down) is front facing.
134    pub front_counter_clockwise: bool,
135    /// The depth test; `None`: no depth buffer.
136    pub depth: Option<DepthState>,
137}
138
139/// What a draw writes to: render targets by output register (`width * height` texels each, row by row, as the pixel
140/// program's output words) and the depth buffer.
141#[derive(Clone, Debug, PartialEq)]
142pub struct Surface {
143    /// Width of every target in pixels.
144    pub width: u32,
145    /// Height of every target in pixels.
146    pub height: u32,
147    /// By output register; `None` for a register the draw does not write.
148    pub targets: Vec<Option<Vec<[u32; 4]>>>,
149    /// `width * height` depths, when the state tests depth.
150    pub depth: Option<Vec<f32>>,
151}
152
153/// What a draw did.
154#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
155pub struct DrawStats {
156    /// Triangles in the index list.
157    pub triangles: usize,
158    /// Triangles dropped by culling.
159    pub culled: usize,
160    /// Triangles clipped away entirely.
161    pub clipped_away: usize,
162    /// Triangles with a NaN position after clipping, which draw nothing.
163    pub not_a_number: usize,
164    /// Pixels the program ran for (helpers not counted).
165    pub shaded: usize,
166    /// Pixels the pixel program discarded.
167    pub discarded: usize,
168    /// Pixels that failed the depth test.
169    pub depth_failed: usize,
170    /// Pixels written to the targets.
171    pub written: usize,
172}
173
174/// Where a pixel program input comes from.
175#[derive(Clone, Debug)]
176enum Source {
177    /// A vertex program output: its register and lanes, the pixel input's register and lanes, the interpolation.
178    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/// A vertex and a pixel program linked for drawing.
204#[derive(Debug)]
205pub struct DrawShader<'a> {
206    vertex: &'a ProgramShader,
207    pixel: &'a ProgramShader,
208    sources: Vec<Source>,
209}
210
211/// A vertex after the vertex program: its clip-space position and its output registers.
212#[derive(Clone, Debug)]
213struct Shaded {
214    position: [f32; 4],
215    outputs: Vec<[u32; 4]>,
216}
217
218/// A vertex in the rasterizer: its clip-space position and its weights on the drawn triangle's three vertices in clip
219/// space (a vertex of the triangle weighs itself one; one clipping makes, the blend of its edge's ends).
220#[derive(Clone, Debug)]
221struct ClipVertex {
222    clip: [f32; 4],
223    weights: [f64; 3],
224}
225
226/// The weights of the screen position of a point whose clip-space weights on a triangle are `clip` and whose
227/// vertices' w are `w`: each clip-space weight times its vertex's w, normalised (a screen position is the clip-space
228/// one over its w, so it is the blend of the vertices' screen positions by these).
229fn 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    /// Link `vertex`'s outputs to `pixel`'s inputs by semantic.
237    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                    // the signature names the lane the facing is read in: a mask of none is a malformed signature
258                    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    /// Draw `indices` (a triangle list) of `vertices` (each the vertex program's input registers) into `surface`.
316    #[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        // the vertex program once per vertex the indices name
343        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        // quads aligned to even pixels
435        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                    // the pixel is inside the surface: `weights` and the bounds are clamped to its width and height
490                    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    /// The pixel program's input registers at a pixel of barycentrics `b` (weights of `tri`'s vertices), `tri` a
525    /// triangle of the clipped polygon of the drawn triangle `original` (whose first vertex is the provoking one).
526    #[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        // perspective-correct weights: b / w, normalised; and the interpolated w (1 / sum of b / w)
540        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        // the same weights on the drawn triangle's vertices: perspective-correct through each vertex's clip-space
544        // weights, screen-linear through the weights of its screen position
545        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
613/// Set word `lane` of register `register` of a pixel program's inputs, where the program has them.
614fn 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
620/// The 16.8 fixed point's range in 1/256 pixels: 16 signed integer bits, 8 fractional (3.2.4.1).
621const FIXED: [i64; 2] = [-(1 << 23), (1 << 23) - 1];
622
623/// A screen coordinate in 16.8 fixed point: to the nearest, a tie to even, saturating at the format's range (3.2.4.1's
624/// reference conversion; WARP takes the tie to even too, on exec_warp_draw's tie cases).
625#[allow(clippy::cast_possible_truncation)] // the clamp to the 16.8 range follows, and a NaN casts to 0
626fn fixed(x: f64) -> i64 {
627    ((x * 256.).round_ties_even() as i64).clamp(FIXED[0], FIXED[1])
628}
629
630/// Twice the signed area of `a`, `b`, `p` in fixed point: positive when `p` is right of `a -> b` (y down). With every
631/// position in the 16.8 range a difference is under 2^24 and a product under 2^48.
632#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] // fixed-size arrays, and the range above
633fn 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/// A clipped triangle set up for rasterization: its positions through the viewport, snapped, and its edges.
638#[derive(Debug)]
639struct Setup {
640    /// Screen positions (unsnapped) and depth.
641    screen: [[f64; 3]; 3],
642    /// The reciprocal of each vertex's w.
643    inv_w: [f64; 3],
644    /// The snapped positions.
645    fixed: [[i64; 2]; 3],
646    /// Whether the snapped triangle is clockwise on screen (y down).
647    clockwise: bool,
648    /// Twice the snapped triangle's area, in fixed point (positive).
649    area: i64,
650    /// The edges in clockwise order, as indices into the triangle.
651    edges: [(usize, usize); 3],
652    /// Whether each edge is a top or a left edge.
653    top_left: [bool; 3],
654}
655
656impl Setup {
657    /// `None` for a triangle of no snapped area, which draws nothing.
658    #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] // fixed-size arrays indexed by 0..3, and the 16.8 range
659    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        // y down: a positive area is clockwise on screen
678        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        // top-left rule for a clockwise triangle with y down: a top edge is horizontal and runs right (dx > 0); a
682        // left edge runs up (dy < 0)
683        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    /// The pixels of a `w` x `h` target the triangle's bounding box holds, as x and y ranges; `None` when it holds
700    /// none.
701    #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] // an axis of 0 or 1, and a shift of a 16.8 value
702    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    /// The barycentrics of a pixel centre from the snapped positions (the weight of each vertex of the triangle), and
713    /// whether the triangle covers it.
714    #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] // the edges' indices are 0..3; pixel and edge values stay within the 16.8 range
715    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            // the edge a -> c weighs the vertex opposite it
725            let opposite = 3 - a - c;
726            b[opposite] = value as f64 / self.area as f64;
727        }
728        (b, inside)
729    }
730}
731
732/// Clip a triangle to `0 <= z <= w` and the viewport (`-w <= x, y <= w`): the polygon left, as clip-space vertices with
733/// their weights on the triangle's (an empty list when nothing is left). A polygon with a vertex at the clip-space
734/// origin (w = 0, so x, y and z are 0 too: the planes keep `w >= |x|, |y|` and `w >= z >= 0`) is empty: a point of a
735/// triangle is the zero vector only when its vertices are linearly dependent, so the triangle's plane holds the camera
736/// and it has no area on screen; it has no screen position to divide out there.
737#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] // the corners are indexed by a loop over the polygon, and the plane tests take the four components of a clip position
738fn clip(triangle: Vec<ClipVertex>) -> Vec<ClipVertex> {
739    let w = |c: &[f32; 4]| f64::from(c[3]);
740    // each plane as the signed distance of a vertex: kept where >= 0
741    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/// The point at `t` from `a` to `b` in clip space, its weights blended alike.
780#[allow(clippy::indexing_slicing, clippy::cast_possible_truncation)] // fixed-size arrays indexed by 0..4, and a clip position is single precision
781fn 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    /// A clip-space position at pixel coordinates (x right, y down), depth z, w.
803    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    /// A clip-space position on screen, in pixels of the target.
820    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    /// How many times each pixel of the target is covered by the triangles of the clipped polygon.
828    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        // c, at w = 1e-7, is about 8e9 pixels right of the target (and a fifth of that up), past what 16.8 fixed point
851        // holds and what the edge function's products hold in i64; the viewport's sides cut its edges inside the
852        // target. The triangle seen from the target is a, b and the point at infinity along (5, -1): the centres with
853        // 42.25 < x + 5 y < 78.25 right of a -> b (x - y > -5.75). No pixel centre is within 0.04 pixels of its
854        // edges, and snapping the cut vertices moves an edge by under 1/256 of a pixel.
855        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    /// A vertex clipping makes is the blend of the triangle's vertices by its weights in clip space, and of their
877    /// screen positions by its screen weights ([`screen_weights`]): what a perspective-correct and a screen-linear
878    /// output of it are blended by. Where the ends of a cut edge have different w the two differ: the viewport's right
879    /// side cuts both edges to the far vertex (w = 1e-7) about 1e-3 of the way along in clip space, and its weight on
880    /// screen is 1e-7 times that (the ratio of the ends' w).
881    #[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    /// A vertex inside the viewport nearer than any fixed w is kept, as Direct3D's clip volume keeps it: its screen
926    /// position is a division by its w, which the viewport's planes keep within the target.
927    #[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    /// A triangle through the clip-space origin (its vertices linearly dependent: `c = -(a + b) / 2` here) is a line on
937    /// screen; clipping leaves the origin a vertex of it, and it draws nothing.
938    #[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        // a tie goes to the even step, either way from zero
974        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}