mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
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:
@@ -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
@@ -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]
|
||||
|
||||
@@ -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")))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user