Skip to main content

concinnity_core/render/
rt_refit.rs

1//! Backend-agnostic refit cadence for the per-frame skinned bottom-level
2//! acceleration structures. A skinned object's BLAS traces vertices a compute
3//! pass re-poses every frame, so it has to be updated every frame -- but while
4//! the triangle set is unchanged that update can be a REFIT (Vulkan's
5//! `VK_BUILD_ACCELERATION_STRUCTURE_MODE_UPDATE_KHR`, DXR's `PERFORM_UPDATE`),
6//! which re-fits the existing tree's bounding volumes in place instead of
7//! rebuilding it from scratch.
8//!
9//! A refit keeps the tree the last full build produced, so traversal quality
10//! decays as the pose drifts away from the one that tree was built for; this
11//! module bounds that with a periodic full rebuild. It owns only the pure
12//! decision -- the descriptors, the allocation and the recorded build are
13//! per-backend (directx/raytrace.rs, vulkan/raytrace.rs). Split out so the
14//! cadence is unit-testable without a GPU.
15//!
16//! Consumed by the DirectX + Vulkan backends. The Metal backend keeps its own
17//! equivalent copy (metal/rt_ring.rs), the same split `rt_topology` already has.
18
19use alloc::vec::Vec;
20
21/// Full rebuilds per ring slot: after this many consecutive refits the next
22/// skinned update rebuilds that slot's BLAS from scratch. Each slot counts
23/// independently and they are touched on different frames, so the rebuilds
24/// stagger rather than landing on one frame.
25pub const REFIT_LIMIT: u32 = 32;
26
27/// The geometry one skinned BLAS is built over: its slice of the shared skinned
28/// index buffer plus the vertex range the deformed buffer spans. Equal
29/// signatures mean the same triangles addressing the same vertex range with only
30/// the positions moved, which is exactly when a refit is legal; anything else (a
31/// mesh hot-reload, a different mesh becoming visible, a grown deformed buffer)
32/// changes the geometry description and needs a full rebuild. `vertex_extent` is
33/// carried because both APIs require the vertex count to match the structure
34/// being updated, even though the vertex buffer's address may move.
35#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
36pub struct SkinnedShape {
37    /// First index of this slot's range in the shared index buffer.
38    pub index_offset: usize,
39    /// Indices in this slot's range.
40    pub index_count: usize,
41    /// Vertices the slot's range spans.
42    pub vertex_extent: u32,
43}
44
45/// Whether a slot's skinned BLAS can be refit from this frame's pose or must be
46/// rebuilt from scratch.
47#[derive(Clone, Copy, PartialEq, Eq, Debug)]
48pub enum BlasUpdate {
49    /// Rebuild the acceleration structure from scratch.
50    Build,
51    /// Refit the existing acceleration structure in place.
52    Refit,
53}
54
55// Rebuild when the geometry changed (a refit is then illegal), when the slot has
56// no built tree to refit, or once every `limit` refits to bound the quality
57// drift. Pure so the cadence is unit-testable without a GPU.
58fn blas_update(shape_changed: bool, built: bool, refits: u32, limit: u32) -> BlasUpdate {
59    if shape_changed || !built || refits >= limit {
60        BlasUpdate::Build
61    } else {
62        BlasUpdate::Refit
63    }
64}
65
66/// One ring slot's refit bookkeeping: the geometry its BLAS were last built over,
67/// whether they hold a tree a refit can update, and how many consecutive refits
68/// have run since the last full build.
69#[derive(Default)]
70pub struct SkinnedRefit {
71    shapes: Vec<SkinnedShape>,
72    built: bool,
73    refits: u32,
74}
75
76impl SkinnedRefit {
77    /// How this frame's skinned BLAS should be updated, recording the choice so
78    /// the refit run stays bounded and the shapes so the next frame can compare.
79    /// `storage_changed` must be set when the structures or the buffer they trace
80    /// were (re)allocated this frame, which leaves no tree to refit. Call once the
81    /// frame's fallible work has passed: recording a build the backend never
82    /// encodes would leave the slot claiming a tree a later refit cannot update.
83    pub fn plan(&mut self, shapes: &[SkinnedShape], storage_changed: bool) -> BlasUpdate {
84        let changed = storage_changed || self.shapes != shapes;
85        let update = blas_update(changed, self.built, self.refits, REFIT_LIMIT);
86        if changed {
87            self.shapes.clear();
88            self.shapes.extend_from_slice(shapes);
89        }
90        match update {
91            BlasUpdate::Build => {
92                self.built = true;
93                self.refits = 0;
94            }
95            BlasUpdate::Refit => self.refits += 1,
96        }
97        update
98    }
99
100    /// Forget the tree this slot's BLAS hold, so the next update rebuilds rather
101    /// than refitting. Called when the slot stops publishing (no skinned object is
102    /// visible) or its structures are otherwise invalidated.
103    pub fn reset(&mut self) {
104        self.shapes.clear();
105        self.built = false;
106        self.refits = 0;
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    fn shape(tag: usize) -> SkinnedShape {
115        SkinnedShape {
116            index_offset: tag,
117            index_count: 300,
118            vertex_extent: 512,
119        }
120    }
121
122    #[test]
123    fn a_changed_triangle_set_forces_a_full_build() {
124        // A refit cannot add or remove geometry, so a changed shape always
125        // rebuilds even when the slot has a tree and refits to spare.
126        assert_eq!(blas_update(true, true, 0, 32), BlasUpdate::Build);
127    }
128
129    #[test]
130    fn an_unbuilt_slot_cannot_be_refit() {
131        assert_eq!(blas_update(false, false, 0, 32), BlasUpdate::Build);
132    }
133
134    #[test]
135    fn a_stable_shape_refits_until_the_limit() {
136        assert_eq!(blas_update(false, true, 0, 32), BlasUpdate::Refit);
137        assert_eq!(blas_update(false, true, 31, 32), BlasUpdate::Refit);
138        // The 32nd refit is instead a rebuild, bounding the quality drift.
139        assert_eq!(blas_update(false, true, 32, 32), BlasUpdate::Build);
140        assert_eq!(blas_update(false, true, 99, 32), BlasUpdate::Build);
141    }
142
143    #[test]
144    fn a_zero_limit_never_refits() {
145        assert_eq!(blas_update(false, true, 0, 0), BlasUpdate::Build);
146    }
147
148    #[test]
149    fn shape_equality_covers_slice_and_vertex_extent() {
150        let a = shape(12);
151        assert_eq!(a, a);
152        assert_ne!(a, shape(13));
153        assert_ne!(
154            a,
155            SkinnedShape {
156                index_count: 303,
157                ..a
158            }
159        );
160        assert_ne!(
161            a,
162            SkinnedShape {
163                vertex_extent: 513,
164                ..a
165            }
166        );
167    }
168
169    #[test]
170    fn a_fresh_slot_builds_then_refits() {
171        let mut slot = SkinnedRefit::default();
172        let shapes = [shape(0), shape(1)];
173        assert_eq!(slot.plan(&shapes, false), BlasUpdate::Build);
174        assert_eq!(slot.plan(&shapes, false), BlasUpdate::Refit);
175        assert_eq!(slot.plan(&shapes, false), BlasUpdate::Refit);
176    }
177
178    #[test]
179    fn reallocated_storage_rebuilds_even_on_an_unchanged_shape() {
180        let mut slot = SkinnedRefit::default();
181        let shapes = [shape(0)];
182        assert_eq!(slot.plan(&shapes, false), BlasUpdate::Build);
183        assert_eq!(slot.plan(&shapes, true), BlasUpdate::Build);
184        assert_eq!(slot.plan(&shapes, false), BlasUpdate::Refit);
185    }
186
187    #[test]
188    fn a_changed_object_set_rebuilds_and_restarts_the_run() {
189        let mut slot = SkinnedRefit::default();
190        assert_eq!(slot.plan(&[shape(0)], false), BlasUpdate::Build);
191        assert_eq!(slot.plan(&[shape(0)], false), BlasUpdate::Refit);
192        // A second skinned object became visible: one more BLAS, so a full build.
193        assert_eq!(slot.plan(&[shape(0), shape(1)], false), BlasUpdate::Build);
194        assert_eq!(slot.plan(&[shape(0), shape(1)], false), BlasUpdate::Refit);
195    }
196
197    #[test]
198    fn the_refit_run_is_bounded_by_the_limit() {
199        let mut slot = SkinnedRefit::default();
200        let shapes = [shape(0)];
201        assert_eq!(slot.plan(&shapes, false), BlasUpdate::Build);
202        for _ in 0..REFIT_LIMIT {
203            assert_eq!(slot.plan(&shapes, false), BlasUpdate::Refit);
204        }
205        // The run has reached the limit: the next update is a full rebuild, and
206        // the run then restarts.
207        assert_eq!(slot.plan(&shapes, false), BlasUpdate::Build);
208        assert_eq!(slot.plan(&shapes, false), BlasUpdate::Refit);
209    }
210
211    #[test]
212    fn reset_makes_the_next_update_a_build() {
213        let mut slot = SkinnedRefit::default();
214        let shapes = [shape(0)];
215        assert_eq!(slot.plan(&shapes, false), BlasUpdate::Build);
216        assert_eq!(slot.plan(&shapes, false), BlasUpdate::Refit);
217        slot.reset();
218        assert_eq!(slot.plan(&shapes, false), BlasUpdate::Build);
219    }
220}