git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commitf6e2a49dfe1a7d39ebf33cd3970200f22a4da91e
parent42f6b94fd0
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-07 14:31
perf(viewport): build the fill and the vector markers on every core

detail_vertices writes its triangles a stretch of primitives a thread into
one buffer (triangulate's vertices, bit for bit); vis_marker_vertices
builds a piece of points a thread, joined in order; marker_instances
checks coincident points with a plain hasher. present_scene at 57k
points: 6.5 ms a frame to 4.7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md       |  14 ++++++
 src/detangle.rs |   2 +-
 src/geometry.rs | 132 ++++++++++++++++++++++++++++++++++++++++++++++++--------
 src/main.rs     |  41 ++++++++++++++++++
 4 files changed, 169 insertions(+), 20 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 7c92599..baf9599 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -3538,6 +3538,20 @@ merges into an empty one, giving `merge`'s result to the last identity
 (`merge_owned_is_merge`). The graph's evaluation at 57k points: 2.7 ms a
 frame to 0.5.
 
+**The viewport's meshes, rebuilt every frame of a replay** (the same day):
+`geometry::detail_vertices` — the fill — writes its triangles a stretch of
+primitives a thread into one buffer, each primitive's place worked out by
+a first pass (`triangulate`'s vertices, bit for bit:
+`detail_vertices_are_the_triangulation`); `vis_marker_vertices` builds a
+piece of points a thread, joined in order; and `marker_instances` checks
+for coincident points with a plain multiplicative hasher
+(`geometry::QuickHash`) in place of the default one. At 57k points
+`present_scene` went from 6.5 ms a frame to 4.7: the fill 2.2 to 0.9, the
+point markers' instances 1.4 to 1.1, the wire 1.0 to 0.8; the vector
+markers (1.0) and the visualizers' copy and apply (1.0) are what is left,
+with the point markers' duplicate check (0.8 of their 1.1). A fresh 8 MB
+buffer a frame costs 0.2 ms, so reusing buffers was not worth it.
+
 **The markers are instanced** (the same day, cce-ui's
 `SceneDraw::instances`): every kind — Show Point Markers, Show Vertex
 Markers, the selected group's, the marked groups' and the spreadsheet
diff --git a/src/detangle.rs b/src/detangle.rs
index 9119354..47d9756 100644
--- a/src/detangle.rs
+++ b/src/detangle.rs
@@ -952,7 +952,7 @@ fn solve_surface(geom: &mut Detail, before: Option<&[Vec3]>, target: &FsNode, to
 ///
 /// Threads and not a pool, since the crate has none: a thread costs tens
 /// of microseconds to start, which is what `least` is for.
-fn in_pieces<R: Send>(count: usize, least: usize, run: impl Fn(std::ops::Range<usize>) -> R + Sync) -> Vec<R> {
+pub(crate) fn in_pieces<R: Send>(count: usize, least: usize, run: impl Fn(std::ops::Range<usize>) -> R + Sync) -> Vec<R> {
     let threads = std::thread::available_parallelism().map_or(1, |n| n.get()).min(count / least.max(1)).max(1);
     if threads == 1 {
         return vec![run(0..count)];
diff --git a/src/geometry.rs b/src/geometry.rs
index 711f352..20f7870 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -64,8 +64,70 @@ pub fn rt_scene_from_verts(
 
 /// A [`Detail`]'s triangles as renderer vertices — the one place the 3D scene
 /// crosses out of the geometry model.
+///
+/// What `Detail::triangulate` makes, vertex for vertex, written a stretch
+/// of primitives a thread (`detail_vertices_are_the_triangulation`): a
+/// first pass places each primitive's triangles in the buffer, and the
+/// threads fill their own stretches of it. The fill is rebuilt on every
+/// frame of a playing simulation, and at 57k points it was the largest
+/// part of what the viewport's meshes cost (until 2026-10-07).
 pub fn detail_vertices(d: &Detail) -> Vec<Vertex3D> {
-    d.triangulate(|position, color| Vertex3D { position, color })
+    let colors = d.point_colors();
+    let positions = d.positions();
+    let n = d.num_prims();
+    let mut start = Vec::with_capacity(n + 1);
+    let mut total = 0usize;
+    start.push(0);
+    for prim in 0..n {
+        let k = d.prim_points(prim).len();
+        if k >= 3 {
+            total += (k - 2) * 3;
+        }
+        start.push(total);
+    }
+    let corner = |p: u32| {
+        let p = p as usize;
+        Vertex3D {
+            position: positions.get(p).copied().unwrap_or([0.0; 3]),
+            color: colors.get(p).copied().unwrap_or(crate::detail::DEFAULT_COLOR),
+        }
+    };
+    let fill = |out: &mut [Vertex3D], prims: std::ops::Range<usize>| {
+        let mut i = 0;
+        for prim in prims {
+            let pts = d.prim_points(prim);
+            if pts.len() < 3 {
+                continue;
+            }
+            for k in 1..pts.len() - 1 {
+                for &p in &[pts[0], pts[k], pts[k + 1]] {
+                    out[i] = corner(p);
+                    i += 1;
+                }
+            }
+        }
+    };
+    // A thread costs tens of microseconds to start: a piece is at least
+    // this many primitives, so a small mesh stays on one.
+    const PIECE: usize = 8192;
+    let mut out = vec![Vertex3D { position: [0.0; 3], color: [0.0; 3] }; total];
+    let threads = std::thread::available_parallelism().map_or(1, |t| t.get()).min(n / PIECE).max(1);
+    if threads == 1 {
+        fill(&mut out, 0..n);
+        return out;
+    }
+    let per = n.div_ceil(threads);
+    std::thread::scope(|scope| {
+        let mut rest: &mut [Vertex3D] = &mut out;
+        for t in 0..threads {
+            let (a, b) = ((t * per).min(n), ((t + 1) * per).min(n));
+            let (mine, tail) = rest.split_at_mut(start[b] - start[a]);
+            rest = tail;
+            let fill = &fill;
+            scope.spawn(move || fill(mine, a..b));
+        }
+    });
+    out
 }
 
 /// The fill's triangles reordered FARTHEST FIRST from `eye` (mesh space),
@@ -3998,30 +4060,36 @@ pub fn ramp_color(name: &str, t: f32) -> [f32; 3] {
 /// overlay walk keeps a marker a property of the geometry that reached the
 /// viewport rather than of a node's display prefs, and costs one pass over
 /// geometry already in hand.
-pub fn vis_marker_vertices(geom: &Detail, linearize: impl Fn([f32; 3]) -> [f32; 3]) -> Vec<Vertex3D> {
+pub fn vis_marker_vertices(geom: &Detail, linearize: impl Fn([f32; 3]) -> [f32; 3] + Sync) -> Vec<Vertex3D> {
     let mut out = Vec::new();
+    let colors = geom.point_colors();
+    let positions = geom.positions();
     for name in geom.points().names() {
         if !name.starts_with(crate::detail::VIS_PREFIX) {
             continue;
         }
         let Some(data) = geom.points().get(name) else { continue };
-        let cd = geom.points().get(crate::detail::CD);
-        for p in 0..geom.num_points() {
-            let Some(v) = data.get(p) else { continue };
-            let dir = v.as_vec3();
-            if dir.length_squared() < 1e-12 {
-                continue;
+        // A piece of points a thread, the pieces joined in order: the
+        // colour conversion a point is most of the cost, and every point's
+        // is its own (since 2026-10-07).
+        let pieces = crate::detangle::in_pieces(geom.num_points(), 8192, |range| {
+            let mut piece = Vec::new();
+            for p in range {
+                let Some(v) = data.get(p) else { continue };
+                let dir = v.as_vec3();
+                if dir.length_squared() < 1e-12 {
+                    continue;
+                }
+                // Drawn in the point's own colour, so a Ramp Visualize upstream
+                // colours the markers too and one chain says two things at once.
+                let color = linearize(colors.get(p).copied().unwrap_or(crate::detail::DEFAULT_COLOR));
+                piece.push(Vertex3D { position: positions[p], color });
+                piece.push(Vertex3D { position: (Vec3::from_array(positions[p]) + dir).to_array(), color });
             }
-            // Drawn in the point's own colour, so a Ramp Visualize upstream
-            // colours the markers too and one chain says two things at once.
-            // Read as `Detail::color` does, off the column found once.
-            let color = linearize(match cd.and_then(|c| c.get(p)) {
-                Some(AttribValue::Float3(c)) => c,
-                Some(other) => other.as_vec3().to_array(),
-                None => crate::detail::DEFAULT_COLOR,
-            });
-            out.push(Vertex3D { position: geom.positions()[p], color });
-            out.push(Vertex3D { position: (geom.pos(p) + dir).to_array(), color });
+            piece
+        });
+        for piece in pieces {
+            out.extend(piece);
         }
     }
     out
@@ -6744,7 +6812,11 @@ pub fn marker_sphere(size: f32) -> Vec<Vertex3D> {
 /// uploaded on every frame of a playing simulation; instanced, they are
 /// ten thousand of these.
 pub fn marker_instances(src: &[Vertex3D], color: [f32; 3]) -> Vec<Vertex3D> {
-    let mut seen = std::collections::HashSet::with_capacity(src.len());
+    // Membership is all the set is asked; the default hasher's protection
+    // against chosen keys is wasted on quantized positions and was most of
+    // what building the instances cost (until 2026-10-07).
+    let mut seen: std::collections::HashSet<(i32, i32, i32), std::hash::BuildHasherDefault<QuickHash>> =
+        std::collections::HashSet::with_capacity_and_hasher(src.len(), Default::default());
     let mut out = Vec::with_capacity(src.len());
     for v in src {
         let key = (
@@ -6759,6 +6831,28 @@ pub fn marker_instances(src: &[Vertex3D], color: [f32; 3]) -> Vec<Vertex3D> {
     out
 }
 
+/// A plain multiplicative hasher for small integer keys, where the set is
+/// the program's own and nothing chooses its keys.
+#[derive(Default)]
+pub(crate) struct QuickHash(u64);
+
+impl std::hash::Hasher for QuickHash {
+    fn finish(&self) -> u64 {
+        self.0
+    }
+    fn write(&mut self, bytes: &[u8]) {
+        for &b in bytes {
+            self.write_u64(b as u64);
+        }
+    }
+    fn write_i32(&mut self, i: i32) {
+        self.write_u64(i as u32 as u64);
+    }
+    fn write_u64(&mut self, i: u64) {
+        self.0 = (self.0.rotate_left(5) ^ i).wrapping_mul(0x51_7c_c1_b7_27_22_0a_95);
+    }
+}
+
 /// What a renderer draws for `mesh` instanced over `instances` (cce-ui's
 /// `SceneDraw::instances`): the mesh once per instance, each instance's
 /// position added to every vertex and its colour multiplying theirs — the
diff --git a/src/main.rs b/src/main.rs
index 78fd264..52ef29a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18115,6 +18115,47 @@ mod tests {
         }
     }
 
+    /// The fill's vertices are `triangulate`'s, vertex for vertex, written
+    /// a stretch of primitives a thread on a mesh large enough to share
+    /// out and on one thread below that — with polygons, segments and a
+    /// primitive naming a point past the end among them; and the vector
+    /// markers are what one thread writes, in order.
+    #[test]
+    fn detail_vertices_are_the_triangulation() {
+        use crate::geometry::{detail_vertices, Vertex3D};
+        let bits = |v: &[Vertex3D]| v.iter().map(|v| (v.position.map(f32::to_bits), v.color.map(f32::to_bits))).collect::<Vec<_>>();
+        let mut meshes = vec![crate::geometry::sphere_detail(Vec3::ZERO, 1.0, 5, 7), crate::geometry::sphere_detail(Vec3::new(0.1, 0.2, 0.3), 2.0, 140, 280)];
+        let mut odd = crate::geometry::sphere_detail(Vec3::ZERO, 1.0, 4, 6);
+        odd.add_prim(&[0, 1]);
+        odd.add_prim(&[2, 3, 4, 5, 6]);
+        odd.add_prim(&[1, 2, 999]);
+        meshes.push(odd);
+        for d in &mut meshes {
+            let n = d.num_points();
+            d.points_mut().insert(crate::detail::CD, crate::detail::AttribData::Float3((0..n).map(|p| [p as f32 / n as f32, 0.25, 0.5]).collect())).unwrap();
+            let want = d.triangulate(|position, color| Vertex3D { position, color });
+            assert_eq!(bits(&detail_vertices(d)), bits(&want), "{} prims", d.num_prims());
+
+            d.points_mut().create(&format!("{}dir", crate::detail::VIS_PREFIX), crate::detail::AttribValue::Float3([0.0; 3]));
+            let name = format!("{}dir", crate::detail::VIS_PREFIX);
+            for p in (0..n).step_by(3) {
+                d.points_mut().set_value(&name, p, crate::detail::AttribValue::Float3([0.1, p as f32 * 0.001, -0.2])).unwrap();
+            }
+            let lin = cce_ui::colors::to_linear_rgb;
+            let mut want = Vec::new();
+            for p in 0..n {
+                let dir = d.points().value(&name, p).unwrap().as_vec3();
+                if dir.length_squared() < 1e-12 {
+                    continue;
+                }
+                let color = lin(d.color(p));
+                want.push(Vertex3D { position: d.positions()[p], color });
+                want.push(Vertex3D { position: (d.pos(p) + dir).to_array(), color });
+            }
+            assert_eq!(bits(&crate::geometry::vis_marker_vertices(d, lin)), bits(&want));
+        }
+    }
+
     /// The playbar's cache strip, as a rule: a frame is cached when every
     /// simnet in the tree holds it, stale when one of them holds it from
     /// the chain as it was — before an edit the solve went on across, or