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}