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
30// Lock in full per-item documentation for this module (issue #73).
31#![warn(missing_docs)]
32
33use std::sync::Arc;
34use std::sync::Mutex;
35use std::sync::atomic::{AtomicU64, Ordering};
36
37use glam::Vec3;
38
39use crate::scene::bounds::Aabb;
40
41/// Stable identity for one [`GeometryHandle`]'s GPU buffer cache entry.
42/// Allocated once when the handle is created and constant for its life;
43/// backends key their vertex/index buffers on it. Re-creating a handle
44/// (`GeometryHandle::new`) yields a fresh id, so the old buffer falls off
45/// the cache like any unused entry.
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
47pub struct GeometryId(pub u64);
48
49/// Allocate a fresh [`GeometryId`]. Called by [`GeometryHandle::new`];
50/// app code goes through the handle constructor, not this directly.
51pub fn next_geometry_id() -> GeometryId {
52 static COUNTER: AtomicU64 = AtomicU64::new(1);
53 GeometryId(COUNTER.fetch_add(1, Ordering::Relaxed))
54}
55
56/// One mesh vertex: object-space position and normal. Colour/material is
57/// per-mark, not per-vertex (see `Material` in the scene style).
58#[repr(C)]
59#[derive(Clone, Copy, Debug, PartialEq)]
60pub struct MeshVertex {
61 /// Object-space position (the mark's transform maps it to world).
62 pub position: Vec3,
63 /// Object-space normal, used for lighting. Expected unit length.
64 pub normal: Vec3,
65}
66
67/// A point/marker for scatter data. `color` is **authoring-space** sRGBA;
68/// the backend converts to the runner's working linear space at upload.
69#[repr(C)]
70#[derive(Clone, Copy, Debug, PartialEq)]
71pub struct ScenePoint {
72 /// Object-space position (the mark's transform maps it to world).
73 pub position: Vec3,
74 /// Authoring-space sRGBA, converted to the working space at upload.
75 pub color: [f32; 4],
76}
77
78/// A line segment. `color` is **authoring-space** sRGBA (see [`ScenePoint`]).
79#[repr(C)]
80#[derive(Clone, Copy, Debug, PartialEq)]
81pub struct LineSegment {
82 /// Object-space start point.
83 pub start: Vec3,
84 /// Object-space end point.
85 pub end: Vec3,
86 /// Authoring-space sRGBA, converted to the working space at upload.
87 pub color: [f32; 4],
88}
89
90/// Triangle geometry. Indexed when `indices` is `Some`, otherwise a flat
91/// triangle list over `vertices`.
92#[derive(Clone, Debug, Default, PartialEq)]
93pub struct MeshData {
94 /// The vertex set.
95 pub vertices: Vec<MeshVertex>,
96 /// Triangle indices into `vertices`; `None` draws `vertices` as a
97 /// flat triangle list.
98 pub indices: Option<Vec<u32>>,
99}
100
101/// A batch of points/markers.
102#[derive(Clone, Debug, Default, PartialEq)]
103pub struct PointData {
104 /// The points, in draw order.
105 pub points: Vec<ScenePoint>,
106}
107
108/// A batch of line segments.
109#[derive(Clone, Debug, Default, PartialEq)]
110pub struct LineData {
111 /// The segments, in draw order.
112 pub segments: Vec<LineSegment>,
113}
114
115/// Geometry that can report its own bounds, so handles can cache an
116/// [`Aabb`] for camera auto-framing and axis tick ranges.
117pub trait GeometryData: Send + Sync + 'static {
118 /// The [`Aabb`] enclosing every position in the batch —
119 /// [`Aabb::EMPTY`] when there are none.
120 fn compute_bounds(&self) -> Aabb;
121}
122
123impl GeometryData for MeshData {
124 fn compute_bounds(&self) -> Aabb {
125 Aabb::from_points(self.vertices.iter().map(|v| v.position))
126 }
127}
128
129impl GeometryData for PointData {
130 fn compute_bounds(&self) -> Aabb {
131 Aabb::from_points(self.points.iter().map(|p| p.position))
132 }
133}
134
135impl GeometryData for LineData {
136 fn compute_bounds(&self) -> Aabb {
137 let mut bb = Aabb::EMPTY;
138 for seg in &self.segments {
139 bb.expand(seg.start);
140 bb.expand(seg.end);
141 }
142 bb
143 }
144}
145
146/// Shared inner store: the current data (behind an `Arc` so a backend can
147/// snapshot it cheaply under a short lock) plus its cached bounds.
148struct Inner<T> {
149 data: Arc<T>,
150 bounds: Aabb,
151}
152
153struct Store<T> {
154 id: GeometryId,
155 rev: AtomicU64,
156 inner: Mutex<Inner<T>>,
157}
158
159/// An app-owned, versioned handle to one batch of geometry.
160///
161/// Cheap to clone (`Arc` bump). See the [module docs](self) for the
162/// upload/caching contract. Type aliases [`MeshHandle`], [`PointsHandle`],
163/// and [`LinesHandle`] name the concrete instantiations used by the marks.
164///
165/// # Create once, mutate with `set` — never rebuild per frame
166///
167/// The backend caches GPU buffers by [`Self::id`] and re-uploads only
168/// when [`Self::revision`] advances. A handle constructed inside
169/// `build()` gets a *fresh id every frame*: the whole buffer re-uploads
170/// each frame and the cache never warms. Store handles in app state
171/// (the cache-coherence pattern is just a struct holding the handles
172/// plus whatever inputs they were built from) and call
173/// [`Self::set`] when the data actually changes. The runtime logs a
174/// warning when it sees a scene's handles churn for many consecutive
175/// frames.
176#[derive(Clone)]
177pub struct GeometryHandle<T> {
178 store: Arc<Store<T>>,
179}
180
181impl<T: GeometryData> GeometryHandle<T> {
182 /// Create a handle, allocating a fresh [`GeometryId`] and computing
183 /// bounds. Revision starts at 1.
184 pub fn new(data: T) -> Self {
185 let bounds = data.compute_bounds();
186 Self {
187 store: Arc::new(Store {
188 id: next_geometry_id(),
189 rev: AtomicU64::new(1),
190 inner: Mutex::new(Inner {
191 data: Arc::new(data),
192 bounds,
193 }),
194 }),
195 }
196 }
197
198 /// Replace the geometry and advance the revision, recomputing bounds.
199 /// The backend re-uploads on its next draw because the revision moved.
200 ///
201 /// This is the baseline update path: it re-uploads the whole buffer.
202 /// Finer-grained `update_range` / `append` paths are designed to slot
203 /// in later (the handle is the stable surface) without breaking
204 /// callers of `set`.
205 pub fn set(&self, data: T) {
206 let bounds = data.compute_bounds();
207 {
208 let mut inner = self.store.inner.lock().unwrap();
209 inner.data = Arc::new(data);
210 inner.bounds = bounds;
211 }
212 self.store.rev.fetch_add(1, Ordering::Release);
213 }
214
215 /// Stable identity for backend buffer caches.
216 pub fn id(&self) -> GeometryId {
217 self.store.id
218 }
219
220 /// Monotonic revision; advances on every [`GeometryHandle::set`].
221 pub fn revision(&self) -> u64 {
222 self.store.rev.load(Ordering::Acquire)
223 }
224
225 /// Cached bounds of the current geometry.
226 pub fn bounds(&self) -> Aabb {
227 self.store.inner.lock().unwrap().bounds
228 }
229
230 /// Snapshot the current data and revision for upload. Returns an
231 /// `Arc` clone (no geometry copy) plus the revision it corresponds to,
232 /// so the backend can cache by `(id, revision)`.
233 pub fn snapshot(&self) -> (Arc<T>, u64) {
234 let inner = self.store.inner.lock().unwrap();
235 let rev = self.store.rev.load(Ordering::Acquire);
236 (Arc::clone(&inner.data), rev)
237 }
238}
239
240impl<T: GeometryData> std::fmt::Debug for GeometryHandle<T> {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 f.debug_struct("GeometryHandle")
243 .field("id", &self.id().0)
244 .field("revision", &self.revision())
245 .field("bounds", &self.bounds())
246 .finish()
247 }
248}
249
250/// Handle to triangle geometry (small mesh models, surfaces).
251pub type MeshHandle = GeometryHandle<MeshData>;
252/// Handle to point/marker geometry (scatter data).
253pub type PointsHandle = GeometryHandle<PointData>;
254/// Handle to line geometry (axes, wireframe, series, error bars).
255pub type LinesHandle = GeometryHandle<LineData>;
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn ids_are_unique() {
263 let a = PointsHandle::new(PointData::default());
264 let b = PointsHandle::new(PointData::default());
265 assert_ne!(a.id(), b.id());
266 }
267
268 #[test]
269 fn set_advances_revision_and_recomputes_bounds() {
270 let h = PointsHandle::new(PointData::default());
271 assert_eq!(h.revision(), 1);
272 assert!(!h.bounds().is_valid()); // empty
273
274 h.set(PointData {
275 points: vec![
276 ScenePoint {
277 position: Vec3::ZERO,
278 color: [1.0; 4],
279 },
280 ScenePoint {
281 position: Vec3::new(2.0, 4.0, 6.0),
282 color: [1.0; 4],
283 },
284 ],
285 });
286 assert_eq!(h.revision(), 2);
287 let bb = h.bounds();
288 assert_eq!(bb.min, Vec3::ZERO);
289 assert_eq!(bb.max, Vec3::new(2.0, 4.0, 6.0));
290 }
291
292 #[test]
293 fn snapshot_tracks_current_revision() {
294 let h = MeshHandle::new(MeshData::default());
295 let (_d0, r0) = h.snapshot();
296 assert_eq!(r0, 1);
297 h.set(MeshData {
298 vertices: vec![MeshVertex {
299 position: Vec3::ZERO,
300 normal: Vec3::Y,
301 }],
302 indices: None,
303 });
304 let (d1, r1) = h.snapshot();
305 assert_eq!(r1, 2);
306 assert_eq!(d1.vertices.len(), 1);
307 }
308
309 #[test]
310 fn clone_shares_store() {
311 let h = LinesHandle::new(LineData::default());
312 let c = h.clone();
313 assert_eq!(h.id(), c.id());
314 c.set(LineData {
315 segments: vec![LineSegment {
316 start: Vec3::ZERO,
317 end: Vec3::ONE,
318 color: [1.0; 4],
319 }],
320 });
321 // Mutation through the clone is visible through the original.
322 assert_eq!(h.revision(), 2);
323 assert!(h.bounds().is_valid());
324 }
325}