|
| 1 | +//! Previous-position ownership for raylib-style immediate 3D primitives. |
| 2 | +//! |
| 3 | +//! Immediate vertices are rebuilt in world space every frame, so a previous |
| 4 | +//! model matrix cannot describe their motion. Stable submission slots own a |
| 5 | +//! compact CPU copy of the prior positions instead. The existing tangent lane |
| 6 | +//! is unused by `pipeline_3d`; xyz carries the previous world position and w |
| 7 | +//! marks that payload for the vertex shader. This changes neither the vertex |
| 8 | +//! stride nor GPU upload size. |
| 9 | +
|
| 10 | +use super::{Renderer, Vertex3D}; |
| 11 | + |
| 12 | +pub(super) const PREVIOUS_POSITION_MARKER: f32 = 2.0; |
| 13 | + |
| 14 | +#[derive(Clone, Copy, Debug, Eq, PartialEq)] |
| 15 | +pub(super) enum PrimitiveKind { |
| 16 | + Cube, |
| 17 | + CubeWires, |
| 18 | + Sphere, |
| 19 | + SphereWires, |
| 20 | + Cylinder, |
| 21 | + Plane, |
| 22 | + Grid, |
| 23 | + Ray, |
| 24 | +} |
| 25 | + |
| 26 | +#[derive(Clone, Copy, Debug)] |
| 27 | +struct Range { |
| 28 | + kind: PrimitiveKind, |
| 29 | + start: usize, |
| 30 | + count: usize, |
| 31 | +} |
| 32 | + |
| 33 | +#[derive(Default)] |
| 34 | +pub(super) struct History { |
| 35 | + previous_positions: Vec<[f32; 3]>, |
| 36 | + current_positions: Vec<[f32; 3]>, |
| 37 | + previous_ranges: Vec<Range>, |
| 38 | + current_ranges: Vec<Range>, |
| 39 | + next_slot: usize, |
| 40 | +} |
| 41 | + |
| 42 | +impl History { |
| 43 | + pub(super) fn begin_frame(&mut self) { |
| 44 | + std::mem::swap(&mut self.previous_positions, &mut self.current_positions); |
| 45 | + self.current_positions.clear(); |
| 46 | + std::mem::swap(&mut self.previous_ranges, &mut self.current_ranges); |
| 47 | + self.current_ranges.clear(); |
| 48 | + self.next_slot = 0; |
| 49 | + } |
| 50 | + |
| 51 | + pub(super) fn reset(&mut self) { |
| 52 | + self.previous_positions.clear(); |
| 53 | + self.current_positions.clear(); |
| 54 | + self.previous_ranges.clear(); |
| 55 | + self.current_ranges.clear(); |
| 56 | + self.next_slot = 0; |
| 57 | + } |
| 58 | + |
| 59 | + /// Attach the matching prior-frame positions to one completed primitive. |
| 60 | + /// |
| 61 | + /// Kind and vertex count are a topology fence. A first appearance, a |
| 62 | + /// reordered primitive of another kind, or a changed tessellation seeds |
| 63 | + /// previous=current so it cannot create a false motion vector. |
| 64 | + pub(super) fn record(&mut self, kind: PrimitiveKind, vertices: &mut [Vertex3D]) { |
| 65 | + let previous = self |
| 66 | + .previous_ranges |
| 67 | + .get(self.next_slot) |
| 68 | + .copied() |
| 69 | + .filter(|range| { |
| 70 | + range.kind == kind |
| 71 | + && range.count == vertices.len() |
| 72 | + && range.start.saturating_add(range.count) <= self.previous_positions.len() |
| 73 | + }); |
| 74 | + let current_start = self.current_positions.len(); |
| 75 | + for (index, vertex) in vertices.iter_mut().enumerate() { |
| 76 | + let prior = previous |
| 77 | + .map(|range| self.previous_positions[range.start + index]) |
| 78 | + .unwrap_or(vertex.position); |
| 79 | + vertex.tangent = [prior[0], prior[1], prior[2], PREVIOUS_POSITION_MARKER]; |
| 80 | + self.current_positions.push(vertex.position); |
| 81 | + } |
| 82 | + self.current_ranges.push(Range { |
| 83 | + kind, |
| 84 | + start: current_start, |
| 85 | + count: vertices.len(), |
| 86 | + }); |
| 87 | + self.next_slot += 1; |
| 88 | + } |
| 89 | + |
| 90 | + #[cfg_attr(target_arch = "wasm32", allow(dead_code))] |
| 91 | + pub(super) fn stats(&self) -> (usize, usize) { |
| 92 | + let entries = self.current_ranges.len(); |
| 93 | + let bytes = (self.previous_positions.capacity() + self.current_positions.capacity()) |
| 94 | + * std::mem::size_of::<[f32; 3]>() |
| 95 | + + (self.previous_ranges.capacity() + self.current_ranges.capacity()) |
| 96 | + * std::mem::size_of::<Range>(); |
| 97 | + (entries, bytes) |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +impl Renderer { |
| 102 | + pub(super) fn record_immediate_motion(&mut self, kind: PrimitiveKind, vertex_start: usize) { |
| 103 | + self.immediate_motion |
| 104 | + .record(kind, &mut self.vertices_3d[vertex_start..]); |
| 105 | + } |
| 106 | +} |
| 107 | + |
| 108 | +#[cfg(test)] |
| 109 | +mod tests { |
| 110 | + use super::*; |
| 111 | + |
| 112 | + fn vertex(position: [f32; 3]) -> Vertex3D { |
| 113 | + Vertex3D { |
| 114 | + position, |
| 115 | + tangent: [9.0; 4], |
| 116 | + ..Default::default() |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + fn prior(vertex: &Vertex3D) -> [f32; 3] { |
| 121 | + [vertex.tangent[0], vertex.tangent[1], vertex.tangent[2]] |
| 122 | + } |
| 123 | + |
| 124 | + #[test] |
| 125 | + fn first_appearance_seeds_zero_motion_then_stable_slot_uses_prior_position() { |
| 126 | + let mut history = History::default(); |
| 127 | + history.begin_frame(); |
| 128 | + let mut first = [vertex([1.0, 2.0, 3.0])]; |
| 129 | + history.record(PrimitiveKind::Cube, &mut first); |
| 130 | + assert_eq!(prior(&first[0]), first[0].position); |
| 131 | + assert_eq!(first[0].tangent[3], PREVIOUS_POSITION_MARKER); |
| 132 | + |
| 133 | + history.begin_frame(); |
| 134 | + let mut moved = [vertex([4.0, 5.0, 6.0])]; |
| 135 | + history.record(PrimitiveKind::Cube, &mut moved); |
| 136 | + assert_eq!(prior(&moved[0]), [1.0, 2.0, 3.0]); |
| 137 | + } |
| 138 | + |
| 139 | + #[test] |
| 140 | + fn kind_and_topology_mismatches_cannot_inherit_unrelated_motion() { |
| 141 | + let mut history = History::default(); |
| 142 | + history.begin_frame(); |
| 143 | + history.record( |
| 144 | + PrimitiveKind::Cube, |
| 145 | + &mut [vertex([1.0, 0.0, 0.0]), vertex([2.0, 0.0, 0.0])], |
| 146 | + ); |
| 147 | + |
| 148 | + history.begin_frame(); |
| 149 | + let mut wrong_kind = [vertex([8.0, 0.0, 0.0]), vertex([9.0, 0.0, 0.0])]; |
| 150 | + history.record(PrimitiveKind::Sphere, &mut wrong_kind); |
| 151 | + assert_eq!(prior(&wrong_kind[0]), wrong_kind[0].position); |
| 152 | + |
| 153 | + history.begin_frame(); |
| 154 | + let mut wrong_count = [vertex([12.0, 0.0, 0.0])]; |
| 155 | + history.record(PrimitiveKind::Sphere, &mut wrong_count); |
| 156 | + assert_eq!(prior(&wrong_count[0]), wrong_count[0].position); |
| 157 | + } |
| 158 | + |
| 159 | + #[test] |
| 160 | + fn an_empty_frame_breaks_submission_history() { |
| 161 | + let mut history = History::default(); |
| 162 | + history.begin_frame(); |
| 163 | + history.record(PrimitiveKind::Ray, &mut [vertex([1.0, 0.0, 0.0])]); |
| 164 | + history.begin_frame(); |
| 165 | + history.begin_frame(); |
| 166 | + |
| 167 | + let mut reappeared = [vertex([7.0, 0.0, 0.0])]; |
| 168 | + history.record(PrimitiveKind::Ray, &mut reappeared); |
| 169 | + assert_eq!(prior(&reappeared[0]), reappeared[0].position); |
| 170 | + } |
| 171 | +} |
0 commit comments