Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 10 additions & 45 deletions crates/difftest/known_mismatches.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,51 +41,6 @@ unit = "topology.brep_p3"
case = "gen0241"
why = "TurnToNURBS tracker: upstream curves/surfaces notify their own SM_NO_PRE_EDIT/SM_NO_POST_EDIT edits, the port's geometry sends none (plan/findings.md: topology.face_p1 SetSurface SM_NO_CHANGE_GEOMETRY wired (PR #637), 'Still deferred: geometry edit notifications')"

[[mismatch]]
unit = "primitive.primitive_creation_p2"
case = "skin_concentric_loops"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "sweep.api_surfaces"
case = "skin_primitive_annular"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "two_cubes_at_a_vertex"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "two_disjoint_cubes"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "cube_in_cube"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "cube_with_wire"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "cube_with_inner_wire"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "cube_with_inner_vertex"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "cube_with_outer_vertex"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.face_p1"
case = "cone.smb_1_f5_op9"
Expand All @@ -95,3 +50,13 @@ why = "MoveSeam with split: the runner's null classification handles cannot name
unit = "topology.face_p1"
case = "torus.smb_1_f5_op9"
why = "MoveSeam with split: the runner's null classification handles cannot name topology, so the port's post-move HasSeamProblem fails (BrepSeamObjects InvalidInput, ignored as 'no problem') where upstream finds one and splits; reachable once the UB-137 fork trap runs the call (plan/findings.md: topology.face_p1 fork trap merged with main)"

[[mismatch]]
unit = "primitive.primitive_creation_p2"
case = "skin_concentric_loops"
why = "the annular cap between two concentric loops comes out as two faces (record[7] cpp 1, rust 2): region nesting still differs once IsInnerShell runs on the B-rep with the face surfaces' kept trimmed caches (plan/findings.md: IsInnerShell on the B-rep with the kept trimmed caches)"

[[mismatch]]
unit = "sweep.api_surfaces"
case = "skin_primitive_annular"
why = "the annular cap between two concentric loops comes out as two faces (faces[1] cpp 1, rust 2): region nesting still differs once IsInnerShell runs on the B-rep with the face surfaces' kept trimmed caches (plan/findings.md: IsInnerShell on the B-rep with the kept trimmed caches)"
7 changes: 7 additions & 0 deletions crates/difftest/src/units/intersect_curve_class_p9.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,12 @@ fn loop_vertex_uv(k: &Kernel, surface: &Surface, point: Vec3, zone: f64) -> ScRe
..Vertexuse::default()
})?;
b.loopuse_mut(lu)?.start = LoopuseStart::Vertexuse(Some(vu));
// The shim attaches its cache to the surface first, with point testing off, so
// ComputeUVPoint's smsurf_GetSurfaceCache reuses it instead of building a trimmed
// cache of the face: give the Rust surface the same kind of cache.
if let Some(s) = b.face(f)?.surface.as_ref() {
s.attach_trim_cache(|c| c.base_mut().point_test_enabled = false)?;
}
solidcore::topology::vertexuse::compute_uv_point(k, &mut b, vu, true)
}
fn rust(i: &Input) -> ScResult<Output> {
Expand Down Expand Up @@ -546,6 +552,7 @@ fn rust(i: &Input) -> ScResult<Output> {
surface: &pool.surface,
loops: &loops,
cache: &cache,
cache_handle: &Some,
},
i.check3d,
)
Expand Down
13 changes: 13 additions & 0 deletions crates/difftest/src/units/topology_brep_p4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,19 @@ impl UnitDiff for Runner {
}
let mut b = import(&f)?;
own_surfaces(&mut b)?;
// reading a file heals the B-rep during a Boolean operation, which leaves its
// face surfaces with trimmed caches whose trees are not built (and that later
// smsurf_GetSurfaceCache calls reuse): the face surfaces' cache states as the
// C++ read left them
if let BrepIn::File { .. } = &i.brep {
use super::topology_edgeuse_p1 as eu1;
let file = eu1::BrepIn::File {
file: String::new(),
rebuild: false,
};
let blk = eu1::brep_block_of(Some(&file), false, false)?;
eu1::seed_cache_tree_states(&b, &path, &blk)?;
}
damage(&mut b, i.damage)?;
b.set_editing_enabled(i.editing);
let pre = record_counts(&b);
Expand Down
13 changes: 6 additions & 7 deletions crates/difftest/src/units/topology_topology_traverser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,14 @@ impl UnitDiff for Runner {
&mut k, &mut b, &mut marks, start_fu, target, &mut fus, &lock,
),
6 => tr::remove_shell(&mut k, &mut b, ShellId::from_index(r.start as u32), target),
_ => tr::is_inner_shell(
&mut k,
&b,
&b.faceuses()
_ => {
let faceuses = b
.faceuses()
.iter()
.filter_map(|(id, fu)| (fu.shell == Some(target)).then_some(id))
.collect::<Vec<_>>(),
)
.map(|v| closed = i64::from(v)),
.collect::<Vec<_>>();
tr::is_inner_shell(&mut k, &mut b, &faceuses).map(|v| closed = i64::from(v))
}
};
if let Err(e) = result {
if let Some(out) = Output::unported(&e) {
Expand Down
35 changes: 34 additions & 1 deletion crates/difftest/src/units/topology_vertexuse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1407,8 +1407,41 @@ pub(crate) fn recalc_gaps(b: &mut Brep) -> Result<(), SolidError> {
/// Each record is stored only after it is fully computed. `strict`: stop at the first
/// failure; else skip the failed record.
fn fill_gaps(b: &mut Brep, strict: bool) -> Result<(), SolidError> {
// The refill reconstructs values the C++ export reported, and must not leave cache
// history the C++ side did not have: its smsurf_GetSurfaceCache calls build no
// tree (as during a Boolean operation: no UV trim curves are made), and the trimmed
// caches they keep are dropped again, leaving each surface's cache slot as a
// face-unaware lookup outside a Boolean operation leaves it (a built tree).
use solidcore::surface::surface::CacheTreeState;
let before: Vec<Option<(bool, CacheTreeState)>> = b
.faces()
.iter()
.map(|(_, f)| {
f.surface
.as_ref()
.map(|s| (s.has_trim_cache(), s.cache_tree_state()))
})
.collect();
let result = fill_gaps_body(b, strict);
for ((_, f), before) in b.faces().iter().zip(before) {
if let (Some(s), Some((had, state))) = (f.surface.as_ref(), before) {
if !had && s.has_trim_cache() && !s.trim_cache_in_use() {
s.delete_trim_cache();
}
if state == CacheTreeState::NoCache && s.cache_tree_state() == CacheTreeState::Unbuilt
{
s.set_cache_tree_state(CacheTreeState::Built);
}
}
}
result
}

fn fill_gaps_body(b: &mut Brep, strict: bool) -> Result<(), SolidError> {
use std::collections::BTreeSet;
let k = Kernel::default();
let mut k = Kernel::default();
k.context.set_doing_boolean(true);
let k = k;
let keep = |r: Result<(), SolidError>| match r {
Err(e) if strict => Err(e),
_ => Ok(()),
Expand Down
119 changes: 92 additions & 27 deletions crates/solidcore/src/geom/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
//! | `lock_mark`, `unlock_mark` (private) | `SmNewMarkAndLock::LockMark`, `UnlockMark` |
//! | `unlocked_mark` (private) | `SmNewMarkAndLock::GetUnlockedMark` |
//! | [`with_mark_lock`] | the scoped `{ SmNewMarkAndLock sMarkLock(pContext, mask); ... }` idiom |
//! | [`Context::claim_mark`], [`SharedMarkLock`] | the same idiom in a `const` query (`SmNewMarkAndLock(const SmContext*, mask)`) |
//! | [`MassPropertyBehavior`] | `SmMassPropertyBehavior` (and its constructor) |
//! | [`create_context`] | `SmApiCreateContext` |
//! | [`get_or_create_context`] | `SmApiGetOrCreateContext` |
Expand All @@ -58,6 +59,10 @@
//! [`Kernel`]'s own context. A method that upstream would run through a null
//! context pointer (a crash) returns [`SolidError::InvalidInput`], as does an index
//! outside the slice.
//! - The marks and their locks are atomics, so that a `const` query that only holds the
//! context shared (the port's `&Kernel`) can still claim a mark as upstream's do
//! ([`Context::claim_mark`], whose guard unlocks on drop). The kernel is
//! single-threaded: the accesses happen in program order.
//! - The upstream two-context constructor locks the *first* context's mark type in the
//! second context and increments the first context's mark twice; this is ported
//! as is (see `plan/findings.md`).
Expand All @@ -76,7 +81,7 @@
//! `Dump`, `GetTypeString` and `IsLockedMark` are unused upstream and not ported.

use std::sync::{
atomic::{AtomicU64, Ordering},
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
};

Expand Down Expand Up @@ -187,11 +192,14 @@ pub trait NotifyCallback: std::fmt::Debug + Send {
#[derive(Debug)]
pub struct Context {
/// `m_lCurrentMark`, `m_lCurrentMark2`, `m_lCurrentMark3`, `m_lCurrentMarkIO`,
/// `m_lCurrentMarkAssert`.
current_marks: [u64; 5],
/// `m_lCurrentMarkAssert`. Atomic so that a query holding the context shared can
/// claim a mark ([`Context::claim_mark`]), as upstream's `const` queries do
/// through `SmNewMarkAndLock(const SmContext*, ..)`; single-threaded use orders the
/// accesses.
current_marks: [AtomicU64; 5],
/// `m_bMarkLock`, `m_bMark2Lock`, `m_bMark3Lock`, `m_bMarkIOLock`,
/// `m_bMarkAssertLock`.
mark_locks: [bool; 5],
mark_locks: [AtomicBool; 5],
/// `m_bDoingBoolean`.
doing_boolean: bool,
/// `m_dThisModelSizeEstimate`.
Expand Down Expand Up @@ -460,8 +468,14 @@ impl Context {
/// An immutable metadata snapshot, without copying callback ownership.
pub(crate) fn metadata_copy(&self) -> Self {
Self {
current_marks: self.current_marks,
mark_locks: self.mark_locks,
current_marks: self
.current_marks
.each_ref()
.map(|m| AtomicU64::new(m.load(Ordering::Relaxed))),
mark_locks: self
.mark_locks
.each_ref()
.map(|l| AtomicBool::new(l.load(Ordering::Relaxed))),
doing_boolean: self.doing_boolean,
model_size_estimate: self.model_size_estimate,
large_small_size_ratio: self.large_small_size_ratio,
Expand All @@ -479,8 +493,8 @@ impl Context {
#[must_use]
pub fn new(model_size_estimate: f64, large_small_size_ratio: f64) -> Self {
Self {
current_marks: [1; 5],
mark_locks: [false; 5],
current_marks: [1; 5].map(AtomicU64::new),
mark_locks: [false; 5].map(AtomicBool::new),
doing_boolean: false,
model_size_estimate,
large_small_size_ratio,
Expand Down Expand Up @@ -676,34 +690,29 @@ impl Context {
pub fn current_mark(&self, mark_type: MarkType) -> u64 {
match mark_type {
MarkType::NoMark => {
let [m1, m2, m3, mio, massert] = self.current_marks;
let [m1, m2, m3, mio, massert] = self
.current_marks
.each_ref()
.map(|m| m.load(Ordering::Relaxed));
1 + max5(m1, m2, m3, mio, massert)
}
_ => self
.current_marks
.get(mark_type.slot().unwrap_or(MARK_IO))
.copied()
.unwrap_or(0),
.map_or(0, |m| m.load(Ordering::Relaxed)),
}
}

/// Advances a mark by two and returns the new value (`SmContext::NewMark`; the
/// lock check is a debug-only assertion upstream). [`MarkType::NoMark`] changes
/// nothing and returns [`Context::current_mark`] of it.
fn new_mark(&mut self, mark_type: MarkType) -> u64 {
fn new_mark(&self, mark_type: MarkType) -> u64 {
if mark_type == MarkType::NoMark {
return self.current_mark(MarkType::NoMark);
}
match self
.current_marks
.get_mut(mark_type.slot().unwrap_or(MARK_IO))
{
Some(m) => {
*m = m.wrapping_add(2);
*m
}
None => 0,
}
self.current_marks
.get(mark_type.slot().unwrap_or(MARK_IO))
.map_or(0, |m| m.fetch_add(2, Ordering::Relaxed).wrapping_add(2))
}

/// Whether a mark is locked (`IsMarkLocked`); never for [`MarkType::NoMark`] or
Expand All @@ -713,14 +722,52 @@ impl Context {
mark_type
.slot()
.and_then(|s| self.mark_locks.get(s))
.copied()
.unwrap_or(false)
.is_some_and(|l| l.load(Ordering::Relaxed))
}

/// Sets a mark's lock flag (no-op for marks without a slot).
fn set_lock(&mut self, mark_type: MarkType, locked: bool) {
if let Some(l) = mark_type.slot().and_then(|s| self.mark_locks.get_mut(s)) {
*l = locked;
fn set_lock(&self, mark_type: MarkType, locked: bool) {
if let Some(l) = mark_type.slot().and_then(|s| self.mark_locks.get(s)) {
l.store(locked, Ordering::Relaxed);
}
}

/// `SmNewMarkAndLock(const SmContext*, mask)` for a holder of the context that
/// cannot change it otherwise (upstream's `const` queries): picks the first
/// unlocked mark allowed by `mask` as [`MarkLock::new`] does, advances and locks
/// it until the returned guard is dropped (`~SmNewMarkAndLock`).
#[must_use]
pub fn claim_mark(&self, mask: i32) -> SharedMarkLock<'_> {
let mark_type = unlocked_mark(self, mask);
self.new_mark(mark_type);
self.set_lock(mark_type, true);
SharedMarkLock {
context: self,
mark_type,
}
}
}

/// A mark claimed through a shared context ([`Context::claim_mark`]); dropping it
/// unlocks the mark if it is still locked (`~SmNewMarkAndLock`).
#[derive(Debug)]
pub struct SharedMarkLock<'a> {
context: &'a Context,
mark_type: MarkType,
}

impl SharedMarkLock<'_> {
/// The claimed mark (`GetMarkType`).
#[must_use]
pub const fn mark_type(&self) -> MarkType {
self.mark_type
}
}

impl Drop for SharedMarkLock<'_> {
fn drop(&mut self) {
if self.context.is_mark_locked(self.mark_type) {
self.context.set_lock(self.mark_type, false);
}
}
}
Expand Down Expand Up @@ -1072,6 +1119,24 @@ pub fn get_or_create_context(slot: &mut Option<Context>) -> &mut Context {

#[cfg(test)]
mod tests {
#[test]
fn a_shared_context_claims_and_unlocks_a_mark() {
let mut k = Kernel::default();
let before = k.context.current_mark(MarkType::Mark2);
let m = with_mark_lock(&mut k, ALL_MARKS, |k, outer| {
assert_eq!(outer, MarkType::Mark);
let lock = k.context.claim_mark(ALL_MARKS);
// the first mark is locked by the enclosing lock: the second is claimed
assert_eq!(lock.mark_type(), MarkType::Mark2);
assert!(k.context.is_mark_locked(MarkType::Mark2));
lock.mark_type()
})
.unwrap();
assert_eq!(m, MarkType::Mark2);
assert!(!k.context.is_mark_locked(MarkType::Mark2));
assert_eq!(k.context.current_mark(MarkType::Mark2), before + 2);
}

use super::*;

const ALL: [MarkType; 7] = [
Expand Down
4 changes: 2 additions & 2 deletions crates/solidcore/src/intersect/curve_class/p4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ fn edgeuse_evaluate_binormal<O: ClassObject, G: ClassGeometry<O>>(
if !eu1::is_loop_edgeuse(b, eu)? {
return Ok(());
}
let mut scratch = b.clone();
let mut scratch = b.clone_sharing_surface_caches();
match topo_eu3::evaluate_binormal(
k,
&mut scratch,
Expand Down Expand Up @@ -500,7 +500,7 @@ fn vertexuse_compute_uv_sector<O: ClassObject, G: ClassGeometry<O>>(
vertexuse: O,
) -> Result<UvSectorIo<O>> {
let (b, vu) = vertexuse_id(geometry, vertexuse, OP_CROSS)?;
let mut scratch = b.clone();
let mut scratch = b.clone_sharing_surface_caches();
let io = topo_vertexuse::compute_uv_sector(k, &mut scratch, vu, false)?.io;
let eu_handle = |eu: Option<EdgeuseId>| -> Result<Option<O>> {
eu.map(|eu| handle_of(geometry, b, EntityRef::Edgeuse(eu), OP_CROSS))
Expand Down
Loading
Loading