solidcraft-smlib: SMLib statically linked behind its C API, with a safe wrapper

A new crate (L1, only the kernel uses it) builds tangency-labs/smlib's SMLib,
SM_API and C API with the cc crate when its `native` feature is on, from the
commit in crates/smlib/SMLIB_REV (scripts/fetch-smlib.sh fetches just those
sources). The kernel's `smlib` feature turns it on. Without it nothing C++
is built and every call returns NotBuilt, so workspace builds need no C++
toolchain.

unsafe is confined to src/ffi.rs. The wrapper owns handles, maps statuses to
errors, checks the C API version, and sends SMLib's per-call log to the log
crate. Crash reports note the native kernel call in flight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brennen Herbruck
2026-10-20 23:14:39 -04:00
committed by jyviko
parent c06c3cb2cd
commit 8695af8acc
13 changed files with 594 additions and 1 deletions
Generated
+9
View File
@@ -4366,6 +4366,7 @@ dependencies = [
"serde",
"serde_json",
"solidcraft-geom",
"solidcraft-smlib",
"spade",
"thiserror 2.0.21",
"truck-meshalgo",
@@ -4410,6 +4411,14 @@ dependencies = [
"ttf-parser",
]
[[package]]
name = "solidcraft-smlib"
version = "0.18.1"
dependencies = [
"cc",
"log",
]
[[package]]
name = "solidcraft-space-mouse"
version = "0.24.0"
+1
View File
@@ -18,6 +18,7 @@ robust = "1.2"
solidcraft-geom = { path = "crates/geom" }
solidcraft-sketch = { path = "crates/sketch" }
solidcraft-kernel = { path = "crates/kernel" }
solidcraft-smlib = { path = "crates/smlib" }
solidcraft-render = { path = "crates/render" }
solidcraft-gpu = { path = "crates/gpu" }
solidcraft-doc = { path = "crates/doc" }
+3
View File
@@ -1011,6 +1011,9 @@ fn write_report(state: &State, kind: &str, p: Option<&Panic>, extra: Extra) -> O
fs::create_dir_all(&folder).ok()?;
let mut files = serde_json::Map::new();
let mut notes = extra.notes;
if let Some(call) = solidcraft_kernel::native_call_in_flight() {
notes.push(format!("native kernel call in flight: {call}"));
}
if let Some(d) = &extra.design {
if d.len() <= MAX_DESIGN_BYTES {
if solidcraft_io::write_atomic(&folder.join("design.solidcraft"), d, false).is_ok() {
+6
View File
@@ -8,8 +8,14 @@ license.workspace = true
[lints]
workspace = true
[features]
# Link NVIDIA's SMLib (tangency-labs/smlib) into the kernel; needs a C++ toolchain and
# scripts/fetch-smlib.sh (ADR 0002).
smlib = ["dep:solidcraft-smlib", "solidcraft-smlib/native"]
[dependencies]
solidcraft-geom = { workspace = true }
solidcraft-smlib = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
+12
View File
@@ -240,6 +240,18 @@ pub enum KernelError {
pub type Result<T> = std::result::Result<T, KernelError>;
/// The native kernel call running right now (SMLib, with the `smlib` feature), for crash reports.
pub fn native_call_in_flight() -> Option<String> {
#[cfg(feature = "smlib")]
{
solidcraft_smlib::current_call()
}
#[cfg(not(feature = "smlib"))]
{
None
}
}
thread_local! {
static GUARDED: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "solidcraft-smlib"
description = "SolidCraft: NVIDIA's SMLib B-rep kernel (tangency-labs/smlib, Apache-2.0), statically linked behind its C API, with a safe Rust wrapper"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
links = "smlib"
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/ffi.rs can allow it.
# Calling a C library has no safe API. Keep the rest in sync with [workspace.lints].
[lints.rust]
unsafe_code = "deny"
[lints.clippy]
dbg_macro = "warn"
todo = "warn"
too_many_arguments = "allow"
type_complexity = "allow"
neg_cmp_op_on_partial_ord = "allow"
[features]
# Compile and link SMLib. Off, the crate builds without a C++ toolchain or the SMLib sources, and
# every call returns `Error::NotBuilt`, so workspace-wide builds and checks need neither.
native = []
[dependencies]
log = { workspace = true }
[build-dependencies]
cc = { version = "1.2", features = ["parallel"] }
+1
View File
@@ -0,0 +1 @@
c21f4b0d7ad0da28c9d2ce2bc0b23ef3929704ef
+92
View File
@@ -0,0 +1,92 @@
//! Builds SMLib, SM_API and their C API (tangency-labs/smlib, at the commit in `SMLIB_REV`) into
//! static libraries, when the `native` feature is on. The sources come from
//! `SOLIDCRAFT_SMLIB_DIR`, or from `target/smlib-<rev>` as `scripts/fetch-smlib.sh` leaves them.
//!
//! The file lists, defines and include order follow the fork's `capi/CMakeLists.txt`: SMLib/inc
//! and SM_API/inc each have a StdAfx.h, so each part must find its own first.
use std::path::{Path, PathBuf};
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=SMLIB_REV");
println!("cargo:rerun-if-env-changed=SOLIDCRAFT_SMLIB_DIR");
if std::env::var_os("CARGO_FEATURE_NATIVE").is_none() {
return;
}
if let Err(e) = build() {
// A build script has no other way to fail the build.
eprintln!("error: {e}");
std::process::exit(1);
}
}
fn build() -> Result<(), String> {
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").map_err(|e| e.to_string())?);
let rev = std::fs::read_to_string(manifest.join("SMLIB_REV")).map_err(|e| format!("SMLIB_REV: {e}"))?.trim().to_string();
let root = match std::env::var_os("SOLIDCRAFT_SMLIB_DIR") {
Some(d) => PathBuf::from(d),
None => manifest.join("../../target").join(format!("smlib-{}", rev.get(..12).unwrap_or(&rev))),
};
if !root.join("capi/include/smlib.h").is_file() {
return Err(format!(
"the SMLib sources are not at {} (feature `native`): run scripts/fetch-smlib.sh, or set SOLIDCRAFT_SMLIB_DIR to a tangency-labs/smlib checkout at {rev}",
root.display()
));
}
println!("cargo:rerun-if-changed={}", root.join("capi").display());
let src = root.join("source");
let core = sources(&src.join("SMLib/src"), &["StdAfx.cpp", "SmDllMain.cpp"])?;
let api = sources(&src.join("SM_API/src"), &["StdAfx.cpp"])?;
let target = std::env::var("TARGET").unwrap_or_default();
let msvc = target.contains("msvc");
let base = || {
let mut b = cc::Build::new();
b.cpp(true).std("c++17").define("NDEBUG", None).define("NOMINMAX", None).define("SM_STATIC", None).define("SM_RAND_SEED", "20240101u");
// Rust links the MSVC runtime as a DLL; the cc crate's default (/MD) matches.
if msvc {
b.flag("/EHsc").flag("/bigobj");
}
b
};
let quiet = |b: &mut cc::Build| {
b.warnings(false);
if msvc {
b.flag("/w");
} else {
b.flag("-w");
}
};
// Link order: the C API needs SM_API, which needs SMLib.
let mut c = base();
c.file(root.join("capi/src/smlib_c.cpp")).include(root.join("capi/include")).include(src.join("SM_API/inc")).include(src.join("SMLib/inc"));
quiet(&mut c);
c.try_compile("smlib_capi").map_err(|e| e.to_string())?;
let mut a = base();
a.files(&api).include(src.join("SM_API/inc")).include(src.join("SMLib/inc"));
quiet(&mut a);
a.try_compile("smlib_api").map_err(|e| e.to_string())?;
let mut k = base();
k.files(&core).include(src.join("SMLib/inc"));
quiet(&mut k);
k.try_compile("smlib_core").map_err(|e| e.to_string())?;
Ok(())
}
/// The `.cpp` files in `dir`, sorted, without `skip`.
fn sources(dir: &Path, skip: &[&str]) -> Result<Vec<PathBuf>, String> {
let mut v: Vec<PathBuf> = std::fs::read_dir(dir)
.map_err(|e| format!("{}: {e}", dir.display()))?
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|x| x == "cpp") && !p.file_name().is_some_and(|n| skip.iter().any(|s| n == *s)))
.collect();
v.sort();
if v.is_empty() {
return Err(format!("no sources in {}", dir.display()));
}
Ok(v)
}
+177
View File
@@ -0,0 +1,177 @@
//! The C API of `capi/include/smlib.h` (tangency-labs/smlib), declared by hand. The only place in
//! the crate with `unsafe`: every function here is called through the safe wrappers in the crate
//! root, which check their arguments and own the handles.
#![allow(unsafe_code)]
use std::ffi::{c_char, c_void};
/// `SMLIB_ABI_VERSION` this crate was written against.
pub const ABI_VERSION: i32 = 1;
pub const OK: i32 = 0;
pub const ERR_EXCEPTION: i32 = -1000;
pub const ERR_BUFFER: i32 = -1001;
pub const ERR_ARG: i32 = -1002;
pub const ERR_EMPTY: i32 = -1003;
/// `smlib_brep`, opaque.
#[repr(C)]
pub struct RawBrep {
_private: [u8; 0],
}
pub type LogFn = extern "C" fn(user: *mut c_void, level: i32, line: *const c_char);
#[cfg(feature = "native")]
unsafe extern "C" {
pub fn smlib_abi_version() -> i32;
pub fn smlib_init() -> i32;
pub fn smlib_set_log(log: Option<LogFn>, user: *mut c_void);
pub fn smlib_set_dump_dir(dir: *const c_char) -> i32;
pub fn smlib_current_call(buf: *mut c_char, cap: i64) -> i64;
pub fn smlib_brep_read(path: *const c_char, ascii: i32, out: *mut *mut RawBrep) -> i32;
pub fn smlib_brep_write(brep: *const RawBrep, path: *const c_char, ascii: i32) -> i32;
pub fn smlib_brep_free(brep: *mut RawBrep);
pub fn smlib_brep_copy(brep: *const RawBrep, out: *mut *mut RawBrep) -> i32;
pub fn smlib_make_box(origin: *const f64, size: *const f64, out: *mut *mut RawBrep) -> i32;
pub fn smlib_brep_volume(brep: *const RawBrep, relative_accuracy: f64, out: *mut f64) -> i32;
pub fn smlib_brep_counts(brep: *const RawBrep, out: *mut i64) -> i32;
}
/// Returned by every call when the crate was built without the `native` feature.
pub const NOT_BUILT: i32 = -2000;
/// An owned `smlib_brep`. Only made here, from a pointer the C API returned; freed once on drop.
#[cfg_attr(not(feature = "native"), allow(dead_code))]
pub struct Handle(std::ptr::NonNull<RawBrep>);
// SMLib serialises every call behind its own lock, and a handle is only ever used through it.
unsafe impl Send for Handle {}
unsafe impl Sync for Handle {}
impl Drop for Handle {
fn drop(&mut self) {
#[cfg(feature = "native")]
unsafe {
smlib_brep_free(self.0.as_ptr())
};
}
}
impl Handle {
#[cfg_attr(not(feature = "native"), allow(dead_code))]
fn raw(&self) -> *const RawBrep {
self.0.as_ptr()
}
}
/// Runs a call that returns a new B-rep through `out`.
#[cfg(feature = "native")]
fn new_brep(f: impl FnOnce(*mut *mut RawBrep) -> i32) -> Result<Handle, i32> {
let mut out: *mut RawBrep = std::ptr::null_mut();
let s = f(&mut out);
match (s, std::ptr::NonNull::new(out)) {
(OK, Some(p)) => Ok(Handle(p)),
(OK, None) => Err(ERR_EMPTY),
(s, _) => Err(s),
}
}
#[cfg(feature = "native")]
mod calls {
use super::*;
use std::ffi::CStr;
pub fn abi_version() -> i32 {
unsafe { smlib_abi_version() }
}
pub fn init() -> i32 {
unsafe { smlib_init() }
}
pub fn set_log(log: Option<LogFn>) {
unsafe { smlib_set_log(log, std::ptr::null_mut()) }
}
pub fn set_dump_dir(dir: Option<&CStr>) -> i32 {
unsafe { smlib_set_dump_dir(dir.map_or(std::ptr::null(), CStr::as_ptr)) }
}
pub fn current_call(buf: &mut [u8]) -> usize {
let cap = i64::try_from(buf.len()).unwrap_or(i64::MAX);
let n = unsafe { smlib_current_call(buf.as_mut_ptr().cast(), cap) };
usize::try_from(n).unwrap_or(0).min(buf.len().saturating_sub(1))
}
pub fn read(path: &CStr, ascii: bool) -> Result<Handle, i32> {
new_brep(|out| unsafe { smlib_brep_read(path.as_ptr(), i32::from(ascii), out) })
}
pub fn write(b: &Handle, path: &CStr, ascii: bool) -> i32 {
unsafe { smlib_brep_write(b.raw(), path.as_ptr(), i32::from(ascii)) }
}
pub fn copy(b: &Handle) -> Result<Handle, i32> {
new_brep(|out| unsafe { smlib_brep_copy(b.raw(), out) })
}
pub fn make_box(origin: &[f64; 3], size: &[f64; 3]) -> Result<Handle, i32> {
new_brep(|out| unsafe { smlib_make_box(origin.as_ptr(), size.as_ptr(), out) })
}
pub fn volume(b: &Handle, relative_accuracy: f64) -> Result<f64, i32> {
let mut v = 0.0;
match unsafe { smlib_brep_volume(b.raw(), relative_accuracy, &mut v) } {
OK => Ok(v),
s => Err(s),
}
}
pub fn counts(b: &Handle) -> Result<[i64; 3], i32> {
let mut c = [0i64; 3];
match unsafe { smlib_brep_counts(b.raw(), c.as_mut_ptr()) } {
OK => Ok(c),
s => Err(s),
}
}
}
/// The same calls when SMLib is not built: each one fails with [`NOT_BUILT`].
#[cfg(not(feature = "native"))]
mod calls {
use super::*;
use std::ffi::CStr;
pub fn abi_version() -> i32 {
NOT_BUILT
}
pub fn init() -> i32 {
NOT_BUILT
}
pub fn set_log(_: Option<LogFn>) {}
pub fn set_dump_dir(_: Option<&CStr>) -> i32 {
NOT_BUILT
}
pub fn current_call(_: &mut [u8]) -> usize {
0
}
pub fn read(_: &CStr, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
pub fn write(_: &Handle, _: &CStr, _: bool) -> i32 {
NOT_BUILT
}
pub fn copy(_: &Handle) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
pub fn make_box(_: &[f64; 3], _: &[f64; 3]) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
pub fn volume(_: &Handle, _: f64) -> Result<f64, i32> {
Err(NOT_BUILT)
}
pub fn counts(_: &Handle) -> Result<[i64; 3], i32> {
Err(NOT_BUILT)
}
}
pub use calls::*;
/// A log line from the C API: NUL-terminated and valid for the callback's duration.
pub fn line(p: *const c_char) -> String {
if p.is_null() {
return String::new();
}
unsafe { std::ffi::CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
+169
View File
@@ -0,0 +1,169 @@
//! NVIDIA's SMLib B-rep kernel (tangency-labs/smlib, Apache-2.0), statically linked behind its C
//! API, with a safe Rust wrapper. Only `crates/kernel` uses this crate (ADR 0002).
//!
//! The C API never lets a C++ exception, type or pointer out, never changes its inputs, and
//! serialises every call behind one lock (SMLib keeps process-global state). This wrapper owns
//! the handles, turns status codes into [`Error`], and sends SMLib's one-line-per-call log to the
//! `log` crate (target `smlib`).
//!
//! Built without the `native` feature, the crate needs no C++ toolchain and every call returns
//! [`Error::NotBuilt`].
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
use std::ffi::{CString, c_char, c_void};
use std::path::Path;
use std::sync::Once;
use std::sync::atomic::{AtomicI32, Ordering};
mod ffi;
/// Why an SMLib call failed.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Error {
/// SMLib's own status (1001 `SM_ERR`, 1007 `SM_ERR_INVALID_INPUT`, …).
Status(i32),
/// An argument the C API rejected before SMLib saw it.
Argument,
/// SMLib reported success but returned no body.
Empty,
/// A C++ exception (most likely out of memory) was caught at the boundary.
Exception,
/// The C API is a different version from the one this crate was written against.
Version(i32),
/// This build has no SMLib (the `native` feature is off).
NotBuilt,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Status(1007) => write!(f, "SMLib rejected the input (SM_ERR_INVALID_INPUT)"),
Error::Status(s) => write!(f, "SMLib failed (status {s})"),
Error::Argument => write!(f, "invalid argument for SMLib"),
Error::Empty => write!(f, "SMLib returned an empty body"),
Error::Exception => write!(f, "SMLib raised an internal exception"),
Error::Version(v) => write!(f, "SMLib C API version {v}, expected {}", ffi::ABI_VERSION),
Error::NotBuilt => write!(f, "not supported yet: this build has no SMLib"),
}
}
}
impl std::error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
fn error(s: i32) -> Error {
match s {
ffi::ERR_ARG | ffi::ERR_BUFFER => Error::Argument,
ffi::ERR_EMPTY => Error::Empty,
ffi::ERR_EXCEPTION => Error::Exception,
ffi::NOT_BUILT => Error::NotBuilt,
s => Error::Status(s),
}
}
fn check(s: i32) -> Result<()> {
if s == ffi::OK { Ok(()) } else { Err(error(s)) }
}
fn c_path(p: &Path) -> Result<CString> {
CString::new(p.to_string_lossy().as_bytes()).map_err(|_| Error::Argument)
}
extern "C" fn log_line(_: *mut c_void, level: i32, line: *const c_char) {
let text = ffi::line(line);
if level == 0 {
log::debug!(target: "smlib", "{text}");
} else {
log::warn!(target: "smlib", "{text}");
}
}
static INIT: Once = Once::new();
/// What [`init`] found, kept for every later call (0: ready).
static INIT_RESULT: AtomicI32 = AtomicI32::new(0);
/// Checks the C API's version, creates SMLib's context and routes its log to the `log` crate.
/// Every other call runs it first; calling it at start-up keeps the cost out of the first operation.
pub fn init() -> Result<()> {
INIT.call_once(|| {
let v = ffi::abi_version();
let result = if v == ffi::NOT_BUILT {
v
} else if v != ffi::ABI_VERSION {
v.saturating_add(10_000)
} else {
ffi::set_log(Some(log_line));
ffi::init()
};
INIT_RESULT.store(result, Ordering::Release);
});
match INIT_RESULT.load(Ordering::Acquire) {
ffi::OK => Ok(()),
v if v >= 10_000 => Err(Error::Version(v - 10_000)),
s => Err(error(s)),
}
}
/// Where failing calls leave their inputs for replay (see the C API's `smlib_set_dump_dir`);
/// `None` turns that off.
pub fn set_dump_dir(dir: Option<&Path>) -> Result<()> {
init()?;
let c = dir.map(c_path).transpose()?;
check(ffi::set_dump_dir(c.as_deref()))
}
/// The SMLib call running right now, if any: for crash reports. Takes no lock.
pub fn current_call() -> Option<String> {
let mut buf = [0u8; 512];
let n = ffi::current_call(&mut buf);
let s = buf.get(..n).map(|b| String::from_utf8_lossy(b).into_owned()).unwrap_or_default();
(!s.is_empty()).then_some(s)
}
/// An SMLib B-rep. Immutable: operations return new bodies.
pub struct Brep(ffi::Handle);
impl std::fmt::Debug for Brep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("smlib::Brep")
}
}
impl Brep {
/// An axis-aligned box from its lower corner, with positive edge lengths (mm).
pub fn make_box(origin: [f64; 3], size: [f64; 3]) -> Result<Brep> {
init()?;
ffi::make_box(&origin, &size).map(Brep).map_err(error)
}
/// Reads a native SMLib `.smb` file.
pub fn read(path: &Path, ascii: bool) -> Result<Brep> {
init()?;
ffi::read(&c_path(path)?, ascii).map(Brep).map_err(error)
}
/// Writes this body to a native SMLib `.smb` file.
pub fn write(&self, path: &Path, ascii: bool) -> Result<()> {
check(ffi::write(&self.0, &c_path(path)?, ascii))
}
/// A deep copy.
pub fn try_clone(&self) -> Result<Brep> {
ffi::copy(&self.0).map(Brep).map_err(error)
}
/// The enclosed volume (mm³) at `relative_accuracy` (SMLib clamps it to [1e-8, 1e-1]).
pub fn volume(&self, relative_accuracy: f64) -> Result<f64> {
ffi::volume(&self.0, relative_accuracy).map_err(error)
}
/// Face, edge and vertex counts.
pub fn counts(&self) -> Result<[usize; 3]> {
let c = ffi::counts(&self.0).map_err(error)?;
Ok(c.map(|n| usize::try_from(n).unwrap_or(0)))
}
}
#[cfg(test)]
mod tests;
+70
View File
@@ -0,0 +1,70 @@
use super::*;
#[cfg(feature = "native")]
#[test]
fn box_end_to_end() {
let b = Brep::make_box([1.0, 2.0, 3.0], [10.0, 20.0, 30.0]).unwrap();
assert!((b.volume(1e-7).unwrap() - 6000.0).abs() < 1e-6);
assert_eq!(b.counts().unwrap(), [6, 12, 8]);
let c = b.try_clone().unwrap();
drop(b);
assert!((c.volume(1e-7).unwrap() - 6000.0).abs() < 1e-6);
assert_eq!(current_call(), None, "no call in flight between calls");
}
#[cfg(feature = "native")]
#[test]
fn bad_arguments_are_errors() {
assert_eq!(Brep::make_box([0.0; 3], [1.0, -1.0, 1.0]).unwrap_err(), Error::Argument);
assert_eq!(Brep::make_box([f64::NAN, 0.0, 0.0], [1.0; 3]).unwrap_err(), Error::Argument);
let b = Brep::make_box([0.0; 3], [1.0; 3]).unwrap();
assert_eq!(b.volume(f64::INFINITY).unwrap_err(), Error::Argument);
}
#[cfg(feature = "native")]
#[test]
fn native_files_round_trip_and_failures_dump_their_inputs() {
let dir = std::env::temp_dir().join(format!("solidcraft-smlib-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let b = Brep::make_box([0.0; 3], [2.0, 3.0, 4.0]).unwrap();
let path = dir.join("box.smb");
b.write(&path, false).unwrap();
let back = Brep::read(&path, false).unwrap();
assert!((back.volume(1e-7).unwrap() - 24.0).abs() < 1e-9);
let dumps = dir.join("dumps");
std::fs::create_dir_all(&dumps).unwrap();
set_dump_dir(Some(&dumps)).unwrap();
assert!(b.write(&dir.join("missing/x.smb"), false).is_err());
set_dump_dir(None).unwrap();
let names: Vec<String> =
std::fs::read_dir(&dumps).unwrap().filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().into_owned()).collect();
assert!(names.iter().any(|n| n.ends_with("-smlib_brep_write.txt")), "{names:?}");
let smb = names.iter().find(|n| n.ends_with("-smlib_brep_write-brep.smb")).unwrap();
let replayed = Brep::read(&dumps.join(smb), false).unwrap();
assert!((replayed.volume(1e-7).unwrap() - 24.0).abs() < 1e-9);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(feature = "native")]
#[test]
fn calls_from_many_threads_are_serialised() {
let handles: Vec<_> = (0..8)
.map(|i| {
std::thread::spawn(move || {
let s = 1.0 + i as f64;
let b = Brep::make_box([0.0; 3], [s; 3]).unwrap();
(b.volume(1e-7).unwrap() - s * s * s).abs() < 1e-9 * s * s * s
})
})
.collect();
assert!(handles.into_iter().all(|h| h.join().unwrap()));
}
#[cfg(not(feature = "native"))]
#[test]
fn without_native_every_call_says_so() {
assert_eq!(init(), Err(Error::NotBuilt));
assert_eq!(Brep::make_box([0.0; 3], [1.0; 3]).unwrap_err(), Error::NotBuilt);
assert_eq!(current_call(), None);
}
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Fetches the SMLib sources solidcraft-smlib builds (feature `native`): tangency-labs/smlib at
# the commit in crates/smlib/SMLIB_REV, only the parts the build reads (source/SMLib,
# source/SM_API, capi), into target/smlib-<rev12>. OpenCASCADE is not part of the fork.
set -euo pipefail
root="$(cd "$(dirname "$0")/.." && pwd)"
rev="$(tr -d '[:space:]' < "$root/crates/smlib/SMLIB_REV")"
dest="${SOLIDCRAFT_SMLIB_DIR:-$root/target/smlib-${rev:0:12}}"
if [ -f "$dest/capi/include/smlib.h" ] && [ "$(git -C "$dest" rev-parse HEAD 2>/dev/null)" = "$rev" ]; then
echo "SMLib $rev already at $dest"
exit 0
fi
rm -rf "$dest"
mkdir -p "$dest"
git -C "$dest" init -q
git -C "$dest" remote add origin https://github.com/tangency-labs/smlib.git
git -C "$dest" sparse-checkout set --no-cone /source/SMLib/ /source/SM_API/ /capi/ /LICENSE
git -C "$dest" fetch -q --depth 1 --filter=blob:none origin "$rev"
git -C "$dest" checkout -q FETCH_HEAD
echo "SMLib $rev at $dest"
+3 -1
View File
@@ -43,6 +43,8 @@ pub const TABLE: &[(&str, Class)] = &[
// the 3Dconnexion driver's client API on macOS (no workspace dependencies)
("3dx-client", Class::Standalone),
("sketch", Class::Layer(1)),
// NVIDIA's SMLib behind its C API (kernel-smlib branch, ADR 0002); only the kernel uses it
("smlib", Class::Layer(1)),
("kernel", Class::Layer(1)),
("render", Class::Layer(1)),
("doc", Class::Layer(2)),
@@ -62,7 +64,7 @@ pub const TABLE: &[(&str, Class)] = &[
/// Explicit orderings *within* a layer (earlier may be used by later).
/// The viewport draws display plans the GPU crate evaluates and draws with its pipelines; the MCP
/// server's tests draw screenshots with them.
pub const INTRA_LAYER_ORDER: &[&[&str]] = &[&["gpu", "ui-egui"], &["gpu", "mcp"]];
pub const INTRA_LAYER_ORDER: &[&[&str]] = &[&["gpu", "ui-egui"], &["gpu", "mcp"], &["smlib", "kernel"]];
fn intra_layer_allowed(from: &str, to: &str) -> bool {
let (from, to) = (short_name(from), short_name(to));