damascene_core/scene/geometry.rs
1//! Backend-neutral 3D geometry and the app-owned, versioned handles that
2//! carry it into a scene draw-op.
3//!
4//! # The handle pattern
5//!
6//! Geometry follows the same identity/caching shape as
7//! [`crate::surface::AppTexture`], but inverted across the device
8//! boundary. `AppTexture` wraps a *GPU* texture the app allocates; a
9//! [`GeometryHandle`] wraps *CPU* data the app owns and the backend
10//! uploads. This keeps the scene fully backend-neutral — the app never
11//! touches a device — while still giving the backend a stable
12//! [`GeometryId`] to cache GPU buffers against and a monotonic revision
13//! to decide when a re-upload is needed.
14//!
15//! Create a handle once, store it in app state, and reference it from the
16//! El tree every frame (cloning a handle is a cheap `Arc` bump, never a
17//! geometry copy). Mutate with [`GeometryHandle::set`]; the backend
18//! re-uploads only when the revision advances. Geometry that merely moves
19//! is a per-frame transform (a uniform), not a `set`, so it never
20//! re-uploads.
21//!
22//! Vertex types here speak glam ([`Vec3`] positions/normals) for the
23//! attributes apps build with, and authoring-space sRGBA `[f32; 4]` for
24//! colour. They are `#[repr(C)]` `Copy` logical types; each backend maps
25//! them to its own GPU vertex layout at upload (e.g. padding to `vec4`
26//! where a uniform/storage layout needs it, as the volumetric renderer
27//! does). `bytemuck` is available in core, so a backend may also cast
28//! directly once a `Pod` layout is settled.
29
30use std::sync::Arc;
31use std::sync::Mutex;
32use std::sync::atomic::{AtomicU64, Ordering};
33
34use glam::Vec3;
35
36use crate::scene::bounds::Aabb;
37
38/// Stable identity for one [`GeometryHandle`]'s GPU buffer cache entry.
39/// Allocated once when the handle is created and constant for its life;
40/// backends key their vertex/index buffers on it. Re-creating a handle
41/// (`GeometryHandle::new`) yields a fresh id, so the old buffer falls off
42/// the cache like any unused entry.
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
44pub struct GeometryId(pub u64);
45
46/// Allocate a fresh [`GeometryId`]. Called by [`GeometryHandle::new`];
47/// app code goes through the handle constructor, not this directly.
48pub fn next_geometry_id() -> GeometryId {
49 static COUNTER: AtomicU64 = AtomicU64::new(1);
50 GeometryId(COUNTER.fetch_add(1, Ordering::Relaxed))
51}
52
53/// One mesh vertex: object-space position and normal. Colour/material is
54/// per-mark, not per-vertex (see `Material` in the scene style).
55#[repr(C)]
56#[derive(Clone, Copy, Debug, PartialEq)]
57pub struct MeshVertex {
58 pub position: Vec3,
59 pub normal: Vec3,
60}
61
62/// A point/marker for scatter data. `color` is **authoring-space** sRGBA;
63/// the backend converts to the runner's working linear space at upload.
64#[repr(C)]
65#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct ScenePoint {
67 pub position: Vec3,
68 pub color: [f32; 4],
69}
70
71/// A line segment. `color` is **authoring-space** sRGBA (see [`ScenePoint`]).
72#[repr(C)]
73#[derive(Clone, Copy, Debug, PartialEq)]
74pub struct LineSegment {
75 pub start: Vec3,
76 pub end: Vec3,
77 pub color: [f32; 4],
78}
79
80/// Triangle geometry. Indexed when `indices` is `Some`, otherwise a flat
81/// triangle list over `vertices`.
82#[derive(Clone, Debug, Default, PartialEq)]
83pub struct MeshData {
84 pub vertices: Vec<MeshVertex>,
85 pub indices: Option<Vec<u32>>,
86}
87
88/// A batch of points/markers.
89#[derive(Clone, Debug, Default, PartialEq)]
90pub struct PointData {
91 pub points: Vec<ScenePoint>,
92}
93
94/// A batch of line segments.
95#[derive(Clone, Debug, Default, PartialEq)]
96pub struct LineData {
97 pub segments: Vec<LineSegment>,
98}
99
100/// Geometry that can report its own bounds, so handles can cache an
101/// [`Aabb`] for camera auto-framing and axis tick ranges.
102pub trait GeometryData: Send + Sync + 'static {
103 fn compute_bounds(&self) -> Aabb;
104}
105
106impl GeometryData for MeshData {
107 fn compute_bounds(&self) -> Aabb {
108 Aabb::from_points(self.vertices.iter().map(|v| v.position))
109 }
110}
111
112impl GeometryData for PointData {
113 fn compute_bounds(&self) -> Aabb {
114 Aabb::from_points(self.points.iter().map(|p| p.position))
115 }
116}
117
118impl GeometryData for LineData {
119 fn compute_bounds(&self) -> Aabb {
120 let mut bb = Aabb::EMPTY;
121 for seg in &self.segments {
122 bb.expand(seg.start);
123 bb.expand(seg.end);
124 }
125 bb
126 }
127}
128
129/// Shared inner store: the current data (behind an `Arc` so a backend can
130/// snapshot it cheaply under a short lock) plus its cached bounds.
131struct Inner<T> {
132 data: Arc<T>,
133 bounds: Aabb,
134}
135
136struct Store<T> {
137 id: GeometryId,
138 rev: AtomicU64,
139 inner: Mutex<Inner<T>>,
140}
141
142/// An app-owned, versioned handle to one batch of geometry.
143///
144/// Cheap to clone (`Arc` bump). See the [module docs](self) for the
145/// upload/caching contract. Type aliases [`MeshHandle`], [`PointsHandle`],
146/// and [`LinesHandle`] name the concrete instantiations used by the marks.
147#[derive(Clone)]
148pub struct GeometryHandle<T> {
149 store: Arc<Store<T>>,
150}
151
152impl<T: GeometryData> GeometryHandle<T> {
153 /// Create a handle, allocating a fresh [`GeometryId`] and computing
154 /// bounds. Revision starts at 1.
155 pub fn new(data: T) -> Self {
156 let bounds = data.compute_bounds();
157 Self {
158 store: Arc::new(Store {
159 id: next_geometry_id(),
160 rev: AtomicU64::new(1),
161 inner: Mutex::new(Inner {
162 data: Arc::new(data),
163 bounds,
164 }),
165 }),
166 }
167 }
168
169 /// Replace the geometry and advance the revision, recomputing bounds.
170 /// The backend re-uploads on its next draw because the revision moved.
171 ///
172 /// This is the baseline update path: it re-uploads the whole buffer.
173 /// Finer-grained `update_range` / `append` paths are designed to slot
174 /// in later (the handle is the stable surface) without breaking
175 /// callers of `set`.
176 pub fn set(&self, data: T) {
177 let bounds = data.compute_bounds();
178 {
179 let mut inner = self.store.inner.lock().unwrap();
180 inner.data = Arc::new(data);
181 inner.bounds = bounds;
182 }
183 self.store.rev.fetch_add(1, Ordering::Release);
184 }
185
186 /// Stable identity for backend buffer caches.
187 pub fn id(&self) -> GeometryId {
188 self.store.id
189 }
190
191 /// Monotonic revision; advances on every [`GeometryHandle::set`].
192 pub fn revision(&self) -> u64 {
193 self.store.rev.load(Ordering::Acquire)
194 }
195
196 /// Cached bounds of the current geometry.
197 pub fn bounds(&self) -> Aabb {
198 self.store.inner.lock().unwrap().bounds
199 }
200
201 /// Snapshot the current data and revision for upload. Returns an
202 /// `Arc` clone (no geometry copy) plus the revision it corresponds to,
203 /// so the backend can cache by `(id, revision)`.
204 pub fn snapshot(&self) -> (Arc<T>, u64) {
205 let inner = self.store.inner.lock().unwrap();
206 let rev = self.store.rev.load(Ordering::Acquire);
207 (Arc::clone(&inner.data), rev)
208 }
209}
210
211impl<T: GeometryData> std::fmt::Debug for GeometryHandle<T> {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 f.debug_struct("GeometryHandle")
214 .field("id", &self.id().0)
215 .field("revision", &self.revision())
216 .field("bounds", &self.bounds())
217 .finish()
218 }
219}
220
221/// Handle to triangle geometry (small mesh models, surfaces).
222pub type MeshHandle = GeometryHandle<MeshData>;
223/// Handle to point/marker geometry (scatter data).
224pub type PointsHandle = GeometryHandle<PointData>;
225/// Handle to line geometry (axes, wireframe, series, error bars).
226pub type LinesHandle = GeometryHandle<LineData>;
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn ids_are_unique() {
234 let a = PointsHandle::new(PointData::default());
235 let b = PointsHandle::new(PointData::default());
236 assert_ne!(a.id(), b.id());
237 }
238
239 #[test]
240 fn set_advances_revision_and_recomputes_bounds() {
241 let h = PointsHandle::new(PointData::default());
242 assert_eq!(h.revision(), 1);
243 assert!(!h.bounds().is_valid()); // empty
244
245 h.set(PointData {
246 points: vec![
247 ScenePoint {
248 position: Vec3::ZERO,
249 color: [1.0; 4],
250 },
251 ScenePoint {
252 position: Vec3::new(2.0, 4.0, 6.0),
253 color: [1.0; 4],
254 },
255 ],
256 });
257 assert_eq!(h.revision(), 2);
258 let bb = h.bounds();
259 assert_eq!(bb.min, Vec3::ZERO);
260 assert_eq!(bb.max, Vec3::new(2.0, 4.0, 6.0));
261 }
262
263 #[test]
264 fn snapshot_tracks_current_revision() {
265 let h = MeshHandle::new(MeshData::default());
266 let (_d0, r0) = h.snapshot();
267 assert_eq!(r0, 1);
268 h.set(MeshData {
269 vertices: vec![MeshVertex {
270 position: Vec3::ZERO,
271 normal: Vec3::Y,
272 }],
273 indices: None,
274 });
275 let (d1, r1) = h.snapshot();
276 assert_eq!(r1, 2);
277 assert_eq!(d1.vertices.len(), 1);
278 }
279
280 #[test]
281 fn clone_shares_store() {
282 let h = LinesHandle::new(LineData::default());
283 let c = h.clone();
284 assert_eq!(h.id(), c.id());
285 c.set(LineData {
286 segments: vec![LineSegment {
287 start: Vec3::ZERO,
288 end: Vec3::ONE,
289 color: [1.0; 4],
290 }],
291 });
292 // Mutation through the clone is visible through the original.
293 assert_eq!(h.revision(), 2);
294 assert!(h.bounds().is_valid());
295 }
296}