Skip to main content

iridia_exec/
rayquery.rs

1//! Inline ray tracing: the acceleration structure a caller supplies, the bounding-volume hierarchies the executor builds
2//! over it, and the ray query a program runs against it (DXR 1.1's `RayQuery`, as naga carries it).
3//!
4//! A [`Scene`] is what an acceleration structure is made of: bottom levels of geometry (triangles, or procedural
5//! primitives: axis-aligned boxes the program intersects itself) and a top level of instances of them, each with a
6//! transform, a mask and the ids a hit reports.
7//!
8//! The rules taken from the DXR specification (<https://microsoft.github.io/DirectX-Specs/d3d/Raytracing.html>) are cited
9//! where they apply: the ray extents (section "Ray extents": a triangle hit needs TMin < t < TMax, a procedural one
10//! TMin <= t <= TMax), what the program must do when it commits a procedural hit (section "RayQuery
11//! CommitProceduralPrimitiveHit") and the flags (section "Ray flags").
12//!
13//! A query visits the triangles the ray crosses in an order of the hierarchy's choosing, as hardware does; the closest
14//! committed hit does not depend on the order. An opaque triangle in range is committed on the spot (and with
15//! `ACCEPT_FIRST_HIT_AND_END_SEARCH` ends the search); a non-opaque one is a candidate the program commits or lets go.
16//! The triangle test is the watertight one of DXR: a ray through an edge or a vertex shared by triangles hits one of
17//! them.
18//!
19//! Checked against WARP's DXR 1.1 (`iridia-dxil/tests/exec_warp_rq.rs`): 768 rays through random scenes with instance
20//! transforms, masks and flags, every committed hit equal in its integer fields, facing included, and in distance and
21//! barycentrics to 1e-4 relative. Not checked: rays that hit an edge or a vertex exactly (the scenes are random), and
22//! `ACCEPT_FIRST_HIT_AND_END_SEARCH`, whose hit depends on the order a traversal visits triangles in. Procedural
23//! primitives are checked the same way (`ray_query_procedural.hlsl`, spheres in boxes) with the two exceptions named
24//! at [`Query::generate`].
25
26use std::collections::BTreeMap;
27
28/// A ray's flags, as DXR numbers them (and naga's `RayFlag` after them).
29pub mod flags {
30    /// Opaque geometry regardless of its own flag (`RAY_FLAG_FORCE_OPAQUE`).
31    pub const FORCE_OPAQUE: u32 = 0x1;
32    /// Non-opaque geometry regardless of its own flag (`RAY_FLAG_FORCE_NON_OPAQUE`).
33    pub const FORCE_NON_OPAQUE: u32 = 0x2;
34    /// End the search at the first hit (`RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH`).
35    pub const ACCEPT_FIRST_HIT: u32 = 0x4;
36    /// Run no closest-hit program (`RAY_FLAG_SKIP_CLOSEST_HIT_SHADER`).
37    pub const SKIP_CLOSEST_HIT: u32 = 0x8;
38    /// Cull back-facing triangles (`RAY_FLAG_CULL_BACK_FACING_TRIANGLES`).
39    pub const CULL_BACK_FACING: u32 = 0x10;
40    /// Cull front-facing triangles (`RAY_FLAG_CULL_FRONT_FACING_TRIANGLES`).
41    pub const CULL_FRONT_FACING: u32 = 0x20;
42    /// Cull opaque geometry (`RAY_FLAG_CULL_OPAQUE`).
43    pub const CULL_OPAQUE: u32 = 0x40;
44    /// Cull non-opaque geometry (`RAY_FLAG_CULL_NON_OPAQUE`).
45    pub const CULL_NON_OPAQUE: u32 = 0x80;
46    /// Skip triangle geometry (`RAY_FLAG_SKIP_TRIANGLES`).
47    pub const SKIP_TRIANGLES: u32 = 0x100;
48    /// Skip procedural geometry (`RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES`).
49    pub const SKIP_PROCEDURAL: u32 = 0x200;
50}
51
52/// One geometry of a bottom level: triangles, or (when `boxes` is not empty) procedural primitives, each an
53/// axis-aligned box the program intersects itself.
54#[derive(Clone, Debug)]
55pub struct Geometry {
56    /// The triangles' vertex positions in object space.
57    pub positions: Vec<[f32; 3]>,
58    /// Vertex indices, three to a triangle; none: the positions are the triangles, three to each.
59    pub indices: Option<Vec<u32>>,
60    /// The boxes of a procedural geometry, each as its minimum and maximum corner (the positions are then unused).
61    pub boxes: Vec<[[f32; 3]; 2]>,
62    /// Whether its primitives are opaque (D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE).
63    pub opaque: bool,
64}
65
66impl Geometry {
67    fn procedural(&self) -> bool {
68        !self.boxes.is_empty()
69    }
70    /// How many primitives it has: triangles or boxes.
71    fn primitives(&self) -> usize {
72        if self.procedural() {
73            self.boxes.len()
74        } else {
75            self.triangles()
76        }
77    }
78    fn triangles(&self) -> usize {
79        self.indices.as_ref().map_or(self.positions.len(), Vec::len) / 3
80    }
81    fn triangle(&self, k: usize) -> Option<[[f32; 3]; 3]> {
82        let at = |j: usize| k.saturating_mul(3).saturating_add(j);
83        let vertex = |j: usize| match &self.indices {
84            Some(i) => self.positions.get(*i.get(at(j))? as usize).copied(),
85            None => self.positions.get(at(j)).copied(),
86        };
87        Some([vertex(0)?, vertex(1)?, vertex(2)?])
88    }
89}
90
91/// What an instance does besides place its bottom level.
92#[derive(Clone, Copy, Debug, Default)]
93pub struct InstanceFlags {
94    /// D3D12_RAYTRACING_INSTANCE_FLAG_TRIANGLE_CULL_DISABLE: the ray's culling of facings does not apply.
95    pub cull_disable: bool,
96    /// ..._TRIANGLE_FRONT_COUNTERCLOCKWISE: a triangle is front-facing when counter-clockwise (clockwise by default).
97    pub front_counter_clockwise: bool,
98    /// ..._FORCE_OPAQUE and ..._FORCE_NON_OPAQUE.
99    pub force_opaque: bool,
100    /// `D3D12_RAYTRACING_INSTANCE_FLAG_FORCE_NON_OPAQUE`: all the instance's geometry counts as non-opaque.
101    pub force_non_opaque: bool,
102}
103
104/// An instance of a bottom level in the top level.
105#[derive(Clone, Debug)]
106pub struct Instance {
107    /// Which bottom level.
108    pub bottom: usize,
109    /// Object to world: three rows of four.
110    pub transform: [[f32; 4]; 3],
111    /// InstanceID (24 bits).
112    pub id: u32,
113    /// InstanceMask: the instance is seen by a ray whose mask shares a bit with it.
114    pub mask: u8,
115    /// InstanceContributionToHitGroupIndex (24 bits).
116    pub contribution: u32,
117    /// What the instance does besides place its bottom level.
118    pub flags: InstanceFlags,
119}
120
121/// The bottom levels and the top level of an acceleration structure.
122#[derive(Clone, Debug, Default)]
123pub struct Scene {
124    /// The bottom levels, each a list of geometries.
125    pub bottoms: Vec<Vec<Geometry>>,
126    /// The top level: the instances of the bottom levels.
127    pub instances: Vec<Instance>,
128}
129
130type Vec3 = [f32; 3];
131
132#[derive(Clone, Copy, Debug)]
133struct Aabb {
134    min: Vec3,
135    max: Vec3,
136}
137
138impl Aabb {
139    const EMPTY: Aabb = Aabb {
140        min: [f32::INFINITY; 3],
141        max: [f32::NEG_INFINITY; 3],
142    };
143    #[allow(clippy::indexing_slicing)] // k ranges over the three axes of a [f32; 3]
144    fn grow(&mut self, p: Vec3) {
145        for (k, &x) in p.iter().enumerate() {
146            self.min[k] = self.min[k].min(x);
147            self.max[k] = self.max[k].max(x);
148        }
149    }
150    fn union(&mut self, other: &Aabb) {
151        self.grow(other.min);
152        self.grow(other.max);
153    }
154    /// A little larger, so that rounding never lets a ray past a primitive the box holds.
155    #[allow(clippy::indexing_slicing)] // k ranges over the three axes of a [f32; 3]
156    fn padded(self) -> Aabb {
157        let mut b = self;
158        for k in 0..3 {
159            let pad = (b.min[k].abs().max(b.max[k].abs()) * 1e-5).max(1e-20);
160            b.min[k] -= pad;
161            b.max[k] += pad;
162        }
163        b
164    }
165}
166
167/// A node of a hierarchy: an inner node has its children at `first` and `first + 1`, a leaf its `count` primitives at
168/// `first` of the hierarchy's order.
169#[derive(Clone, Debug)]
170struct Node {
171    bounds: Aabb,
172    first: u32,
173    count: u32,
174}
175
176#[derive(Clone, Debug, Default)]
177struct Bvh {
178    nodes: Vec<Node>,
179    order: Vec<u32>,
180}
181
182impl Bvh {
183    /// A hierarchy over primitives with these bounds: split at the median of the centroids along the longest axis.
184    fn build(bounds: &[Aabb]) -> Bvh {
185        let mut bvh = Bvh::default();
186        if bounds.is_empty() {
187            return bvh;
188        }
189        bvh.order = (0..u32::try_from(bounds.len()).unwrap_or(u32::MAX)).collect();
190        bvh.nodes.push(Node {
191            bounds: Aabb::EMPTY,
192            first: 0,
193            count: 0,
194        });
195        bvh.split(0, 0, bounds.len(), bounds);
196        bvh
197    }
198
199    #[allow(
200        clippy::arithmetic_side_effects,
201        clippy::cast_possible_truncation,
202        clippy::indexing_slicing
203    )] // node, order and bounds numbers are made together by this build and a leaf holds at most 4 primitives; the f32 axis index is 0 to 2
204    fn split(&mut self, node: usize, lo: usize, hi: usize, bounds: &[Aabb]) {
205        let mut total = Aabb::EMPTY;
206        let mut centroids = Aabb::EMPTY;
207        for &p in &self.order[lo..hi] {
208            let b = &bounds[p as usize];
209            total.union(b);
210            centroids.grow(std::array::from_fn(|k| 0.5 * (b.min[k] + b.max[k])));
211        }
212        self.nodes[node].bounds = total.padded();
213        if hi - lo <= 4 {
214            self.nodes[node].first = lo as u32;
215            self.nodes[node].count = (hi - lo) as u32;
216            return;
217        }
218        let axis = (0..3)
219            .max_by(|&a, &b| {
220                (centroids.max[a] - centroids.min[a]).total_cmp(&(centroids.max[b] - centroids.min[b]))
221            })
222            .unwrap_or(0);
223        self.order[lo..hi].sort_by(|&a, &b| {
224            let (ca, cb) = (&bounds[a as usize], &bounds[b as usize]);
225            (ca.min[axis] + ca.max[axis]).total_cmp(&(cb.min[axis] + cb.max[axis]))
226        });
227        let mid = lo + (hi - lo) / 2;
228        let left = self.nodes.len();
229        for _ in 0..2 {
230            self.nodes.push(Node {
231                bounds: Aabb::EMPTY,
232                first: 0,
233                count: 0,
234            });
235        }
236        self.nodes[node].first = left as u32;
237        self.nodes[node].count = 0;
238        self.split(left, lo, mid, bounds);
239        self.split(left + 1, mid, hi, bounds);
240    }
241}
242
243/// Whether the ray crosses the box within `[tmin, tmax]` (the slab test).
244#[allow(clippy::indexing_slicing)] // k ranges over the three axes of a [f32; 3]
245fn crosses(b: &Aabb, origin: Vec3, inv: Vec3, tmin: f32, tmax: f32) -> bool {
246    let (mut lo, mut hi) = (tmin, tmax);
247    for k in 0..3 {
248        let (a, c) = ((b.min[k] - origin[k]) * inv[k], (b.max[k] - origin[k]) * inv[k]);
249        // a NaN (a zero direction on the box's plane) leaves the bounds as they are
250        lo = lo.max(a.min(c));
251        hi = hi.min(a.max(c));
252    }
253    lo <= hi
254}
255
256#[derive(Clone, Debug)]
257struct BuiltBottom {
258    geometries: Vec<Geometry>,
259    /// The primitives of the hierarchy: (geometry, triangle).
260    primitives: Vec<(u32, u32)>,
261    bvh: Bvh,
262}
263
264#[derive(Clone, Debug)]
265struct BuiltInstance {
266    instance: Instance,
267    /// World to object: three rows of four.
268    inverse: [[f32; 4]; 3],
269}
270
271/// A scene with its hierarchies.
272#[derive(Clone, Debug)]
273pub struct Built {
274    bottoms: Vec<BuiltBottom>,
275    instances: Vec<BuiltInstance>,
276    top: Bvh,
277}
278
279#[allow(clippy::indexing_slicing)] // r ranges over the three rows of a [[f32; 4]; 3]
280pub(super) fn transform_point(m: &[[f32; 4]; 3], p: Vec3) -> Vec3 {
281    std::array::from_fn(|r| m[r][0] * p[0] + m[r][1] * p[1] + m[r][2] * p[2] + m[r][3])
282}
283#[allow(clippy::indexing_slicing)] // r ranges over the three rows of a [[f32; 4]; 3]
284pub(super) fn transform_vector(m: &[[f32; 4]; 3], v: Vec3) -> Vec3 {
285    std::array::from_fn(|r| m[r][0] * v[0] + m[r][1] * v[1] + m[r][2] * v[2])
286}
287
288/// The inverse of an affine transform of three rows of four (None when it is singular).
289#[allow(clippy::cast_possible_truncation, clippy::indexing_slicing)] // r and c range over the three rows and columns of a [[f32; 4]; 3]; the f64 results round to the f32 of the matrix
290fn invert(m: &[[f32; 4]; 3]) -> Option<[[f32; 4]; 3]> {
291    let a = |r: usize, c: usize| f64::from(m[r][c]);
292    let det = a(0, 0) * (a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1))
293        - a(0, 1) * (a(1, 0) * a(2, 2) - a(1, 2) * a(2, 0))
294        + a(0, 2) * (a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0));
295    if det == 0.0 || !det.is_finite() {
296        return None;
297    }
298    let inv = [
299        [
300            (a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1)) / det,
301            (a(0, 2) * a(2, 1) - a(0, 1) * a(2, 2)) / det,
302            (a(0, 1) * a(1, 2) - a(0, 2) * a(1, 1)) / det,
303        ],
304        [
305            (a(1, 2) * a(2, 0) - a(1, 0) * a(2, 2)) / det,
306            (a(0, 0) * a(2, 2) - a(0, 2) * a(2, 0)) / det,
307            (a(0, 2) * a(1, 0) - a(0, 0) * a(1, 2)) / det,
308        ],
309        [
310            (a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0)) / det,
311            (a(0, 1) * a(2, 0) - a(0, 0) * a(2, 1)) / det,
312            (a(0, 0) * a(1, 1) - a(0, 1) * a(1, 0)) / det,
313        ],
314    ];
315    let t = [a(0, 3), a(1, 3), a(2, 3)];
316    let mut out = [[0f32; 4]; 3];
317    for r in 0..3 {
318        for c in 0..3 {
319            out[r][c] = inv[r][c] as f32;
320        }
321        out[r][3] = -(inv[r][0] * t[0] + inv[r][1] * t[1] + inv[r][2] * t[2]) as f32;
322    }
323    Some(out)
324}
325
326impl Built {
327    /// Instance `i`: what its hit-group index, id and transform come from.
328    #[allow(clippy::indexing_slicing)] // every index is a node, instance, bottom level, geometry or primitive number that Built::new made from the scene it indexes
329    pub(super) fn instance(&self, i: u32) -> &Instance {
330        &self.instances[i as usize].instance
331    }
332    /// Instance `i`'s world-to-object transform, three rows of four.
333    #[allow(clippy::indexing_slicing)] // every index is a node, instance, bottom level, geometry or primitive number that Built::new made from the scene it indexes
334    pub(super) fn inverse(&self, i: u32) -> &[[f32; 4]; 3] {
335        &self.instances[i as usize].inverse
336    }
337
338    /// Build the hierarchies of a scene. An instance of a bottom level that does not exist, or with a singular
339    /// transform, is an error.
340    #[allow(clippy::indexing_slicing)] // k ranges over the three axes of a [f32; 3]
341    pub fn new(scene: &Scene) -> Result<Built, String> {
342        let mut bottoms = Vec::new();
343        for geometries in &scene.bottoms {
344            let mut primitives = Vec::new();
345            let mut bounds = Vec::new();
346            for (g, geometry) in geometries.iter().enumerate() {
347                for k in 0..geometry.primitives() {
348                    let mut b = Aabb::EMPTY;
349                    if geometry.procedural() {
350                        let corners = geometry
351                            .boxes
352                            .get(k)
353                            .ok_or_else(|| format!("geometry {g}: box {k} is past its boxes"))?;
354                        corners.iter().for_each(|&p| b.grow(p));
355                    } else {
356                        let tri = geometry.triangle(k).ok_or_else(|| {
357                            format!("geometry {g}: triangle {k} names a vertex that does not exist")
358                        })?;
359                        tri.iter().for_each(|&p| b.grow(p));
360                    }
361                    primitives.push((
362                        u32::try_from(g).map_err(|_| "more than 4294967295 geometries")?,
363                        u32::try_from(k).map_err(|_| "more than 4294967295 primitives")?,
364                    ));
365                    bounds.push(b);
366                }
367            }
368            let bvh = Bvh::build(&bounds);
369            bottoms.push(BuiltBottom {
370                geometries: geometries.clone(),
371                primitives,
372                bvh,
373            });
374        }
375        let mut instances = Vec::new();
376        let mut bounds = Vec::new();
377        for (i, instance) in scene.instances.iter().enumerate() {
378            let bottom = bottoms.get(instance.bottom).ok_or_else(|| {
379                format!(
380                    "instance {i} is of bottom level {}, which does not exist",
381                    instance.bottom
382                )
383            })?;
384            let inverse =
385                invert(&instance.transform).ok_or_else(|| format!("instance {i}: a singular transform"))?;
386            let mut b = Aabb::EMPTY;
387            if let Some(root) = bottom.bvh.nodes.first() {
388                for corner in 0..8 {
389                    let c: Vec3 = std::array::from_fn(|k| {
390                        if corner >> k & 1 == 0 {
391                            root.bounds.min[k]
392                        } else {
393                            root.bounds.max[k]
394                        }
395                    });
396                    b.grow(transform_point(&instance.transform, c));
397                }
398            }
399            bounds.push(b);
400            instances.push(BuiltInstance {
401                instance: instance.clone(),
402                inverse,
403            });
404        }
405        let top = Bvh::build(&bounds);
406        Ok(Built {
407            bottoms,
408            instances,
409            top,
410        })
411    }
412}
413
414/// A committed or candidate hit.
415#[derive(Clone, Copy, Debug)]
416pub(super) struct Hit {
417    /// naga's intersection kind: 1 a triangle, 2 a generated intersection, 3 a procedural candidate.
418    pub kind: u32,
419    pub t: f32,
420    pub barycentrics: [f32; 2],
421    pub front: bool,
422    pub instance: u32,
423    pub geometry: u32,
424    pub primitive: u32,
425    /// Whether the geometry's primitives are opaque to this ray (a program offered a non-opaque hit may reject it).
426    pub opaque: bool,
427}
428
429#[derive(Clone, Copy, Debug)]
430enum Frame {
431    Top(u32),
432    Instance(u32),
433    Bottom(u32),
434    Triangle(u32, u32),
435}
436
437/// A ray query: the ray, the search's place, and what it has found.
438#[derive(Clone, Debug)]
439pub(super) struct Query {
440    scene: iridia::ResourceId,
441    pub flags: u32,
442    mask: u32,
443    pub tmin: f32,
444    tmax: f32,
445    pub origin: Vec3,
446    pub direction: Vec3,
447    stack: Vec<Frame>,
448    started: bool,
449    done: bool,
450    /// The instance being searched, and the ray in its object space.
451    inside: Option<(u32, Vec3, Vec3)>,
452    pub candidate: Option<Hit>,
453    pub committed: Option<Hit>,
454}
455
456fn inverse_direction(d: Vec3) -> Vec3 {
457    d.map(|x| 1.0 / x)
458}
459
460impl Query {
461    pub(super) fn new(
462        scene: iridia::ResourceId,
463        flags: u32,
464        mask: u32,
465        tmin: f32,
466        tmax: f32,
467        origin: Vec3,
468        direction: Vec3,
469    ) -> Query {
470        Query {
471            scene,
472            flags,
473            mask,
474            tmin,
475            tmax,
476            origin,
477            direction,
478            stack: Vec::new(),
479            started: false,
480            done: false,
481            inside: None,
482            candidate: None,
483            committed: None,
484        }
485    }
486
487    pub(super) fn scene(&self) -> iridia::ResourceId {
488        self.scene
489    }
490
491    /// The far end of the ray's current range: the ray's t max, then the nearest hit committed so far.
492    pub(super) fn tmax(&self) -> f32 {
493        self.tmax
494    }
495
496    /// End the search.
497    pub(super) fn terminate(&mut self) {
498        self.done = true;
499        self.stack.clear();
500    }
501
502    /// Commit the candidate (a non-opaque triangle the program accepts).
503    pub(super) fn confirm(&mut self) -> Result<(), String> {
504        let Some(hit) = self.candidate.take() else {
505            return Err("a candidate hit committed when there is none".into());
506        };
507        self.tmax = hit.t;
508        self.committed = Some(hit);
509        if self.flags & flags::ACCEPT_FIRST_HIT != 0 {
510            self.terminate();
511        }
512        Ok(())
513    }
514
515    /// Whether a geometry's primitives are opaque to this ray: the ray's flags decide first, then the instance's,
516    /// then the geometry's own.
517    fn opaque_of(&self, geometry: &Geometry, f: InstanceFlags) -> bool {
518        if self.flags & flags::FORCE_OPAQUE != 0 {
519            true
520        } else if self.flags & flags::FORCE_NON_OPAQUE != 0 {
521            false
522        } else if f.force_opaque {
523            true
524        } else if f.force_non_opaque {
525            false
526        } else {
527            geometry.opaque
528        }
529    }
530
531    /// Commit a procedural candidate at distance `t`, which the program found by intersecting the primitive itself.
532    ///
533    /// DXR (section "RayQuery CommitProceduralPrimitiveHit") puts the check on the program: it "must only commit hit(s)
534    /// when it manually determines that a given hit being reported satisfies TMin <= t <= TMax", within "the most recent
535    /// [TMin ... TMax] (inclusive) range", and the system does not do that work for procedural primitives (section
536    /// "TraceRayInline control flow", note 4). What a commit outside that range does is not specified. This executor
537    /// ignores it; WARP takes it even when it is farther than the committed hit, so the WARP gate's program commits only
538    /// nearer hits and nothing checks this against WARP.
539    ///
540    /// Not specified either (the section "Ray extents" speaks of the intersection's t, not of the box): whether a box
541    /// the ray starts inside is offered. This executor offers it (the box overlaps the range); WARP offers none, and the
542    /// gate leaves such rays out.
543    pub(super) fn generate(&mut self, t: f32) -> Result<(), String> {
544        let Some(mut hit) = self.candidate else {
545            return Err("an intersection generated when there is no candidate".into());
546        };
547        if hit.kind != 3 {
548            return Err(
549                "an intersection generated for a candidate that is not a procedural primitive".into(),
550            );
551        }
552        if !(self.tmin <= t && t <= self.tmax) {
553            return Ok(());
554        }
555        hit.kind = 2;
556        hit.t = t;
557        // the candidate stays: a program may commit more than one hit for it, each within the range the last left
558        self.tmax = t;
559        self.committed = Some(hit);
560        if self.flags & flags::ACCEPT_FIRST_HIT != 0 {
561            self.terminate();
562        }
563        Ok(())
564    }
565
566    /// Go on to the next candidate: true when there is one for the program to commit or let go.
567    #[allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)] // every index is a node, instance, bottom level, geometry or primitive number that Built::new made from the scene it indexes
568    pub(super) fn proceed(&mut self, scene: &Built) -> bool {
569        self.candidate = None;
570        if self.done {
571            return false;
572        }
573        if !self.started {
574            self.started = true;
575            if !scene.top.nodes.is_empty() {
576                self.stack.push(Frame::Top(0));
577            }
578        }
579        while let Some(frame) = self.stack.pop() {
580            match frame {
581                Frame::Top(n) => {
582                    let node = &scene.top.nodes[n as usize];
583                    let inv = inverse_direction(self.direction);
584                    if !crosses(&node.bounds, self.origin, inv, self.tmin, self.tmax) {
585                        continue;
586                    }
587                    if node.count == 0 {
588                        self.stack.push(Frame::Top(node.first + 1));
589                        self.stack.push(Frame::Top(node.first));
590                    } else {
591                        for k in (node.first..node.first + node.count).rev() {
592                            self.stack.push(Frame::Instance(scene.top.order[k as usize]));
593                        }
594                    }
595                }
596                Frame::Instance(i) => {
597                    let instance = &scene.instances[i as usize];
598                    if self.mask & u32::from(instance.instance.mask) == 0 {
599                        continue;
600                    }
601                    let bottom = &scene.bottoms[instance.instance.bottom];
602                    if bottom.bvh.nodes.is_empty() {
603                        continue;
604                    }
605                    self.inside = Some((
606                        i,
607                        transform_point(&instance.inverse, self.origin),
608                        transform_vector(&instance.inverse, self.direction),
609                    ));
610                    self.stack.push(Frame::Bottom(0));
611                }
612                Frame::Bottom(n) => {
613                    let (i, origin, direction) = self.inside.expect("inside an instance");
614                    let bottom = &scene.bottoms[scene.instances[i as usize].instance.bottom];
615                    let node = &bottom.bvh.nodes[n as usize];
616                    if !crosses(
617                        &node.bounds,
618                        origin,
619                        inverse_direction(direction),
620                        self.tmin,
621                        self.tmax,
622                    ) {
623                        continue;
624                    }
625                    if node.count == 0 {
626                        self.stack.push(Frame::Bottom(node.first + 1));
627                        self.stack.push(Frame::Bottom(node.first));
628                    } else {
629                        for k in (node.first..node.first + node.count).rev() {
630                            let (g, t) = bottom.primitives[bottom.bvh.order[k as usize] as usize];
631                            self.stack.push(Frame::Triangle(g, t));
632                        }
633                    }
634                }
635                Frame::Triangle(g, k) => {
636                    let (i, origin, direction) = self.inside.expect("inside an instance");
637                    let built = &scene.instances[i as usize];
638                    let geometry = &scene.bottoms[built.instance.bottom].geometries[g as usize];
639                    if geometry.procedural() {
640                        if self.flags & flags::SKIP_PROCEDURAL != 0 {
641                            continue;
642                        }
643                        let [min, max] = geometry.boxes[k as usize];
644                        let boxed = Aabb { min, max };
645                        if !crosses(&boxed, origin, inverse_direction(direction), self.tmin, self.tmax) {
646                            continue;
647                        }
648                        let f = built.instance.flags;
649                        let opaque = self.opaque_of(geometry, f);
650                        if (self.flags & flags::CULL_OPAQUE != 0 && opaque)
651                            || (self.flags & flags::CULL_NON_OPAQUE != 0 && !opaque)
652                        {
653                            continue;
654                        }
655                        self.candidate = Some(Hit {
656                            kind: 3,
657                            t: self.tmax,
658                            barycentrics: [0.0; 2],
659                            front: false,
660                            instance: i,
661                            geometry: g,
662                            primitive: k,
663                            opaque,
664                        });
665                        return true;
666                    }
667                    if self.flags & flags::SKIP_TRIANGLES != 0 {
668                        continue;
669                    }
670                    let Some(tri) = geometry.triangle(k as usize) else {
671                        continue;
672                    };
673                    let Some((t, b1, b2, front_clockwise)) = intersect(&tri, origin, direction) else {
674                        continue;
675                    };
676                    // section "Ray extents": a triangle is hit only at TMin < t < TMax, both ends excluded
677                    if !(self.tmin < t && t < self.tmax) {
678                        continue;
679                    }
680                    let f = built.instance.flags;
681                    // front-facing is clockwise unless the instance says counter-clockwise
682                    let front = front_clockwise != f.front_counter_clockwise;
683                    let opaque = self.opaque_of(geometry, f);
684                    if (self.flags & flags::CULL_OPAQUE != 0 && opaque)
685                        || (self.flags & flags::CULL_NON_OPAQUE != 0 && !opaque)
686                    {
687                        continue;
688                    }
689                    if !f.cull_disable
690                        && ((self.flags & flags::CULL_BACK_FACING != 0 && !front)
691                            || (self.flags & flags::CULL_FRONT_FACING != 0 && front))
692                    {
693                        continue;
694                    }
695                    let hit = Hit {
696                        kind: 1,
697                        t,
698                        barycentrics: [b1, b2],
699                        front,
700                        instance: i,
701                        geometry: g,
702                        primitive: k,
703                        opaque,
704                    };
705                    if opaque {
706                        self.committed = Some(hit);
707                        self.tmax = t;
708                        if self.flags & flags::ACCEPT_FIRST_HIT != 0 {
709                            self.terminate();
710                            return false;
711                        }
712                    } else {
713                        self.candidate = Some(hit);
714                        return true;
715                    }
716                }
717            }
718        }
719        self.done = true;
720        false
721    }
722}
723
724/// The watertight ray-triangle test: the distance along the ray, the barycentric weights of the second and third
725/// vertices, and whether the triangle's winding is clockwise as the ray origin sees it. None for a miss.
726#[allow(
727    clippy::arithmetic_side_effects,
728    clippy::cast_possible_truncation,
729    clippy::indexing_slicing
730)] // kx, ky and kz are a permutation of the three axes of a [f32; 3]; the f64 results round to the f32 the test returns
731fn intersect(tri: &[Vec3; 3], origin: Vec3, direction: Vec3) -> Option<(f32, f32, f32, bool)> {
732    // the axis the ray runs most along, and the other two in an order that keeps the winding
733    let abs = direction.map(f32::abs);
734    let kz = if abs[0] >= abs[1] && abs[0] >= abs[2] {
735        0
736    } else if abs[1] >= abs[2] {
737        1
738    } else {
739        2
740    };
741    let (mut kx, mut ky) = ((kz + 1) % 3, (kz + 2) % 3);
742    if direction[kz] < 0.0 {
743        std::mem::swap(&mut kx, &mut ky);
744    }
745    let s = [
746        direction[kx] / direction[kz],
747        direction[ky] / direction[kz],
748        1.0 / direction[kz],
749    ];
750    let rel: [Vec3; 3] = std::array::from_fn(|j| std::array::from_fn(|k| tri[j][k] - origin[k]));
751    let shear = |p: &Vec3| -> [f32; 2] { [p[kx] - s[0] * p[kz], p[ky] - s[1] * p[kz]] };
752    let (a, b, c) = (shear(&rel[0]), shear(&rel[1]), shear(&rel[2]));
753    let mut u = c[0] * b[1] - c[1] * b[0];
754    let mut v = a[0] * c[1] - a[1] * c[0];
755    let mut w = b[0] * a[1] - b[1] * a[0];
756    if u == 0.0 || v == 0.0 || w == 0.0 {
757        // an edge: redo the products in double precision, as the algorithm does
758        let (c0, c1, b0, b1, a0, a1) = (
759            f64::from(c[0]),
760            f64::from(c[1]),
761            f64::from(b[0]),
762            f64::from(b[1]),
763            f64::from(a[0]),
764            f64::from(a[1]),
765        );
766        u = (c0 * b1 - c1 * b0) as f32;
767        v = (a0 * c1 - a1 * c0) as f32;
768        w = (b0 * a1 - b1 * a0) as f32;
769    }
770    if (u < 0.0 || v < 0.0 || w < 0.0) && (u > 0.0 || v > 0.0 || w > 0.0) {
771        return None;
772    }
773    let det = u + v + w;
774    if det == 0.0 {
775        return None;
776    }
777    let (az, bz, cz) = (s[2] * rel[0][kz], s[2] * rel[1][kz], s[2] * rel[2][kz]);
778    let t = (u * az + v * bz + w * cz) / det;
779    // the winding the ray origin sees: the sign of the triangle's normal against the direction
780    let e1: Vec3 = std::array::from_fn(|k| tri[1][k] - tri[0][k]);
781    let e2: Vec3 = std::array::from_fn(|k| tri[2][k] - tri[0][k]);
782    let normal = [
783        e1[1] * e2[2] - e1[2] * e2[1],
784        e1[2] * e2[0] - e1[0] * e2[2],
785        e1[0] * e2[1] - e1[1] * e2[0],
786    ];
787    let along: f32 = (0..3).map(|k| normal[k] * direction[k]).sum();
788    Some((t, v / det, w / det, along < 0.0))
789}
790
791/// The intersection a query reports, as the fields of naga's `RayIntersection`, from `hit` in `scene`.
792pub(super) struct Reported {
793    pub kind: u32,
794    pub t: f32,
795    pub custom: u32,
796    pub instance: u32,
797    pub sbt: u32,
798    pub geometry: u32,
799    pub primitive: u32,
800    pub barycentrics: [f32; 2],
801    pub front: bool,
802    /// Four columns of three rows each.
803    pub object_to_world: [Vec3; 4],
804    pub world_to_object: [Vec3; 4],
805}
806
807impl Reported {
808    pub(super) fn none() -> Reported {
809        Reported {
810            kind: 0,
811            t: 0.0,
812            custom: 0,
813            instance: 0,
814            sbt: 0,
815            geometry: 0,
816            primitive: 0,
817            barycentrics: [0.0; 2],
818            front: false,
819            object_to_world: [[0.0; 3]; 4],
820            world_to_object: [[0.0; 3]; 4],
821        }
822    }
823
824    #[allow(clippy::indexing_slicing)] // the matrix columns and rows are the 4 and 3 of a [[f32; 4]; 3]
825    pub(super) fn of(hit: &Hit, scene: &Built) -> Reported {
826        let built = &scene.instances[hit.instance as usize];
827        let columns =
828            |m: &[[f32; 4]; 3]| -> [Vec3; 4] { std::array::from_fn(|c| std::array::from_fn(|r| m[r][c])) };
829        Reported {
830            kind: hit.kind,
831            t: hit.t,
832            custom: built.instance.id & 0x00ff_ffff,
833            instance: hit.instance,
834            sbt: built.instance.contribution & 0x00ff_ffff,
835            geometry: hit.geometry,
836            primitive: hit.primitive,
837            barycentrics: hit.barycentrics,
838            front: hit.front,
839            object_to_world: columns(&built.instance.transform),
840            world_to_object: columns(&built.inverse),
841        }
842    }
843}
844
845/// The scenes a run was bound with.
846pub(super) type Scenes = BTreeMap<iridia::ResourceId, std::sync::Arc<Built>>;