Model cache: a saved model is its kernel's

The cache key did not name the kernel, so a design saved on solidcore opened under SOLIDCRAFT_KERNEL=smlib showed solidcore's bodies, and the other way round. Case fillets/report-fillet-concave-arc-and-front-top-edge-together-r7-solver-residues: saved on solidcore and opened on C++ gave volume 9446.261 (C++ evaluates 9453.338); saved on C++ and opened on solidcore gave 9453.338 (solidcore evaluates 9446.261). The manifest now records the kernel and a cache of another kernel, or of none, is a miss: the design is evaluated on the kernel in force.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdcgEu4Lr3moCCisdZShKz
This commit is contained in:
brett19
2026-11-01 00:42:31 -07:00
parent 8f78846314
commit 4a98a1f8a4
4 changed files with 70 additions and 8 deletions
+40
View File
@@ -385,3 +385,43 @@ fn a_design_saved_before_names_were_pinned_opens_pinned_and_unchanged() {
}
let _ = std::fs::remove_file(&path);
}
/// A saved model is its kernel's: the two differ in tolerances and in what they refuse, so a design
/// opened on the other kernel is evaluated on it, in both directions; on the kernel that saved it the
/// model is taken as it is. A process each, since the kernel is chosen for the process.
#[cfg(feature = "smlib")]
#[test]
fn a_model_saved_on_one_kernel_is_evaluated_again_on_the_other() {
let cli = || {
let mut c = std::process::Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"));
c.env_remove("SOLIDCRAFT_NO_MODEL_CACHE");
c
};
// Under the build's own temporary folder: the files are the command line's, not this process's.
let temp = |name: &str| Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("model-cache-{}-{name}.solidcraft", std::process::id()));
let script = temp("kernels-script").with_extension("json");
let recipe = json!({"commands": [
{"command": "solid.box", "params": {"length": 20, "width": 20, "height": 10, "body_name": "Plate"}},
{"command": "solid.cylinder", "params": {"diameter": 8, "height": 10, "base": [10, 10, 10], "operation": "join", "targets": ["Plate"]}},
]});
std::fs::write(&script, recipe.to_string()).unwrap();
// Whether opening found a model in the file it may take, whether it took it, and how many
// features it evaluated.
let open = |design: &Path, kernel: &str| {
let out = cli().env("SOLIDCRAFT_KERNEL", kernel).arg("bench-open").arg(design).output().unwrap();
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
let lines: Vec<Value> = String::from_utf8_lossy(&out.stdout).lines().filter_map(|l| serde_json::from_str(l).ok()).collect();
let step = |name: &str| lines.iter().find(|l| l["step"] == name).cloned().unwrap();
let (parse, evaluated) = (step("parse"), step("evaluate"));
(parse["saved_model"].as_bool().unwrap(), evaluated["adopted"].as_bool().unwrap(), evaluated["recomputed"].as_u64().unwrap())
};
for (saved, other) in [("solidcore", "smlib"), ("smlib", "solidcore")] {
let design = temp(&format!("kernels-{saved}"));
let out = cli().env("SOLIDCRAFT_KERNEL", saved).arg("run").arg(&script).arg("--save").arg(&design).output().unwrap();
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
assert_eq!(open(&design, saved), (true, true, 0), "saved on {saved}, opened on it");
assert_eq!(open(&design, other), (false, false, 2), "saved on {saved}, opened on {other}");
std::fs::remove_file(&design).unwrap();
}
std::fs::remove_file(&script).unwrap();
}
+15 -2
View File
@@ -1209,7 +1209,7 @@ mod tests {
let tree: Value = serde_json::from_slice(manifest).unwrap();
assert_eq!(serde_json::to_vec(&tree).unwrap(), *manifest);
assert_eq!(tree["model"], model.records);
assert_eq!(tree.as_object().unwrap().len(), 11, "{:?}", tree.as_object().unwrap().keys().collect::<Vec<_>>());
assert_eq!(tree.as_object().unwrap().len(), 12, "{:?}", tree.as_object().unwrap().keys().collect::<Vec<_>>());
}
/// #9170: a mesh body (an imported STL) is kept in the model cache with the rest of the
@@ -1429,7 +1429,7 @@ mod tests {
let doc = document();
let bytes = write_design_container_with_cache(&doc, DesignCompression::Fast, None, Some(&native_geometry())).unwrap();
let original = parts(&bytes);
for key in ["format", "producer", "evaluation", "document", "payload"] {
for key in ["format", "producer", "evaluation", "kernel", "document", "payload"] {
let mut entries = original.clone();
let mut manifest: Value = serde_json::from_slice(&entries["cache/model.json"]).unwrap();
manifest[key] = json!("unsupported-or-stale");
@@ -1503,6 +1503,19 @@ mod tests {
manifest.as_object_mut().unwrap().remove("evaluation");
entries.insert("cache/model.json".into(), serde_json::to_vec(&manifest).unwrap());
assert!(read_design_geometry(&package(&entries)).is_none());
// The kernel that computed it is recorded, and another kernel's model is evaluated again:
// the same source gives different bodies on the two (a cache without one is a miss too).
use solidcraft_kernel::kernel_route::{Backend, choice};
assert_eq!(written(&original)["kernel"], json!(choice().id()));
let other = if choice() == Backend::Solidcore { Backend::Smlib } else { Backend::Solidcore };
let bytes = with(&[("kernel", other.id())]);
assert!(read_design_geometry(&bytes).is_none(), "{}'s cache", other.id());
assert_eq!(crate::read_design(&bytes).unwrap().to_json(), doc.to_json());
let mut entries = original.clone();
let mut manifest = written(&entries);
manifest.as_object_mut().unwrap().remove("kernel");
entries.insert("cache/model.json".into(), serde_json::to_vec(&manifest).unwrap());
assert!(read_design_geometry(&package(&entries)).is_none());
}
#[test]
+12 -3
View File
@@ -1,7 +1,7 @@
//! The model cache: a design's evaluated timeline (every feature's result and the state after
//! it, [`SavedModel`]), saved with it so opening need not evaluate it again. Bodies use
//! SolidCraft's exact native B-rep encoding (a mesh body, its own triangles); the key ties the
//! cache to the exact document, and to the source and platform that computed it
//! cache to the exact document, and to the source, platform and geometry kernel that computed it
//! (docs/design-container.md).
use super::*;
use solidcraft_doc::{ModelState, SavedModel};
@@ -29,6 +29,12 @@ fn evaluation() -> String {
format!("{}/{}-{}", env!("SOLIDCRAFT_EVALUATION_SOURCE"), std::env::consts::ARCH, std::env::consts::OS)
}
/// The geometry kernel that answers in this process: another kernel's cache is not its result (the
/// two differ in tolerances and in what they refuse), so a design opened under it evaluates again.
fn kernel() -> &'static str {
solidcraft_kernel::kernel_route::choice().id()
}
/// A 4 x 4 matrix as its 16 numbers, in storage order.
fn matrix(t: &[[f64; 4]; 4]) -> Vec<f64> {
t.iter().flatten().copied().collect()
@@ -296,6 +302,7 @@ impl Encoded {
document: digest(document),
evaluation: evaluation(),
format: FORMAT,
kernel: kernel(),
model: records,
payload: digest(&data),
producer: PRODUCER,
@@ -320,6 +327,7 @@ struct Manifest<'a> {
document: String,
evaluation: String,
format: &'a str,
kernel: &'a str,
model: &'a Value,
payload: String,
producer: &'a str,
@@ -336,14 +344,15 @@ pub fn read_design_geometry(bytes: &[u8]) -> Option<std::sync::Arc<ModelState>>
DesignContainerReader::new(Cursor::new(bytes)).ok()?.geometry()?.final_state().ok()
}
/// Whether `manifest` is a model cache of this build's evaluation source for `document`. The
/// application version and commit that wrote it are not asked: another version of the same
/// Whether `manifest` is a model cache of this build's evaluation source and kernel for `document`.
/// The application version and commit that wrote it are not asked: another version of the same
/// evaluation source computed the same model.
fn current(manifest: &Value, document: &[u8]) -> bool {
let is = |key: &str, value: &str| manifest.get(key).and_then(Value::as_str) == Some(value);
is("format", FORMAT)
&& is("producer", PRODUCER)
&& is("evaluation", &evaluation())
&& is("kernel", kernel())
&& is("document", &digest(document))
&& source_trusted(env!("SOLIDCRAFT_EVALUATION_SOURCE"), cfg!(any(test, feature = "trust-unknown-source")))
}
+3 -3
View File
@@ -164,12 +164,12 @@ same triangles, colour and paint; the picture shown while opening draws its tria
`cache/model.json` records `solidcraft-model-cache/8`, the producer revision
`solidcraft-truck-adapter/4`, the application version, the build (source commit,
CPU architecture and OS), the evaluation source (below, with the CPU architecture
and OS), the SHA-256 of the exact `document.json` (whose blob references pin every
and OS), the geometry kernel that computed it (`solidcore` or `smlib`), the SHA-256 of the exact `document.json` (whose blob references pin every
STEP, mesh and image payload) and of `cache/bodies.bin`, the slices, each kernel
body's slice and display share, and the model's records. A cache is read back only
when the format, the producer, the evaluation source, the platform and both
when the format, the producer, the evaluation source, the platform, the kernel and both
digests match: the same source compiled for the same platform computes the same
numbers. The application version and the commit are recorded, not compared.
numbers on the same kernel; a design opened on the other kernel is evaluated on it. The application version and the commit are recorded, not compared.
The evaluation source (`SOLIDCRAFT_EVALUATION_SOURCE`, `crates/io/build.rs`) is a
digest of the committed source a model is computed from: the `doc` crate and every