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