Skip to main content

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}