From 66c7fa82fcf48baa8b8979db062a410035db8e01 Mon Sep 17 00:00:00 2001 From: Alex Lytvynenko Date: Fri, 23 Oct 2026 15:31:04 +0200 Subject: [PATCH 1/2] claim: geom.grid Co-Authored-By: GPT-6.1 Sol Fast From 2efc98e065f92d852dd0cb7e0449a583cb1d19ae Mon Sep 17 00:00:00 2001 From: Alex Lytvynenko Date: Fri, 23 Oct 2026 15:50:22 +0200 Subject: [PATCH 2/2] harness: compare explicit native surface caches in geom.grid Add six affine surface/cache cases and unit-local native grid observations while preserving the original 615 inputs and both opaque deferrals. Add canonical-CI identity, node-validation and mutation-sensitivity controls. No production paths or tolerances change. Co-Authored-By: GPT-6.1 Sol Fast --- crates/difftest/src/units/geom_grid.rs | 114 ++++- crates/difftest/src/units/geom_grid/native.rs | 394 ++++++++++++++++++ crates/difftest/src/units/geom_grid/tests.rs | 185 ++++++++ crates/solidcore-ffi/src/units/geom_grid.rs | 86 +++- fixtures/geom.grid/native_cache.json | 101 +++++ harness/cpp/shim/geom_grid.cpp | 84 +++- harness/cpp/shim/geom_grid_native.inc | 192 +++++++++ 7 files changed, 1148 insertions(+), 8 deletions(-) create mode 100644 crates/difftest/src/units/geom_grid/native.rs create mode 100644 crates/difftest/src/units/geom_grid/tests.rs create mode 100644 fixtures/geom.grid/native_cache.json create mode 100644 harness/cpp/shim/geom_grid_native.inc diff --git a/crates/difftest/src/units/geom_grid.rs b/crates/difftest/src/units/geom_grid.rs index d8ea2570b..9214e99ac 100644 --- a/crates/difftest/src/units/geom_grid.rs +++ b/crates/difftest/src/units/geom_grid.rs @@ -18,13 +18,26 @@ //! surface hierarchy element and `add_surface` need surface subdivision caches: //! the shim does not run them and the Rust side is deferred (they appear only in //! fixtures). Every run has at least [`MIN_GENERATED`] generated cases. +//! +//! New `define_surface` / `add_cached_hier` / `expand_cached` / +//! `add_cached_leaf` / `observe_cached` fixtures opt into real, explicitly defined +//! surface caches. They do not resolve the legacy opaque object/node labels. +//! The observer records cache leaf order, surface identities, pool order, voxel +//! memberships and all surface-leaf fields, in addition to the original dumps. + +#[path = "geom_grid/native.rs"] +mod native; + +#[cfg(test)] +#[path = "geom_grid/tests.rs"] +mod native_tests; use serde::{Deserialize, Serialize}; use serde_json::Value; use solidcore::geom::extent3d::Extent3d; use solidcore::geom::grid::{ - ElementId, ElementSource, Grid, GridItem, HierarchyElement, HierarchyId, + ElementId, ElementSource, Grid, GridItem, HierarchyElement, HierarchyId, SurfaceGridElement, }; use solidcore::geom::tree::NodeId; use solidcore::math::{Point3, Vec3, BIG_DOUBLE}; @@ -94,6 +107,28 @@ enum Op { AddSurface { bbox: E6, }, + DefineSurface { + id: i64, + origin: P3, + u: P3, + v: P3, + domain: [f64; 4], + spans: [usize; 2], + }, + AddCachedHier { + surface: i64, + tol: f64, + bbox: E6, + }, + ExpandCached { + h: usize, + }, + AddCachedLeaf { + surface: i64, + leaf: usize, + bbox: E6, + }, + ObserveCached, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -177,6 +212,35 @@ fn prog(i: &Inputs) -> Vec { v.push(10.0); v.extend(bbox); } + Op::DefineSurface { + id, + origin, + u, + v: axis_v, + domain, + spans, + } => { + v.extend([11.0, *id as f64]); + v.extend(origin); + v.extend(u); + v.extend(axis_v); + v.extend(domain); + v.extend(spans.map(|n| n as f64)); + } + Op::AddCachedHier { surface, tol, bbox } => { + v.extend([12.0, *surface as f64, *tol]); + v.extend(bbox); + } + Op::ExpandCached { h } => v.extend([13.0, *h as f64]), + Op::AddCachedLeaf { + surface, + leaf, + bbox, + } => { + v.extend([14.0, *surface as f64, *leaf as f64]); + v.extend(bbox); + } + Op::ObserveCached => v.push(15.0), } } v @@ -231,8 +295,26 @@ struct Stats { } fn run_rust(i: &Inputs, st: &mut Stats) -> solidcore::Result<(Vec, Vec)> { + run_rust_controlled(i, st, true, |leaf| leaf) +} + +// Unit-local mutation controls are used only by the native integration tests. +// Normal runs always expand and preserve every field. No kernel path is changed. +fn run_rust_controlled( + i: &Inputs, + st: &mut Stats, + expand: bool, + mut leaf_control: impl FnMut(SurfaceGridElement) -> SurfaceGridElement, +) -> solidcore::Result<(Vec, Vec)> { + if i.ops + .iter() + .any(|op| matches!(op, Op::DefineSurface { .. })) + { + ffi::validate(&prog(i)).map_err(|_| native::invalid("invalid explicit cache program"))?; + } let size = i.grid.size.map(|s| s as usize); let mut g: Grid = Grid::new(&ext(i.grid.bbox), size)?; + let mut caches = native::Caches::default(); let (mut oi, mut of) = (Vec::new(), Vec::new()); let mut ready = false; for op in &i.ops { @@ -332,6 +414,36 @@ fn run_rust(i: &Inputs, st: &mut Stats) -> solidcore::Result<(Vec, Vec g.add_element(ElementSource::Surface(NodeId(0)), &ext(*bbox))?; oi.push(0); } + Op::DefineSurface { + id, + origin, + u, + v, + domain, + spans, + } => { + caches.define(*id, *origin, *u, *v, *domain, *spans)?; + oi.push(0); + } + Op::AddCachedHier { surface, tol, bbox } => { + caches.add_hierarchy(&mut g, *surface, *tol, &ext(*bbox))?; + oi.push(0); + } + Op::ExpandCached { h } => { + if expand { + caches.expand(&mut g, HierarchyId(*h), &mut leaf_control)?; + } + oi.push(0); + } + Op::AddCachedLeaf { + surface, + leaf, + bbox, + } => { + caches.add_leaf(&mut g, *surface, *leaf, &ext(*bbox), &mut leaf_control)?; + oi.push(0); + } + Op::ObserveCached => caches.observe(&g, &mut oi, &mut of)?, } } Ok((oi, of)) diff --git a/crates/difftest/src/units/geom_grid/native.rs b/crates/difftest/src/units/geom_grid/native.rs new file mode 100644 index 000000000..e8780bc0e --- /dev/null +++ b/crates/difftest/src/units/geom_grid/native.rs @@ -0,0 +1,394 @@ +//! Opt-in real cache inputs/observations for `geom.grid` only. +//! +//! Geometry is the explicit affine map `origin + u * U + v * V`, represented by +//! a clamped, non-rational degree-1 tensor-product B-spline with the given domain +//! and uniform span counts. Leaf ordinals refer only to eligible nodes of that +//! surface's actual cache, in `GetAllTreeNodes` order; they are not arena ids. + +use solidcore::geom::polar_box::PolarBoxState; +use solidcore::geom::tree::{AuxDataType, NodeClass, NodeData}; +use solidcore::intersect::ray_tracer::surface_leaf; +use solidcore::kernel::Kernel; +use solidcore::nurbs::b_spline_surface::BSplineSurface; +use solidcore::nurbs::math::{CPoint, NOW}; +use solidcore::nurbs::srf_basic::Surface as NurbSurface; +use solidcore::surface::surface::p2::surface_cache; +use solidcore::surface::surface::Surface; +use solidcore::surface::surface_cache::p1::SurfaceCache; + +use super::{ + p3, put3, put_elem, put_ext, ElementId, Extent3d, Grid, GridItem, HierarchyElement, + HierarchyId, NodeId, SurfaceGridElement, Vec3, P3, +}; +use solidcore::SolidError; + +pub(super) fn invalid(reason: &'static str) -> SolidError { + SolidError::invalid_input("difftest::geom_grid::native", reason) +} + +struct CachedSurface { + id: i64, + surface: Surface, + cache: SurfaceCache, + leaves: Vec, +} + +/// Definitions remain in input order; no global cache or pointer-derived identity. +#[derive(Default)] +pub(super) struct Caches { + kernel: Kernel, + surfaces: Vec, + hierarchies: Vec<(HierarchyId, i64)>, +} + +fn leaf_nodes(cache: &SurfaceCache) -> solidcore::Result> { + let tree = cache.tree().ok_or_else(|| invalid("cache has no tree"))?; + let mut out = Vec::new(); + for id in tree.all_tree_nodes() { + let n = tree.node(id)?; + if n.aux_type != AuxDataType::BezierSurface + || n.child1 + .map(|c| { + tree.node(c) + .map(|n| n.aux_type == AuxDataType::BezierSurface) + }) + .transpose()? + .unwrap_or(false) + { + continue; + } + let NodeData::Surface(aux) = n.data else { + return Err(invalid("Bezier node has no cache data")); + }; + if aux.node_class != NodeClass::Outside { + cache.patch(id)?; + out.push(id); + } + } + if out.is_empty() || out.len() > 256 { + return Err(invalid("unexpected affine cache leaf count")); + } + Ok(out) +} + +fn init_leaf( + kernel: &Kernel, + s: &mut CachedSurface, + node: NodeId, +) -> solidcore::Result> { + let patch = s.cache.patch(node)?; + let domain = patch.aux.uv_domain; + let pseudo = *patch.pseudo_box(); + let polar = s.cache.polar_box(kernel, node)?; + let bbox = s + .cache + .tree() + .ok_or_else(|| invalid("cache has no tree"))? + .node(node)? + .bbox; + // Reuse the existing Init evaluator, without changing any ray-tracer path. + let leaf = surface_leaf( + kernel, + s.id as usize, + &s.surface, + &domain, + &bbox, + &pseudo, + &polar, + )?; + Ok(SurfaceGridElement { + surface: s.id, + uv_domain: leaf.uv_domain, + bbox: leaf.bbox, + pseudo_box: leaf.pseudo_box, + polar_box: leaf.polar_box, + mid_point: leaf.mid_point, + mid_du: leaf.mid_du, + mid_dv: leaf.mid_dv, + mid_normal: leaf.mid_normal, + }) +} + +impl Caches { + pub(super) fn define( + &mut self, + id: i64, + origin: P3, + u: P3, + v: P3, + domain: [f64; 4], + spans: [usize; 2], + ) -> solidcore::Result<()> { + let [u0, v0, u1, v1] = domain; + let [nu, nv] = spans; + // The FFI guard has already bounded counts, parameters and conditioning. + let params = |lo: f64, hi: f64, n: usize| -> Vec { + (0..=n) + .map(|i| lo + (hi - lo) * i as f64 / n as f64) + .collect() + }; + let us = params(u0, u1, nu); + let vs = params(v0, v1, nv); + let knots = |p: &[f64]| { + std::iter::once(p.first().copied().unwrap_or(0.0)) + .chain(p.iter().copied()) + .chain(std::iter::once(p.last().copied().unwrap_or(0.0))) + .collect() + }; + let net = us + .iter() + .map(|&a| { + vs.iter() + .map(|&b| { + let p = p3(origin) + p3(u) * a + p3(v) * b; + CPoint::new(p.x, p.y, p.z, NOW) + }) + .collect() + }) + .collect(); + let nurb = NurbSurface::new(net, 1, 1, knots(&us), knots(&vs))?; + let surface = Surface::from(BSplineSurface::from_nurb(nurb, false)); + let mut cache = surface_cache(&self.kernel, &surface, Some(id))?; + if cache.get_tree(&self.kernel)?.owner_object()? != Some(id) + || cache.surface_handle() != Some(id) + { + return Err(invalid("surface cache identity was not retained")); + } + let leaves = leaf_nodes(&cache)?; + self.surfaces.push(CachedSurface { + id, + surface, + cache, + leaves, + }); + Ok(()) + } + + fn surface_mut(&mut self, id: i64) -> solidcore::Result<&mut CachedSurface> { + self.surfaces + .iter_mut() + .find(|s| s.id == id) + .ok_or_else(|| invalid("unknown surface identity")) + } + + pub(super) fn add_hierarchy( + &mut self, + grid: &mut Grid, + id: i64, + tol: f64, + bbox: &Extent3d, + ) -> solidcore::Result<()> { + self.surface_mut(id)?; + let h = grid.add_hierarchy_element(HierarchyElement::new_surface(id, tol), bbox)?; + self.hierarchies.push((h, id)); + Ok(()) + } + + pub(super) fn expand( + &mut self, + grid: &mut Grid, + h: HierarchyId, + control: &mut impl FnMut(SurfaceGridElement) -> SurfaceGridElement, + ) -> solidcore::Result<()> { + let id = self + .hierarchies + .iter() + .find(|(key, _)| *key == h) + .map(|(_, id)| *id) + .ok_or_else(|| invalid("not an explicit surface hierarchy"))?; + let kernel = &self.kernel; + let s = self + .surfaces + .iter_mut() + .find(|s| s.id == id) + .ok_or_else(|| invalid("unknown surface identity"))?; + let tree = s + .cache + .tree() + .ok_or_else(|| invalid("cache has no tree"))? + .clone(); + grid.expand_hierarchy_with(h, &tree, |owner, node| { + if owner != id || !s.leaves.contains(&node) { + return Err(invalid("node does not belong to selected cache")); + } + Ok(control(init_leaf(kernel, s, node)?)) + }) + } + + pub(super) fn add_leaf( + &mut self, + grid: &mut Grid, + id: i64, + ordinal: usize, + bbox: &Extent3d, + control: &mut impl FnMut(SurfaceGridElement) -> SurfaceGridElement, + ) -> solidcore::Result<()> { + let kernel = &self.kernel; + let s = self + .surfaces + .iter_mut() + .find(|s| s.id == id) + .ok_or_else(|| invalid("unknown surface identity"))?; + let node = s + .leaves + .get(ordinal) + .copied() + .ok_or_else(|| invalid("leaf ordinal outside selected cache"))?; + grid.add_surface_element(control(init_leaf(kernel, s, node)?), bbox)?; + Ok(()) + } + + fn ordinal(&self, leaf: &SurfaceGridElement) -> solidcore::Result { + let s = self + .surfaces + .iter() + .find(|s| s.id == leaf.surface) + .ok_or_else(|| invalid("unknown leaf owner"))?; + for (i, &node) in s.leaves.iter().enumerate() { + if s.cache.patch(node)?.aux.uv_domain == leaf.uv_domain { + return Ok(i); + } + } + Err(invalid("leaf domain absent from owning cache")) + } + + pub(super) fn observe( + &self, + grid: &Grid, + oi: &mut Vec, + of: &mut Vec, + ) -> solidcore::Result<()> { + // Independent cache inventory in input/eligible-node order. + oi.push(self.surfaces.len() as i64); + for s in &self.surfaces { + oi.extend([s.id, s.leaves.len() as i64]); + for &node in &s.leaves { + put_domain(of, &s.cache.patch(node)?.aux.uv_domain); + put_ext( + of, + &s.cache + .tree() + .ok_or_else(|| invalid("cache has no tree"))? + .node(node)? + .bbox, + ); + } + } + oi.push(grid.hierarchy_elements().len() as i64); + for (i, _) in grid.hierarchy_elements().iter().enumerate() { + oi.push( + self.hierarchies + .iter() + .find(|(h, _)| h.0 == i) + .map_or(-1, |(_, id)| *id), + ); + } + // Surface pool allocation order, independent of voxel encounter order. + let leaves: Vec<_> = grid + .elements() + .iter() + .enumerate() + .filter_map(|(i, e)| { + if let GridItem::Surface(leaf) = e.item() { + Some((ElementId(i), e, leaf)) + } else { + None + } + }) + .collect(); + oi.push(leaves.len() as i64); + for (_, e, leaf) in &leaves { + oi.extend([leaf.surface, self.ordinal(leaf)? as i64]); + put3(of, e.sphere_center()); + of.push(e.sphere_radius()); + put_ext(of, e.bounding_box()); + put_leaf(of, leaf)?; + } + let [nx, ny, nz] = grid.size(); + for ix in 0..nx { + for iy in 0..ny { + for iz in 0..nz { + let voxel = grid.voxel([ix, iy, iz])?; + match voxel.grid_elements() { + None => oi.push(-1), + Some(es) => { + oi.push(es.len() as i64); + for &id in es { + put_elem(oi, grid, id)?; + oi.push( + leaves + .iter() + .position(|(key, _, _)| *key == id) + .map_or(-1, |i| i as i64), + ); + } + } + } + match voxel.hierarchy_elements() { + None => oi.push(-1), + Some(hs) => { + oi.push(hs.len() as i64); + oi.extend(hs.iter().map(|h| h.0 as i64)); + } + } + } + } + } + Ok(()) + } +} + +fn put_domain(out: &mut Vec, d: &solidcore::geom::extent2d::Extent2d) { + out.extend([d.min().x, d.min().y, d.max().x, d.max().y]); +} + +fn put_leaf(out: &mut Vec, leaf: &SurfaceGridElement) -> solidcore::Result<()> { + put_domain(out, &leaf.uv_domain); + put_ext(out, &leaf.bbox); + for v in [leaf.mid_point, leaf.mid_du, leaf.mid_dv, leaf.mid_normal] { + put3(out, v); + } + for j in 0..3 { + put3( + out, + leaf.pseudo_box + .basis_vector(j) + .ok_or_else(|| invalid("missing pseudo box basis"))?, + ); + } + for j in 0..3 { + let iv = leaf + .pseudo_box + .interval(j) + .ok_or_else(|| invalid("missing pseudo box interval"))?; + out.extend([iv.min(), iv.max()]); + } + let polar = &leaf.polar_box; + let state = match polar.state() { + PolarBoxState::Uninitialized => 0, + PolarBoxState::PointWithBasis => 1, + PolarBoxState::SinglePoint => 2, + PolarBoxState::Arc => 3, + PolarBoxState::Region => 4, + PolarBoxState::Unbounded => 5, + }; + out.push(f64::from(state)); + let bounded = (2..=4).contains(&state); + let d = polar.domain(); + out.extend(if bounded { + [d.min().x, d.min().y, d.max().x, d.max().y] + } else { + [0.0; 4] + }); + for b in polar.basis() { + put3( + out, + if bounded { + *b + } else { + Vec3::new(0.0, 0.0, 0.0) + }, + ); + } + Ok(()) +} diff --git a/crates/difftest/src/units/geom_grid/tests.rs b/crates/difftest/src/units/geom_grid/tests.rs new file mode 100644 index 000000000..e5b81abcd --- /dev/null +++ b/crates/difftest/src/units/geom_grid/tests.rs @@ -0,0 +1,185 @@ +//! Canonical-CI tests for explicit native cache coverage and observation sensitivity. +//! These are intentionally not a replacement for the unchanged 615-case corpus. + +use super::*; +use crate::case::{repo_root, CaseSource}; +use crate::compare::compare; + +const NEW_FIXTURES: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../fixtures/geom.grid/native_cache.json" +)); +const LEGACY_FIXTURES: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../fixtures/geom.grid/cases.json" +)); + +fn fixture(doc: &str, name: &str) -> Case { + let v: Value = serde_json::from_str(doc).unwrap(); + let c = v["cases"] + .as_array() + .unwrap() + .iter() + .find(|c| c["name"] == name) + .unwrap(); + Case { + unit: "geom.grid".into(), + name: name.into(), + source: CaseSource::Inline, + inputs: c["inputs"].clone(), + } +} + +fn output((ints, floats): (Vec, Vec)) -> Output { + Output::new().ints("ints", ints).values("floats", floats) +} + +#[test] +fn corpus_keeps_615_legacy_inputs_and_adds_six_explicit_cases() { + let ctx = CaseContext { + root: repo_root(), + seeds: 16, + }; + let cases = Runner.cases(&ctx).unwrap(); + assert_eq!(cases.len(), 621); + let old: Value = serde_json::from_str(LEGACY_FIXTURES).unwrap(); + let old = old["cases"].as_array().unwrap(); + assert_eq!(old.len(), 15); + for c in old { + let actual = cases + .iter() + .find(|x| x.name == c["name"].as_str().unwrap()) + .unwrap(); + assert_eq!(actual.inputs, c["inputs"]); + } + for n in 0..MIN_GENERATED { + let seed = CaseContext::seed_for("geom.grid", n); + let mut rng = Rng::new(seed); + let expected = serde_json::to_value(gen_case(&mut rng)).unwrap(); + let name = format!("gen{n:04}"); + let actual = cases.iter().find(|c| c.name == name).unwrap(); + assert_eq!(actual.inputs, expected); + assert!(!actual.inputs["ops"] + .as_array() + .unwrap() + .iter() + .any(|op| op["op"] == "define_surface")); + } +} + +#[test] +fn original_missing_geometry_is_still_deferred() { + for (name, symbol) in [ + ( + "raytracer_surface_hierarchy_expand", + "smsurf_GetSurfaceCache", + ), + ("surface_leaf_element", "SmBezierPatch::GetPseudoBox"), + ] { + let case = fixture(LEGACY_FIXTURES, name); + let i: Inputs = case.parse().unwrap(); + let error = run_rust(&i, &mut Stats::default()).unwrap_err(); + assert!(matches!(error, SolidError::Unported { upstream, .. } if upstream == symbol)); + } +} + +#[test] +fn explicit_definition_does_not_resolve_a_legacy_opaque_label() { + let mut legacy: Inputs = fixture(LEGACY_FIXTURES, "raytracer_surface_hierarchy_expand") + .parse() + .unwrap(); + let explicit: Inputs = fixture(NEW_FIXTURES, "native_affine_hierarchy_single") + .parse() + .unwrap(); + legacy.ops.insert(0, explicit.ops[0].clone()); // same numeric label 7, different input path + assert!(matches!( + run_rust(&legacy, &mut Stats::default()), + Err(SolidError::Unported { + upstream: "smsurf_GetSurfaceCache", + .. + }) + )); +} + +#[test] +fn explicit_guard_rejects_missing_owners_and_degenerate_geometry() { + let good: Inputs = fixture(NEW_FIXTURES, "native_affine_hierarchy_single") + .parse() + .unwrap(); + ffi::validate(&prog(&good)).unwrap(); + let mut missing = good.clone(); + if let Op::AddCachedHier { surface, .. } = &mut missing.ops[1] { + *surface = 999; + } + assert!(ffi::validate(&prog(&missing)).is_err()); + let mut parallel = good.clone(); + if let Op::DefineSurface { u, v, .. } = &mut parallel.ops[0] { + *v = *u; + } + assert!(ffi::validate(&prog(¶llel)).is_err()); + let mut huge = good.clone(); + if let Op::DefineSurface { spans, .. } = &mut huge.ops[0] { + *spans = [usize::MAX; 2]; + } + assert!(ffi::validate(&prog(&huge)).is_err()); + let mut legacy_hier = good; + legacy_hier.ops[1] = Op::AddHier { + surface: true, + obj: 7, + tol: 0.0, + bbox: [0.0; 6], + }; + assert!(ffi::validate(&prog(&legacy_hier)).is_err()); // expand_cached cannot dereference it +} + +#[test] +fn native_cache_node_selection_is_checked_before_add_element() { + let mut i: Inputs = fixture(NEW_FIXTURES, "native_affine_cache_leaf_ordinal") + .parse() + .unwrap(); + if let Op::AddCachedLeaf { leaf, .. } = &mut i.ops[1] { + *leaf = 255; + } + ffi::validate(&prog(&i)).unwrap(); // only the real built cache knows its leaf count + assert!(ffi::run(&prog(&i)).is_err()); + assert!(run_rust(&i, &mut Stats::default()).is_err()); +} + +#[test] +fn native_observer_detects_skipped_expansion_wrong_identity_and_corrupt_normal() { + let case = fixture(NEW_FIXTURES, "native_affine_identity_twins"); + let i: Inputs = case.parse().unwrap(); + let cpp = Runner.run_cpp(&case).unwrap(); + let rust = output(run_rust(&i, &mut Stats::default()).unwrap()); + let baseline = compare(&cpp, &rust, &[]); + assert!(baseline.pass(), "{baseline:?}"); + assert!(baseline.values_compared > 2 * 51); + assert!(baseline.exact_compared > 10); + + // Preserve successful op statuses, but actually omit the grid mutation. + let skipped = output(run_rust_controlled(&i, &mut Stats::default(), false, |l| l).unwrap()); + assert!(!compare(&cpp, &skipped, &[]).pass()); + + // Equal geometry means only the stable identity changes, not any float output. + let wrong = output( + run_rust_controlled(&i, &mut Stats::default(), true, |mut l| { + l.surface = if l.surface == 7 { 19 } else { 7 }; + l + }) + .unwrap(), + ); + assert_eq!(rust.values, wrong.values); + assert_ne!(rust.ints, wrong.ints); + assert!(!compare(&cpp, &wrong, &[]).pass()); + + // Topology, statuses and every other leaf field stay unchanged. + let corrupt = output( + run_rust_controlled(&i, &mut Stats::default(), true, |mut l| { + l.mid_normal.x += 0.125; + l + }) + .unwrap(), + ); + assert_eq!(rust.ints, corrupt.ints); + assert!(!compare(&cpp, &corrupt, &[]).pass()); +} diff --git a/crates/solidcore-ffi/src/units/geom_grid.rs b/crates/solidcore-ffi/src/units/geom_grid.rs index 356bcb770..0392204a1 100644 --- a/crates/solidcore-ffi/src/units/geom_grid.rs +++ b/crates/solidcore-ffi/src/units/geom_grid.rs @@ -10,6 +10,13 @@ //! //! Runs a small program against one upstream grid; the encoding is documented //! in the shim and validated here before any call. +//! +//! Opcodes 11–15 are an opt-in, unit-local extension: define an affine degree-1 +//! tensor-product B-spline, add its real hierarchy, expand that hierarchy, add a +//! cache-owned leaf by ordinal, and observe the caches/grid. Opcodes 1–10 retain +//! their original opaque-object semantics, even when an explicit surface has the +//! same numeric label. The bounded affine family avoids unbounded subdivision of +//! arbitrary Bezier control nets; it is not a restriction on the legacy corpus. use crate::validate::bad; use crate::{call_with_buffer, FfiError}; @@ -48,11 +55,12 @@ pub const MAX_COORD: f64 = 1e12; pub fn op_size(op: i64) -> Option { Some(match op { 0 | 2 => 9, - 1 => 8, + 1 | 12 | 14 => 8, 3 | 6 | 10 => 6, 4 | 5 => 3, - 7 | 9 => 1, - 8 => 0, + 7 | 9 | 13 => 1, + 8 | 15 => 0, + 11 => 16, _ => return None, }) } @@ -65,7 +73,12 @@ fn coords_ok(a: &[f64]) -> bool { a.iter().all(|v| v.is_finite() && v.abs() <= MAX_COORD) } -fn validate(prog: &[f64]) -> Result<(), FfiError> { +/// Validates a grid program without executing native code. +/// +/// Explicit surfaces are finite, well-conditioned affine patches with at most +/// three spans per direction. A selected leaf is checked again against the actual +/// native cache before its node is passed to `SmGrid::AddElement`. +pub fn validate(prog: &[f64]) -> Result<(), FfiError> { let ctor = prog.get(0..10).ok_or_else(|| bad("missing constructor"))?; if ctor.first() != Some(&0.0) { return Err(bad("constructor must be first")); @@ -89,13 +102,15 @@ fn validate(prog: &[f64]) -> Result<(), FfiError> { } } let mut hier = 0i64; + let mut surfaces = Vec::new(); + let mut real_hier = Vec::new(); let mut i = 10usize; while i < prog.len() { let op = prog .get(i) .copied() .ok_or_else(|| bad("truncated program"))?; - if !int_in(op, 1, 10) { + if !int_in(op, 1, 15) { return Err(bad("op")); } let op = op as i64; @@ -112,6 +127,7 @@ fn validate(prog: &[f64]) -> Result<(), FfiError> { } 2 => { hier += 1; + real_hier.push(false); (at(0) == 0.0 || at(0) == 1.0) && int_in(at(1), 0, MAX_OBJECTS - 1) && coords_ok(a.get(2..9).unwrap_or(&[f64::NAN])) @@ -120,6 +136,34 @@ fn validate(prog: &[f64]) -> Result<(), FfiError> { 5 => a.iter().all(|&v| int_in(v, -1000, 1000)), 7 => !at(0).is_nan(), 9 => int_in(at(0), 0, hier - 1), + 11 => { + let id = at(0); + let valid = surfaces.len() < 4 + && int_in(id, 0, MAX_OBJECTS - 1) + && !surfaces.contains(&(id as i64)) + && affine_surface_ok(a); + if valid { + surfaces.push(id as i64); + } + valid + } + 12 => { + hier += 1; + real_hier.push(true); + int_in(at(0), 0, MAX_OBJECTS - 1) + && surfaces.contains(&(at(0) as i64)) + && at(1).is_finite() + && (0.0..=1.0).contains(&at(1)) + && box_ok(a.get(2..8).unwrap_or_default()) + } + 13 => int_in(at(0), 0, hier - 1) && real_hier.get(at(0) as usize) == Some(&true), + 14 => { + int_in(at(0), 0, MAX_OBJECTS - 1) + && surfaces.contains(&(at(0) as i64)) + && int_in(at(1), 0, 255) + && box_ok(a.get(2..8).unwrap_or_default()) + } + 15 => !surfaces.is_empty(), _ => true, }; if !ok { @@ -130,6 +174,38 @@ fn validate(prog: &[f64]) -> Result<(), FfiError> { Ok(()) } +fn box_ok(a: &[f64]) -> bool { + a.len() == 6 + && coords_ok(a) + && (0..3).all(|k| a.get(k).zip(a.get(k + 3)).is_some_and(|(l, h)| l <= h)) +} + +fn affine_surface_ok(a: &[f64]) -> bool { + let at = |i| a.get(i).copied().unwrap_or(f64::NAN); + if !a + .get(1..10) + .is_some_and(|v| v.iter().all(|x| x.is_finite() && x.abs() <= 1024.0)) + || !(10..14).all(|i| at(i).is_finite() && at(i).abs() <= 8.0) + || ![at(12) - at(10), at(13) - at(11)] + .iter() + .all(|d| (0.125..=16.0).contains(d)) + || !int_in(at(14), 1, 3) + || !int_in(at(15), 1, 3) + { + return false; + } + let u = [at(4), at(5), at(6)]; + let v = [at(7), at(8), at(9)]; + let uu = u.iter().map(|x| x * x).sum::(); + let vv = v.iter().map(|x| x * x).sum::(); + let [ux, uy, uz] = u; + let [vx, vy, vz] = v; + let cross = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx]; + (0.015625..=4096.0).contains(&uu) + && (0.015625..=4096.0).contains(&vv) + && cross.iter().map(|x| x * x).sum::() >= 0.01 * uu * vv +} + /// Runs a grid program; returns its integer and float output streams. pub fn run(prog: &[f64]) -> Result<(Vec, Vec), FfiError> { validate(prog)?; diff --git a/fixtures/geom.grid/native_cache.json b/fixtures/geom.grid/native_cache.json new file mode 100644 index 000000000..1e9099741 --- /dev/null +++ b/fixtures/geom.grid/native_cache.json @@ -0,0 +1,101 @@ +{ + "cases": [ + { + "name": "native_affine_hierarchy_single", + "upstream_ref": "source/SMLib/src/SmGrid.cpp:40,252 (real standalone affine surface/cache; new input, not the legacy opaque fixture)", + "inputs": { + "grid": {"bbox": [-1, -1, -1, 2, 2, 2], "size": [4, 4, 4]}, + "ops": [ + {"op": "define_surface", "id": 7, "origin": [0, 0, 0], "u": [1, 0, 0], "v": [0, 1, 0], "domain": [0, 0, 1, 1], "spans": [1, 1]}, + {"op": "add_cached_hier", "surface": 7, "tol": 0.03125, "bbox": [-0.125, -0.125, -0.125, 1.125, 1.125, 0.125]}, + {"op": "dump"}, + {"op": "observe_cached"}, + {"op": "expand_cached", "h": 0}, + {"op": "dump"}, + {"op": "observe_cached"} + ] + } + }, + { + "name": "native_affine_multispan_hierarchy_oblique", + "upstream_ref": "source/SMLib/src/SmGrid.cpp:64-119 (actual cache node order and tolerance-expanded voxel registration)", + "inputs": { + "grid": {"bbox": [-2, -2, -2, 2, 2, 2], "size": [6, 5, 4]}, + "ops": [ + {"op": "define_surface", "id": 41, "origin": [0.25, -0.5, 0.125], "u": [1, 0, 0.5], "v": [0, 1, 0.25], "domain": [-1, -1, 1, 1], "spans": [2, 2]}, + {"op": "add", "kind": "user", "obj": 5, "bbox": [-0.5, -0.5, -0.5, 0.5, 0.5, 0.5]}, + {"op": "add_cached_hier", "surface": 41, "tol": 0.015625, "bbox": [-0.875, -1.625, -0.75, 1.375, 0.625, 1]}, + {"op": "expand_cached", "h": 0}, + {"op": "dump"}, + {"op": "observe_cached"}, + {"op": "setup", "p": [-3, 0, 0], "v": [1, 0, 0]}, + {"op": "step", "t": 1e20}, + {"op": "step", "t": 1e20}, + {"op": "dump"}, + {"op": "observe_cached"} + ] + } + }, + { + "name": "native_affine_cache_leaf_ordinal", + "upstream_ref": "source/SMLib/src/SmGrid.cpp:252,525 (AddElement of validated nodes from one real multispan cache, reverse ordinal order)", + "inputs": { + "grid": {"bbox": [-1, -1, -1, 3, 2, 1], "size": [8, 3, 2]}, + "ops": [ + {"op": "define_surface", "id": 19, "origin": [0, 0, 0], "u": [1, 0, 0], "v": [0, 1, 0], "domain": [0, 0, 2, 1], "spans": [2, 1]}, + {"op": "add_cached_leaf", "surface": 19, "leaf": 1, "bbox": [1, -0.125, -0.125, 2, 1.125, 0.125]}, + {"op": "add_cached_leaf", "surface": 19, "leaf": 0, "bbox": [0, 0, 0, 1, 1, 0]}, + {"op": "dump"}, + {"op": "observe_cached"} + ] + } + }, + { + "name": "native_affine_identity_twins", + "upstream_ref": "source/SMLib/src/SmGrid.cpp:40,252 (equal geometry, distinct stable surface identities, reverse hierarchy expansion)", + "inputs": { + "grid": {"bbox": [-1, -1, -1, 3, 2, 1], "size": [4, 3, 2]}, + "ops": [ + {"op": "define_surface", "id": 7, "origin": [0.25, 0.25, 0.25], "u": [2, 0, 0], "v": [0, 1, 0], "domain": [0, 0, 1, 1], "spans": [1, 1]}, + {"op": "define_surface", "id": 19, "origin": [0.25, 0.25, 0.25], "u": [2, 0, 0], "v": [0, 1, 0], "domain": [0, 0, 1, 1], "spans": [1, 1]}, + {"op": "add_cached_hier", "surface": 7, "tol": 0, "bbox": [0, 0, 0, 2.5, 1.5, 0.5]}, + {"op": "add_cached_hier", "surface": 19, "tol": 0.03125, "bbox": [0, 0, 0, 2.5, 1.5, 0.5]}, + {"op": "expand_cached", "h": 1}, + {"op": "expand_cached", "h": 0}, + {"op": "dump"}, + {"op": "observe_cached"} + ] + } + }, + { + "name": "native_affine_repeated_expansion", + "upstream_ref": "source/SMLib/src/SmGrid.cpp:40 (repeated real expansion retains distinct pool entries and ordered voxel references)", + "inputs": { + "grid": {"bbox": [-1, -2, -2, 1, 2, 2], "size": [2, 4, 4]}, + "ops": [ + {"op": "define_surface", "id": 73, "origin": [0.25, 0, 0], "u": [0, 0, 1], "v": [0, 1, 0], "domain": [-1, -1, 1, 1], "spans": [1, 1]}, + {"op": "add_cached_hier", "surface": 73, "tol": 0, "bbox": [-0.125, -1, -1, 0.625, 1, 1]}, + {"op": "expand_cached", "h": 0}, + {"op": "expand_cached", "h": 0}, + {"op": "dump"}, + {"op": "observe_cached"} + ] + } + }, + { + "name": "native_affine_leaf_registration_boundaries", + "upstream_ref": "source/SMLib/src/SmGrid.cpp:407,525 (zero-thickness, touching and outside registration boxes; surface itself stays nondegenerate)", + "inputs": { + "grid": {"bbox": [0, 0, 0, 1, 1, 1], "size": [4, 4, 4]}, + "ops": [ + {"op": "define_surface", "id": 3, "origin": [0, 0, 0.5], "u": [1, 0, 0], "v": [0, 1, 0], "domain": [0, 0, 1, 1], "spans": [1, 1]}, + {"op": "add_cached_leaf", "surface": 3, "leaf": 0, "bbox": [0.25, 0.25, 0.5, 0.5, 0.5, 0.5]}, + {"op": "add_cached_leaf", "surface": 3, "leaf": 0, "bbox": [1, 0, 0.5, 1, 1, 0.5]}, + {"op": "add_cached_leaf", "surface": 3, "leaf": 0, "bbox": [-2, -2, -2, -1, -1, -1]}, + {"op": "dump"}, + {"op": "observe_cached"} + ] + } + } + ] +} diff --git a/harness/cpp/shim/geom_grid.cpp b/harness/cpp/shim/geom_grid.cpp index 8e76ae15a..682f1e1fd 100644 --- a/harness/cpp/shim/geom_grid.cpp +++ b/harness/cpp/shim/geom_grid.cpp @@ -30,6 +30,19 @@ // run: a surface element needs a real SmSurface / SmSurfaceCache) // 10 e6 AddElement(SM_GE_SURFACE): ints: -2 (not run: needs a real // surface subdivision tree node) +// Explicit, unit-local cache extension (legacy opcodes above are unchanged): +// 11 id origin3 u3 v3 domain4 spans2 define/build a real affine B-spline cache +// 12 id tol e6 add its real SmSurfaceHierarchyElement +// 13 h call that hierarchy's real ExpandHierarchy +// 14 id leaf_ordinal e6 AddElement(actual cache node, SM_GE_SURFACE) +// 15 observe caches, surface pool and voxel lists +// Ops 11–14 return their status (construction 0 only after a valid cache exists). +// 15 ints: surface count, (input id, eligible-node count)...; hierarchy count and +// real owner ids (-1 for legacy); surface-pool count, (owner id, leaf ordinal)...; +// then x-major voxels: element count/-1, (legacy type/id, surface-pool index/-1)..., +// hierarchy count/-1 and hierarchy indices. Floats: each cache leaf's domain4/box6, +// then each pool entry's sphere4/base box6 and 51 leaf values (domain4, node box6, +// midpoint/DU/DV/normal12, pseudo basis9/intervals6, polar state1/domain4/basis9). // dda dump: ints address, then per axis with next_t < SM_BIG_DOUBLE: step out; floats ray // point, ray vector, ray t, current point, next_t, delta_t, the three delta vectors, then // per axis with next_t < SM_BIG_DOUBLE: next plane point. @@ -37,13 +50,22 @@ // reported as (type code, object id). #include "sc_shim.h" +#include #include +#include +#include #include #include +#include +#include #include +#include #include +#include #include +#include +#include #include namespace @@ -87,6 +109,11 @@ int64_t op_size(int op) case 8: return 0; case 9: return 1; case 10: return 6; + case 11: return 16; + case 12: return 8; + case 13: return 1; + case 14: return 8; + case 15: return 0; default: return -1; } } @@ -99,6 +126,16 @@ class ShimGrid : public SmGrid long size(int i) const { return m_lSize[i]; } + std::vector surface_elements() + { + SmTArray active; + m_sSurfaceElementMgr.GetActiveElements(active); + std::vector out; + for (ULONG i = 0; i < active.GetSize(); ++i) + out.push_back(static_cast(active[i])); + return out; + } + void dump_dda(std::vector& oi, std::vector& of) const { for (int i = 0; i < 3; i++) @@ -153,13 +190,17 @@ void put_sphere_box(std::vector& of, const SmGridElement& e) put_ext(of, e.GetBoundingBox()); } +#include "geom_grid_native.inc" + int32_t run(const double* p, int64_t n, std::vector& oi, std::vector& of) { if (n < 10 || (int)p[0] != 0) return SC_ERR_ARG; SmContext sCtx; + NativeCaches caches; ULONG lSize[3] = {(ULONG)p[7], (ULONG)p[8], (ULONG)p[9]}; - ShimGrid* g = new (sCtx) ShimGrid(ext(p + 1), lSize); + std::unique_ptr owned_grid(new (sCtx) ShimGrid(ext(p + 1), lSize)); + ShimGrid* g = owned_grid.get(); std::vector hier; std::map hidx; bool ready = false; @@ -351,6 +392,43 @@ int32_t run(const double* p, int64_t n, std::vector& oi, std::vectorAddHierarchyElement(h, ext(a + 2)))); + break; + } + case 13: + { + SmHierarchyElement* h = hier.at((size_t)a[0]); + if (caches.hierarchies.count(h) != 1) + return SC_ERR_ARG; + oi.push_back(st(h->ExpandHierarchy(g))); + break; + } + case 14: + { + NativeSurface& s = caches.find((int64_t)a[0]); + // The ordinal is checked against this actual cache, not a fake node id. + if (a[1] < 0 || a[1] >= (double)s.leaves.size()) + return SC_ERR_ARG; + SmTreeNode* node = s.leaves.at((size_t)a[1]); + if (node->GetOwnerObject() != s.surface.get()) + return SC_ERR_ARG; + oi.push_back(st(g->AddElement(node, SM_GE_SURFACE, ext(a + 2)))); + break; + } + case 15: + caches.observe(g, hier, hidx, oi, of); + break; default: rc = SC_ERR_ARG; break; @@ -359,7 +437,9 @@ int32_t run(const double* p, int64_t n, std::vector& oi, std::vector& out, const SmExtent2d& d) +{ + out.push_back(d.GetMin().x); + out.push_back(d.GetMin().y); + out.push_back(d.GetMax().x); + out.push_back(d.GetMax().y); +} + +struct NativeSurface +{ + int64_t id; + std::unique_ptr surface; + SmSurfaceCache* cache; + std::vector leaves; +}; + +void put_native_leaf(std::vector& out, const SmSurfaceGridElement& e) +{ + put_domain(out, e.m_sUVDomain); + put_ext(out, e.m_sBBox); + put3(out, e.m_sMidPoint); + put3(out, e.m_sMidDU); + put3(out, e.m_sMidDV); + put3(out, e.m_sMidNormal); + SmVector3d basis[3]; + e.m_sPseudoBox.GetBasis(basis[0], basis[1], basis[2]); + for (int i = 0; i < 3; ++i) + put3(out, basis[i]); + SmExtent1d iv[3]; + e.m_sPseudoBox.GetIntervals(iv[0], iv[1], iv[2]); + for (int i = 0; i < 3; ++i) + { + out.push_back(iv[i].GetMin()); + out.push_back(iv[i].GetMax()); + } + const SmPolarBox& polar = e.m_sPolarBox; + int ps = (int)polar.GetPolarBoxState(); + out.push_back((double)ps); + bool bounded = ps >= SM_PS_SINGLE_POINT && ps <= SM_PS_REGION; + SmExtent2d d = polar.GetDomain(); + if (bounded) + put_domain(out, d); + else + out.insert(out.end(), 4, 0.0); + for (ULONG j = 0; j < 3; ++j) + put3(out, bounded ? *polar.GetBasis(j) : SmVector3d(0.0, 0.0, 0.0)); +} + +struct NativeCaches +{ + std::vector surfaces; + std::map hierarchies; + + NativeSurface& find(int64_t id) + { + for (NativeSurface& s : surfaces) + if (s.id == id) + return s; + throw std::runtime_error("unknown explicit grid surface"); + } + + void define(const SmContext& ctx, const double* a) + { + // The Rust FFI guard bounds/conditions this affine family before any native call. + int64_t id = (int64_t)a[0]; + for (const NativeSurface& s : surfaces) + if (s.id == id) + throw std::runtime_error("duplicate explicit grid surface"); + gw_INDEX nu = (gw_INDEX)a[14], nv = (gw_INDEX)a[15]; + gw_SURFACE* nurb = sm_AllocateNurbSurface(nu, nv, 1, 1, nu + 2, nv + 2); + if (nurb == nullptr) + throw std::runtime_error("cannot allocate affine surface"); + auto u = [&](gw_INDEX i) { return a[10] + (a[12] - a[10]) * (double)i / (double)nu; }; + auto v = [&](gw_INDEX j) { return a[11] + (a[13] - a[11]) * (double)j / (double)nv; }; + for (gw_INDEX i = 0; i <= nu; ++i) + for (gw_INDEX j = 0; j <= nv; ++j) + { + nurb->net->Pw[i][j].x = a[1] + a[4] * u(i) + a[7] * v(j); + nurb->net->Pw[i][j].y = a[2] + a[5] * u(i) + a[8] * v(j); + nurb->net->Pw[i][j].z = a[3] + a[6] * u(i) + a[9] * v(j); + nurb->net->Pw[i][j].w = NL_NOW; + } + for (gw_INDEX i = 0; i <= nu + 2; ++i) + nurb->knu->U[i] = i == 0 ? u(0) : (i > nu ? u(nu) : u(i - 1)); + for (gw_INDEX j = 0; j <= nv + 2; ++j) + nurb->knv->U[j] = j == 0 ? v(0) : (j > nv ? v(nv) : v(j - 1)); + NativeSurface s; + s.id = id; + s.surface.reset(new (ctx) SmBSplineSurface(nurb, FALSE, &ctx)); + s.cache = smsurf_GetSurfaceCache(s.surface.get()); + if (s.cache == nullptr || s.cache->GetSurface() != s.surface.get()) + throw std::runtime_error("missing or misowned native surface cache"); + SmTree* tree = s.cache->GetTree(); + if (tree == nullptr) + throw std::runtime_error("missing native cache tree"); + SmTArray nodes; + tree->GetAllTreeNodes(nodes); + for (ULONG i = 0; i < nodes.GetSize(); ++i) + { + SmTreeNode* n = nodes[i]; + if (n->m_eAuxDataType != SM_AD_BEZIER_SURFACE || + (n->m_pChild1 != nullptr && n->m_pChild1->m_eAuxDataType == SM_AD_BEZIER_SURFACE)) + continue; + SmBezierPatch* patch = static_cast(n->m_pData); + if (patch == nullptr || n->GetOwnerObject() != s.surface.get()) + throw std::runtime_error("invalid native cache patch ownership"); + if (patch->m_eNodeClass != SM_NC_OUTSIDE) + s.leaves.push_back(n); + } + if (s.leaves.empty() || s.leaves.size() > 256) + throw std::runtime_error("unexpected affine cache leaf count"); + surfaces.push_back(std::move(s)); + } + + std::pair identity(const SmSurfaceGridElement& e) const + { + for (const NativeSurface& s : surfaces) + if (s.surface.get() == e.m_pSurface) + for (size_t j = 0; j < s.leaves.size(); ++j) + { + const SmExtent2d& d = static_cast(s.leaves[j]->m_pData)->m_sUVDomain; + const SmExtent2d& q = e.m_sUVDomain; + if (d.GetMin().x == q.GetMin().x && d.GetMin().y == q.GetMin().y && + d.GetMax().x == q.GetMax().x && d.GetMax().y == q.GetMax().y) + return {s.id, (int64_t)j}; + } + throw std::runtime_error("grid leaf absent from its owning cache"); + } + + void observe(ShimGrid* grid, const std::vector& hier, + const std::map& hidx, + std::vector& oi, std::vector& of) const + { + oi.push_back((int64_t)surfaces.size()); + for (const NativeSurface& s : surfaces) + { + oi.push_back(s.id); + oi.push_back((int64_t)s.leaves.size()); + for (SmTreeNode* node : s.leaves) + { + put_domain(of, static_cast(node->m_pData)->m_sUVDomain); + put_ext(of, node->m_sBBox); + } + } + oi.push_back((int64_t)hier.size()); + for (SmHierarchyElement* h : hier) + { + auto it = hierarchies.find(h); + oi.push_back(it == hierarchies.end() ? -1 : it->second); + } + std::vector leaves = grid->surface_elements(); + oi.push_back((int64_t)leaves.size()); + for (SmSurfaceGridElement* e : leaves) + { + auto key = identity(*e); + oi.push_back(key.first); + oi.push_back(key.second); + put_sphere_box(of, *e); + put_native_leaf(of, *e); + } + for (long ix = 0; ix < grid->size(0); ++ix) + for (long iy = 0; iy < grid->size(1); ++iy) + for (long iz = 0; iz < grid->size(2); ++iz) + { + long ad[3] = {ix, iy, iz}; + SmVoxel* voxel = grid->GetVoxel(ad); + SmTArray* es = voxel->GetGridElements(); + oi.push_back(es == nullptr ? -1 : (int64_t)es->GetSize()); + if (es != nullptr) + for (ULONG j = 0; j < es->GetSize(); ++j) + { + SmGridElement* e = (*es)[j]; + put_elem(oi, e); // preserve the original type/object record + int64_t index = -1; + for (size_t k = 0; k < leaves.size(); ++k) + if (static_cast(leaves[k]) == e) + index = (int64_t)k; + oi.push_back(index); + } + SmTArray* hs = voxel->GetHierarchyElements(); + oi.push_back(hs == nullptr ? -1 : (int64_t)hs->GetSize()); + if (hs != nullptr) + for (ULONG j = 0; j < hs->GetSize(); ++j) + oi.push_back(hidx.at((*hs)[j])); + } + } +};