graphic design tool
git clone https://git.lucas.co/cce-designer.git
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