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

commitc7def7cde714b794a9c2b60a242ba97912a9eb56
parent50e2c6ba62
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-07 13:06
perf(replay): copy the cached simulation state once a frame

The solve's stored states are Arc<Detail>, handed out by reference count:
a replay copied each one out of its checkpoint, into the entry and into a
checkpoint that already held it. And Detail::merge_owned, which the scene
walk merges with, takes a detail's arrays instead of copying them when it
merges into an empty one (merge's result, to the last identity). The
graph's evaluation at 57k points: 2.7 ms a frame to 0.5.

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

 CLAUDE.md       |  13 ++++++
 src/detail.rs   |  38 ++++++++++++++++
 src/geometry.rs | 138 +++++++++++++++++++++++++++++++++-----------------------
 src/main.rs     |  36 +++++++++++++++
 4 files changed, 169 insertions(+), 56 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 072dc41..7c92599 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -3525,6 +3525,19 @@ of edges a thread into one buffer. The wire edges at 57k points: 4.9 ms
 a frame to 0.6. `a_copy_shares_the_topology_until_it_is_edited`,
 `the_wire_vertices_are_the_edges_in_order`.
 
+**A replayed frame copies the mesh once** (the same day). It copied the
+cached simulation state about seven times: out of its checkpoint (the
+state and what its last substep consumed), into the cache entry, into a
+checkpoint that already held the frame, and twice more as the scene walk
+merged it into an empty detail at the geometry node's level and at the
+root's. The solve's stored states are `Arc<Detail>` now (`SimSolve`,
+`Checkpoint`), handed out by reference count and copied once, for what
+the resolver returns, and `Detail::merge_owned` — what the scene walk
+merges with — takes a detail's arrays rather than copying them when it
+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 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/detail.rs b/src/detail.rs
index 8d3cba1..6a4dc2c 100644
--- a/src/detail.rs
+++ b/src/detail.rs
@@ -368,6 +368,12 @@ pub struct AttribStore {
 }
 
 impl AttribStore {
+    /// Whether the store holds nothing at all: no elements, attributes,
+    /// kinds or groups — what appending into is the same as being `other`.
+    fn is_bare(&self) -> bool {
+        self.len == 0 && self.attribs.is_empty() && self.kinds.is_empty() && self.groups.is_empty()
+    }
+
     pub fn with_len(len: usize) -> Self {
         Self { len, attribs: HashMap::new(), kinds: HashMap::new(), groups: HashMap::new() }
     }
@@ -1568,6 +1574,38 @@ impl Detail {
         }
     }
 
+    /// [`merge`](Self::merge) of a detail the caller is done with. Into an
+    /// EMPTY detail — which is how the scene is assembled, each level's
+    /// displayed node into a fresh one — it takes `other`'s arrays instead
+    /// of copying them: the same result to the last value, the identities
+    /// renumbered and the detail attributes left behind as `merge` leaves
+    /// them (`merge_owned_is_merge`). Until 2026-10-07 the scene walk copied
+    /// a 57k-point simulation's whole state twice a frame this way. Into
+    /// anything else it is `merge`.
+    pub fn merge_owned(&mut self, other: Detail) {
+        let empty = self.pos.is_empty()
+            && self.vert_point.is_empty()
+            && self.prim_start.len() <= 1
+            && self.points.is_bare()
+            && self.verts.is_bare()
+            && self.prims.is_bare();
+        if !empty {
+            self.merge(&other);
+            return;
+        }
+        let Detail { pos, vert_point, prim_start, points, verts, prims, topo, .. } = other;
+        let first = self.next_id;
+        self.ids = (0..pos.len() as PointId).map(|i| first + i).collect();
+        self.next_id = first + pos.len() as PointId;
+        self.pos = pos;
+        self.points = points;
+        self.vert_point = vert_point;
+        self.verts = verts;
+        self.prim_start = prim_start;
+        self.prims = prims;
+        self.topo = topo;
+    }
+
     // ---- convenience ----
 
     /// Point color, from `Cd` where it exists.
diff --git a/src/geometry.rs b/src/geometry.rs
index 689beb6..711f352 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -1102,7 +1102,12 @@ struct SimSolve {
     key: u64,
     /// The frame `state` is the solution FOR.
     frame: i32,
-    state: Detail,
+    /// Shared (`Arc`) with the checkpoints that hold the same frame and
+    /// with a resume taken from it: a replay hands states out of the cache
+    /// by reference count, where until 2026-10-07 it copied each one three
+    /// times over — out of its checkpoint, into the entry, and the frame
+    /// left behind into a checkpoint that already held it.
+    state: std::sync::Arc<Detail>,
     /// What the LAST substep that produced `state` consumed — the seed until
     /// a step has run — derivatives cleared and `dt` set, exactly as the
     /// chain saw it. What a visible child inside the simnet is evaluated
@@ -1112,7 +1117,7 @@ struct SimSolve {
     /// that is the chain's last mover draws where the output draws. Until
     /// 2026-09-28 it was the state at the START of the frame, which under
     /// substeps showed one substep of a frame that took several.
-    prev: Detail,
+    prev: std::sync::Arc<Detail>,
     /// Earlier frames of the same solve, kept so a backward scrub resumes
     /// from the nearest one behind it instead of the seed. See
     /// [`Checkpoint`].
@@ -1149,8 +1154,8 @@ struct SimSolve {
 #[derive(Clone)]
 struct Checkpoint {
     frame: i32,
-    state: Detail,
-    prev: Detail,
+    state: std::sync::Arc<Detail>,
+    prev: std::sync::Arc<Detail>,
 }
 
 // Every run of a chain on this thread, through whichever cache — so a
@@ -5968,7 +5973,7 @@ pub fn network_sphere_vertices_with_errors(
         if display_on {
             let mut visited = Vec::new();
             if let Some(geom) = generate_single_node_geometry_with_errors(root, start, &mut visited, ocl_error, sim) {
-                out.merge(&geom);
+                out.merge_owned(geom);
             }
         }
         // A subnet inside draws too, as its output: the Remesh subnet in a
@@ -5989,7 +5994,7 @@ pub fn network_sphere_vertices_with_errors(
                 for child in shown {
                     let mut visited = Vec::new();
                     if let Some(geom) = generate_single_node_geometry_with_errors(root, child, &mut visited, ocl_error, sim) {
-                        out.merge(&geom);
+                        out.merge_owned(geom);
                     }
                 }
                 sim.feedback.pop();
@@ -6020,7 +6025,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             }
             if parent_visible && node.geometry_visible {
                 if let Some(geom) = generate_single_node_geometry_with_errors(root, node, &mut Vec::new(), ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
             return;
@@ -6053,7 +6058,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_extrude_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("line") {
@@ -6097,7 +6102,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_transform_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("scatter") {
@@ -6106,7 +6111,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_scatter_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("group") {
@@ -6115,7 +6120,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_group_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("attribute") {
@@ -6124,7 +6129,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_attribute_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("relax") {
@@ -6133,7 +6138,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_relax_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("neighbour") {
@@ -6142,7 +6147,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_neighbour_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("time") {
@@ -6151,7 +6156,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_time_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("normal") {
@@ -6160,7 +6165,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_normal_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("bounds") {
@@ -6169,7 +6174,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_bounds_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("distance") {
@@ -6178,7 +6183,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_distance_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("connectivity") {
@@ -6187,7 +6192,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_connectivity_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("cull") {
@@ -6196,7 +6201,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_cull_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("copy") {
@@ -6205,7 +6210,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_copy_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("soft_transform") {
@@ -6214,7 +6219,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_soft_transform_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("transfer") {
@@ -6223,7 +6228,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_transfer_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("valence") {
@@ -6232,7 +6237,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_valence_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("deform") {
@@ -6241,7 +6246,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_deform_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("volume") {
@@ -6250,7 +6255,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_volume_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("boolean") {
@@ -6259,7 +6264,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_boolean_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("mold_shell") {
@@ -6268,7 +6273,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_mold_shell_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("hull") {
@@ -6277,7 +6282,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_hull_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("wrangle") {
@@ -6286,7 +6291,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_wrangle_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("switch") {
@@ -6295,7 +6300,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_switch_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("export") {
@@ -6304,7 +6309,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_export_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("subdivide") {
@@ -6313,7 +6318,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_subdivide_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("detangle") {
@@ -6322,7 +6327,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_detangle_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("suture") {
@@ -6331,7 +6336,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_suture_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("remesh") {
@@ -6340,7 +6345,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_remesh_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("develop") {
@@ -6349,7 +6354,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_develop_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("visualize") {
@@ -6358,7 +6363,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_visualize_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("analysis") {
@@ -6367,7 +6372,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_analysis_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("collision") {
@@ -6376,7 +6381,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = resolve_collision_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if node.node_type.eq_ignore_ascii_case("opencl") {
@@ -6385,7 +6390,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = retired_opencl_node(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if is_loop(node) {
@@ -6394,7 +6399,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = generate_single_node_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
             // The chain inside a simnet is the simulation STEP, and inside a
@@ -6412,7 +6417,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = generate_single_node_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
         } else if top
@@ -6429,7 +6434,7 @@ fn walk_level(root: &FsNode, start: &FsNode, ocl_error: &mut Option<String>, sim
             if is_visible {
                 let mut visited = Vec::new();
                 if let Some(geom) = generate_single_node_geometry_with_errors(root, node, &mut visited, ocl_error, sim) {
-                    out.merge(&geom);
+                    out.merge_owned(geom);
                 }
             }
             return;
@@ -7505,7 +7510,7 @@ pub fn resolve_simnet_geometry_with_errors(
     });
     let edited_at = prior.as_ref().and_then(|e| e.edited_at);
     let mut checkpoints = Checkpoints::default();
-    let mut cached: Option<(Detail, Detail, i32)> = None;
+    let mut cached: Option<(std::sync::Arc<Detail>, std::sync::Arc<Detail>, i32)> = None;
     if let Some(prior) = prior {
         checkpoints = prior.checkpoints;
         if prior.frame <= due {
@@ -7539,10 +7544,10 @@ pub fn resolve_simnet_geometry_with_errors(
     // seed for that frame — the interior view and the pull arrows drawn from
     // where the sim started, once per app launch with Cache on.
     let disk = if cached.is_none() && caching { read_sim_cache(&target.id, key, due) } else { None };
-    let (mut state, mut prev_frame, mut done) = match (cached, disk) {
+    let (held, held_prev, mut done) = match (cached, disk) {
         (Some(hit), _) => hit,
-        (None, Some(hit)) => hit,
-        (None, None) => (seed.clone(), seed.clone(), 0),
+        (None, Some((state, prev, frame))) => (std::sync::Arc::new(state), std::sync::Arc::new(prev), frame),
+        (None, None) => (std::sync::Arc::new(seed.clone()), std::sync::Arc::new(seed.clone()), 0),
     };
 
     // Substeps run the chain more than once per frame. A step's size is what
@@ -7562,6 +7567,16 @@ pub fn resolve_simnet_geometry_with_errors(
     let dt = 1.0 / substeps as f32;
 
     let resumed_at = done;
+    // A frame in hand needs no copy of its own; stepping on from it does,
+    // unless nothing else holds it.
+    let mut state = Detail::new();
+    let mut prev_frame = Detail::new();
+    let (held, held_prev) = if done < due {
+        state = std::sync::Arc::unwrap_or_clone(held);
+        (None, None)
+    } else {
+        (Some(held), Some(held_prev))
+    };
     while done < due {
         for _ in 0..substeps {
             // The step boundary, and the contract that makes a chain
@@ -7605,7 +7620,11 @@ pub fn resolve_simnet_geometry_with_errors(
         // frame asked for, which is the entry itself.
         if done % checkpoints.every == 0 && done < due {
             state.topology();
-            checkpoints.keep(Checkpoint { frame: done, state: state.clone(), prev: prev_frame.clone() });
+            checkpoints.keep(Checkpoint {
+                frame: done,
+                state: std::sync::Arc::new(state.clone()),
+                prev: std::sync::Arc::new(prev_frame.clone()),
+            });
         }
     }
 
@@ -7616,6 +7635,13 @@ pub fn resolve_simnet_geometry_with_errors(
     // simulation wrote its whole state to disk several times a frame,
     // even replaying frames solved long before — at ten thousand points
     // most of what the spreadsheet's refresh cost.
+    if done > resumed_at {
+        state.topology();
+    }
+    let (state, prev_frame) = match (held, held_prev) {
+        (Some(state), Some(prev)) => (state, prev),
+        _ => (std::sync::Arc::new(state), std::sync::Arc::new(prev_frame)),
+    };
     if caching && due > 0 && done > resumed_at {
         write_sim_cache(&target.id, key, due, &state, &prev_frame);
     }
@@ -7624,15 +7650,15 @@ pub fn resolve_simnet_geometry_with_errors(
     // Detail`): the wireframe's edges and anything else that asks of the
     // scene are free on a replay, where they were built again every frame.
     // It costs about 1% of a step and is counted in the checkpoints' budget.
-    if done > resumed_at {
-        state.topology();
-    }
+    // The one copy a frame costs: what is handed back, the entry keeping
+    // the shared state.
+    let out = Detail::clone(&state);
     sim.cache.entries.insert(
         target.id.clone(),
-        SimSolve { key, frame: due, state: state.clone(), prev: prev_frame, checkpoints, edited_at, start: start_frame, chain },
+        SimSolve { key, frame: due, state, prev: prev_frame, checkpoints, edited_at, start: start_frame, chain },
     );
     sim.cache.revision += 1;
-    Some(state)
+    Some(out)
 }
 
 /// The state the LAST substep of a simnet's current frame was stepped FROM:
@@ -7648,7 +7674,7 @@ pub fn simnet_step_feedback(
     sim: &mut EvalSim,
 ) -> Option<Detail> {
     resolve_simnet_geometry_with_errors(root, target, visited, ocl_error, sim)?;
-    sim.cache.entries.get(&target.id).map(|e| e.prev.clone())
+    sim.cache.entries.get(&target.id).map(|e| Detail::clone(&e.prev))
 }
 
 /// Where a simnet's solved state is parked between runs.
@@ -7783,7 +7809,7 @@ mod simnet_tests {
     fn checkpoints_are_budgeted_by_what_each_holds() {
         // Frame f's state has 10 f points: the history grows as a solve's does.
         let state = |f: i32| sphere_detail(Vec3::ZERO, 1.0, 2, 5 * f as usize);
-        let at = |f: i32| Checkpoint { frame: f, state: state(f), prev: Detail::default() };
+        let at = |f: i32| Checkpoint { frame: f, state: std::sync::Arc::new(state(f)), prev: std::sync::Arc::new(Detail::default()) };
         let all: usize = (1..=60).map(|f| checkpoint_bytes(&state(f))).sum();
         let latest = checkpoint_bytes(&state(60));
         assert!(60 * latest > all * 3 / 2, "the latest times the count overstates the history");
diff --git a/src/main.rs b/src/main.rs
index f6d4297..78fd264 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18079,6 +18079,42 @@ mod tests {
         assert_eq!(state.positions[SPREADSHEET_IDX].2, 1600.0 - 36.0, "a table wider than the window fills its room");
     }
 
+    /// `merge_owned` takes a detail's arrays where `merge` copies them,
+    /// and the result is `merge`'s: every point, identity (renumbered),
+    /// attribute, kind and group, the detail attributes left behind, and
+    /// the topology carried when built — into an empty detail, and into one
+    /// that is not, where it is `merge` itself.
+    #[test]
+    fn merge_owned_is_merge() {
+        use crate::detail::{AttribData, AttribKind, AttribValue, Detail};
+        let mut other = crate::geometry::sphere_detail(Vec3::new(0.3, 0.1, -0.2), 0.8, 6, 9);
+        let n = other.num_points();
+        other.points_mut().insert("mass", AttribData::Float((0..n).map(|p| p as f32 * 0.5).collect())).unwrap();
+        other.points_mut().create_kind("vel", AttribValue::Float3([0.0, 1.0, 0.0]), AttribKind::Derivative);
+        other.points_mut().create_group("tip");
+        other.points_mut().add_to_group("tip", 3);
+        other.prims_mut().create("pid", AttribValue::Int(7));
+        other.detail_mut().create("note", AttribValue::Float(9.0));
+        for built in [false, true] {
+            if built {
+                other.edges();
+            }
+            let mut copied = Detail::new();
+            copied.merge(&other);
+            let mut moved = Detail::new();
+            moved.merge_owned(other.clone());
+            assert!(moved == copied, "into an empty detail (topology built: {built})");
+            assert_eq!(moved.has_topology(), copied.has_topology());
+            assert_eq!((0..moved.num_points()).map(|p| moved.id(p)).collect::<Vec<_>>(), (0..copied.num_points()).map(|p| copied.id(p)).collect::<Vec<_>>());
+
+            let mut into_copy = crate::geometry::sphere_detail(Vec3::ZERO, 1.0, 3, 4);
+            let mut into_move = into_copy.clone();
+            into_copy.merge(&other);
+            into_move.merge_owned(other.clone());
+            assert!(into_move == into_copy, "into a detail that is not empty");
+        }
+    }
+
     /// 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