mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-10 04:19:59 -06:00
Merge pull request #2436 from vangel76/perf/region-build-no-siphash-set
Region build: test candidate pairs once in their lowest shared cell
This commit is contained in:
+283
-62
@@ -75,7 +75,108 @@ fn split_pair(segs: &[(Vec2, Vec2)], splits: &mut [Vec<f64>], i: usize, j: usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Upper bound on candidate-pair *visits*. Every visit counts, including repeats of a pair that shares several grid
|
||||
/// cells, not only the distinct pairs actually tested. A long segment replicated across many
|
||||
/// cells makes the same pair reappear in each, so a budget on distinct pairs alone leaves the
|
||||
/// repeated work unbounded (CWE-407).
|
||||
const MAX_PAIR_VISITS: u64 = 60_000_000;
|
||||
/// Upper bound on distinct segment pairs tested for a crossing.
|
||||
const MAX_PAIRS_TESTED: usize = 20_000_000;
|
||||
|
||||
/// Split parameters of every segment (always `0` and `1`, plus every crossing), found with a uniform grid.
|
||||
fn pair_splits(segs: &[(Vec2, Vec2)], bb: &Bounds2) -> Option<Vec<Vec<f64>>> {
|
||||
pair_splits_limited(segs, bb, MAX_PAIR_VISITS, MAX_PAIRS_TESTED)
|
||||
}
|
||||
|
||||
/// `pair_splits` with explicit budgets: `None` once more than `max_visits` candidate pairs were visited
|
||||
/// or more than `max_pairs` distinct pairs were tested.
|
||||
fn pair_splits_limited(segs: &[(Vec2, Vec2)], bb: &Bounds2, max_visits: u64, max_pairs: usize) -> Option<Vec<Vec<f64>>> {
|
||||
let n = segs.len();
|
||||
let cells = ((n as f64).sqrt().ceil() as usize).clamp(1, 512);
|
||||
let cw = (bb.width() / cells as f64).max(1e-12);
|
||||
let ch = (bb.height() / cells as f64).max(1e-12);
|
||||
let cell = |p: Vec2| {
|
||||
(
|
||||
((p.x - bb.min.x) / cw).floor().clamp(0.0, (cells - 1) as f64) as usize,
|
||||
((p.y - bb.min.y) / ch).floor().clamp(0.0, (cells - 1) as f64) as usize,
|
||||
)
|
||||
};
|
||||
// Cell lists in a flat grid, indexed `gx * cells + gy`; each list is in ascending segment order.
|
||||
let mut grid: Vec<Vec<usize>> = vec![Vec::new(); cells * cells];
|
||||
// The lowest cell of each segment's cell range; the lowest cell two segments share is the
|
||||
// componentwise maximum of their two.
|
||||
let mut low: Vec<(usize, usize)> = vec![(0, 0); n];
|
||||
let mut is_long = vec![false; n];
|
||||
let mut long: Vec<usize> = Vec::new();
|
||||
for (i, (a, b)) in segs.iter().enumerate() {
|
||||
let (x0, y0) = cell(a.min(*b));
|
||||
let (x1, y1) = cell(a.max(*b));
|
||||
if let Some(l) = low.get_mut(i) {
|
||||
*l = (x0, y0);
|
||||
}
|
||||
if (x1 - x0 + 1) * (y1 - y0 + 1) > 4096 {
|
||||
// Very long segment: too many cells to register in; it is tested against every segment below.
|
||||
long.push(i);
|
||||
if let Some(f) = is_long.get_mut(i) {
|
||||
*f = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for gx in x0..=x1 {
|
||||
for gy in y0..=y1 {
|
||||
if let Some(list) = grid.get_mut(gx * cells + gy) {
|
||||
list.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut splits: Vec<Vec<f64>> = vec![vec![0.0, 1.0]; n];
|
||||
let mut tested: usize = 0;
|
||||
let mut visits: u64 = 0;
|
||||
for (c, list) in grid.iter().enumerate() {
|
||||
let (gx, gy) = (c / cells, c % cells);
|
||||
for (k, &i) in list.iter().enumerate() {
|
||||
let Some(&(ix, iy)) = low.get(i) else { continue };
|
||||
for &j in list.iter().skip(k + 1) {
|
||||
visits += 1;
|
||||
if visits > max_visits {
|
||||
return None;
|
||||
}
|
||||
let Some(&(jx, jy)) = low.get(j) else { continue };
|
||||
// A pair sharing several cells is tested once, in the lowest cell both occupy.
|
||||
if i == j || gx != ix.max(jx) || gy != iy.max(jy) {
|
||||
continue;
|
||||
}
|
||||
split_pair(segs, &mut splits, i, j);
|
||||
tested += 1;
|
||||
if tested > max_pairs {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// A long segment is in no cell list. Two long segments are tested once, from the lower index.
|
||||
for &i in &long {
|
||||
for j in 0..n {
|
||||
if i == j || (j < i && is_long.get(j).copied().unwrap_or(false)) {
|
||||
continue;
|
||||
}
|
||||
split_pair(segs, &mut splits, i, j);
|
||||
tested += 1;
|
||||
if tested > max_pairs {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(splits)
|
||||
}
|
||||
|
||||
fn build(polys: &[Vec<Vec2>], tol: f64) -> Option<Graph> {
|
||||
build_with(polys, tol, pair_splits)
|
||||
}
|
||||
|
||||
/// `build` with the crossing search injected, so tests can run the old and the new search on the same input.
|
||||
fn build_with(polys: &[Vec<Vec2>], tol: f64, find_splits: impl Fn(&[(Vec2, Vec2)], &Bounds2) -> Option<Vec<Vec<f64>>>) -> Option<Graph> {
|
||||
let mut segs: Vec<(Vec2, Vec2)> = Vec::new();
|
||||
let mut owner: Vec<usize> = Vec::new();
|
||||
for (k, pl) in polys.iter().enumerate() {
|
||||
@@ -96,70 +197,9 @@ fn build(polys: &[Vec<Vec2>], tol: f64) -> Option<Graph> {
|
||||
if segs.is_empty() || segs.len() > MAX_SEGMENTS {
|
||||
return None;
|
||||
}
|
||||
// Split points per segment (parameters), found with a uniform grid.
|
||||
let bb = Bounds2::from_points(segs.iter().flat_map(|(a, b)| [*a, *b]));
|
||||
let n = segs.len();
|
||||
let cells = ((n as f64).sqrt().ceil() as usize).clamp(1, 512);
|
||||
let cw = (bb.width() / cells as f64).max(1e-12);
|
||||
let ch = (bb.height() / cells as f64).max(1e-12);
|
||||
let cell = |p: Vec2| {
|
||||
(
|
||||
((p.x - bb.min.x) / cw).floor().clamp(0.0, (cells - 1) as f64) as usize,
|
||||
((p.y - bb.min.y) / ch).floor().clamp(0.0, (cells - 1) as f64) as usize,
|
||||
)
|
||||
};
|
||||
let mut grid: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
|
||||
let mut long: Vec<usize> = Vec::new();
|
||||
for (i, (a, b)) in segs.iter().enumerate() {
|
||||
let (x0, y0) = cell(a.min(*b));
|
||||
let (x1, y1) = cell(a.max(*b));
|
||||
if (x1 - x0 + 1) * (y1 - y0 + 1) > 4096 {
|
||||
// Very long segment: too many cells to register in; it is tested against every segment below.
|
||||
long.push(i);
|
||||
continue;
|
||||
}
|
||||
for gx in x0..=x1 {
|
||||
for gy in y0..=y1 {
|
||||
grid.entry((gx, gy)).or_default().push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut splits: Vec<Vec<f64>> = vec![vec![0.0, 1.0]; n];
|
||||
let mut tested = std::collections::HashSet::new();
|
||||
// Count every candidate-pair *visit*, including repeats of a pair that shares several grid
|
||||
// cells, not only the distinct pairs actually tested. A long segment replicated across many
|
||||
// cells makes the same pair reappear in each, so a budget on distinct pairs alone leaves the
|
||||
// repeated hashing unbounded (CWE-407).
|
||||
let mut visits: u64 = 0;
|
||||
const MAX_PAIR_VISITS: u64 = 60_000_000;
|
||||
for list in grid.values() {
|
||||
for (k, &i) in list.iter().enumerate() {
|
||||
for &j in list.iter().skip(k + 1) {
|
||||
visits += 1;
|
||||
if visits > MAX_PAIR_VISITS {
|
||||
return None;
|
||||
}
|
||||
if i == j || !tested.insert((i.min(j), i.max(j))) {
|
||||
continue;
|
||||
}
|
||||
split_pair(&segs, &mut splits, i, j);
|
||||
if tested.len() > 20_000_000 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for &i in &long {
|
||||
for j in 0..n {
|
||||
if i == j || !tested.insert((i.min(j), i.max(j))) {
|
||||
continue;
|
||||
}
|
||||
split_pair(&segs, &mut splits, i, j);
|
||||
if tested.len() > 20_000_000 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let splits = find_splits(&segs, &bb)?;
|
||||
let mut g = Graph { pts: Vec::new(), adj: Vec::new(), src: Vec::with_capacity(n), edge_src: HashMap::new() };
|
||||
let mut index: HashMap<(i64, i64), usize> = HashMap::new();
|
||||
let mut vid = |p: Vec2, g: &mut Graph| -> usize {
|
||||
@@ -630,6 +670,187 @@ mod tests {
|
||||
vec![Vec2::new(x0, y0), Vec2::new(x0 + s, y0), Vec2::new(x0 + s, y0 + s), Vec2::new(x0, y0 + s), Vec2::new(x0, y0)]
|
||||
}
|
||||
|
||||
/// The crossing search before the lowest-shared-cell rule: a `HashSet` of tested pairs over a `HashMap` grid.
|
||||
fn pair_splits_oracle(segs: &[(Vec2, Vec2)], bb: &Bounds2, max_visits: u64, max_pairs: usize) -> Option<Vec<Vec<f64>>> {
|
||||
let n = segs.len();
|
||||
let cells = ((n as f64).sqrt().ceil() as usize).clamp(1, 512);
|
||||
let cw = (bb.width() / cells as f64).max(1e-12);
|
||||
let ch = (bb.height() / cells as f64).max(1e-12);
|
||||
let cell = |p: Vec2| {
|
||||
(
|
||||
((p.x - bb.min.x) / cw).floor().clamp(0.0, (cells - 1) as f64) as usize,
|
||||
((p.y - bb.min.y) / ch).floor().clamp(0.0, (cells - 1) as f64) as usize,
|
||||
)
|
||||
};
|
||||
let mut grid: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
|
||||
let mut long: Vec<usize> = Vec::new();
|
||||
for (i, (a, b)) in segs.iter().enumerate() {
|
||||
let (x0, y0) = cell(a.min(*b));
|
||||
let (x1, y1) = cell(a.max(*b));
|
||||
if (x1 - x0 + 1) * (y1 - y0 + 1) > 4096 {
|
||||
long.push(i);
|
||||
continue;
|
||||
}
|
||||
for gx in x0..=x1 {
|
||||
for gy in y0..=y1 {
|
||||
grid.entry((gx, gy)).or_default().push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut splits: Vec<Vec<f64>> = vec![vec![0.0, 1.0]; n];
|
||||
let mut tested = std::collections::HashSet::new();
|
||||
let mut visits: u64 = 0;
|
||||
for list in grid.values() {
|
||||
for (k, &i) in list.iter().enumerate() {
|
||||
for &j in list.iter().skip(k + 1) {
|
||||
visits += 1;
|
||||
if visits > max_visits {
|
||||
return None;
|
||||
}
|
||||
if i == j || !tested.insert((i.min(j), i.max(j))) {
|
||||
continue;
|
||||
}
|
||||
split_pair(segs, &mut splits, i, j);
|
||||
if tested.len() > max_pairs {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for &i in &long {
|
||||
for j in 0..n {
|
||||
if i == j || !tested.insert((i.min(j), i.max(j))) {
|
||||
continue;
|
||||
}
|
||||
split_pair(segs, &mut splits, i, j);
|
||||
if tested.len() > max_pairs {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(splits)
|
||||
}
|
||||
|
||||
struct Rng(u64);
|
||||
impl Rng {
|
||||
fn next(&mut self) -> u64 {
|
||||
self.0 ^= self.0 << 13;
|
||||
self.0 ^= self.0 >> 7;
|
||||
self.0 ^= self.0 << 17;
|
||||
self.0
|
||||
}
|
||||
fn unit(&mut self) -> f64 {
|
||||
(self.next() >> 11) as f64 / (1u64 << 53) as f64
|
||||
}
|
||||
}
|
||||
|
||||
/// Random segments: short ones, some long ones, and some on a coarse lattice (shared ends, overlaps).
|
||||
fn random_segments(r: &mut Rng, n: usize, size: f64, long_every: u64) -> Vec<(Vec2, Vec2)> {
|
||||
let mut out = Vec::new();
|
||||
for _ in 0..n {
|
||||
let a = Vec2::new(r.unit() * size, r.unit() * size);
|
||||
let seg = match r.next() % 4 {
|
||||
0 if long_every > 0 && r.next().is_multiple_of(long_every) => (
|
||||
Vec2::new(r.unit() * size * 0.02, r.unit() * size * 0.02),
|
||||
Vec2::new(size * (0.98 + r.unit() * 0.02), size * (0.98 + r.unit() * 0.02)),
|
||||
),
|
||||
1 => {
|
||||
let q = |r: &mut Rng| (r.next() % 9) as f64 * size / 8.0;
|
||||
(Vec2::new(q(r), q(r)), Vec2::new(q(r), q(r)))
|
||||
}
|
||||
_ => (a, a + Vec2::new((r.unit() - 0.5) * size * 0.1, (r.unit() - 0.5) * size * 0.1)),
|
||||
};
|
||||
if !seg.0.near(seg.1, 1e-9) {
|
||||
out.push(seg);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn sorted_bits(splits: &[Vec<f64>]) -> Vec<Vec<u64>> {
|
||||
splits
|
||||
.iter()
|
||||
.map(|v| {
|
||||
let mut v = v.clone();
|
||||
v.sort_by(f64::total_cmp);
|
||||
v.iter().map(|x| x.to_bits()).collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn bounds_of(segs: &[(Vec2, Vec2)]) -> Bounds2 {
|
||||
Bounds2::from_points(segs.iter().flat_map(|(a, b)| [*a, *b]))
|
||||
}
|
||||
|
||||
fn assert_same_search(segs: &[(Vec2, Vec2)], max_visits: u64, max_pairs: usize) {
|
||||
let bb = bounds_of(segs);
|
||||
let old = pair_splits_oracle(segs, &bb, max_visits, max_pairs);
|
||||
let new = pair_splits_limited(segs, &bb, max_visits, max_pairs);
|
||||
assert_eq!(old.as_deref().map(sorted_bits), new.as_deref().map(sorted_bits), "n={} visits<={max_visits} pairs<={max_pairs}", segs.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pair_search_matches_oracle_with_budgets() {
|
||||
// Small inputs in few cells: many pairs share several cells. Sweeping each budget over
|
||||
// 0..=400 crosses its exhaustion point for most seeds, including the exact boundary.
|
||||
for seed in 1..=150u64 {
|
||||
let mut r = Rng(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1);
|
||||
let n = 2 + (r.next() % 28) as usize;
|
||||
let segs = random_segments(&mut r, n, 100.0, 0);
|
||||
if segs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
for v in 0..=400u64 {
|
||||
assert_same_search(&segs, v, usize::MAX);
|
||||
}
|
||||
for p in 0..=400usize {
|
||||
assert_same_search(&segs, u64::MAX, p);
|
||||
}
|
||||
assert_same_search(&segs, 60_000_000, 20_000_000);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pair_search_matches_oracle_with_long_segments() {
|
||||
// Around 5000 segments give a 71x71 grid, so a diagonal covers more than 4096 cells and takes the long path.
|
||||
for seed in 1..=3u64 {
|
||||
let mut r = Rng(seed.wrapping_mul(0xD1B5_4A32_D192_ED03) | 1);
|
||||
let n = 5_000 + (r.next() % 500) as usize;
|
||||
let segs = random_segments(&mut r, n, 1000.0, 40);
|
||||
let bb = bounds_of(&segs);
|
||||
let cells = ((segs.len() as f64).sqrt().ceil() as usize).clamp(1, 512);
|
||||
assert!(cells >= 64, "grid too coarse to reach the long path");
|
||||
assert_same_search(&segs, 60_000_000, 20_000_000);
|
||||
// Budgets exhausted in the cell phase (visits, then pairs).
|
||||
assert_same_search(&segs, 3_000, 20_000_000);
|
||||
assert_same_search(&segs, 60_000_000, 200);
|
||||
assert!(pair_splits_limited(&segs, &bb, 3_000, 20_000_000).is_none());
|
||||
assert!(pair_splits_limited(&segs, &bb, 60_000_000, 200).is_none());
|
||||
let (cw, ch) = ((bb.width() / cells as f64).max(1e-12), (bb.height() / cells as f64).max(1e-12));
|
||||
let span = |a: f64, b: f64, o: f64, w: f64| {
|
||||
(((b.max(a) - o) / w).floor().min((cells - 1) as f64) - ((b.min(a) - o) / w).floor().max(0.0) + 1.0) as usize
|
||||
};
|
||||
let long = segs.iter().filter(|(a, b)| span(a.x, b.x, bb.min.x, cw) * span(a.y, b.y, bb.min.y, ch) > 4096).count();
|
||||
assert!(long > 0, "seed {seed}: no segment takes the long path");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_matches_oracle_on_random_drawings() {
|
||||
for seed in 1..=60u64 {
|
||||
let mut r = Rng(seed.wrapping_mul(0xA076_1D64_78BD_642F) | 1);
|
||||
let n = 5 + (r.next() % 400) as usize;
|
||||
let polys: Vec<Vec<Vec2>> = random_segments(&mut r, n, 100.0, 0).into_iter().map(|(a, b)| vec![a, b]).collect();
|
||||
let old = build_with(&polys, 1e-6, |s, bb| pair_splits_oracle(s, bb, MAX_PAIR_VISITS, MAX_PAIRS_TESTED));
|
||||
let new = build(&polys, 1e-6);
|
||||
let (Some(old), Some(new)) = (old, new) else { panic!("seed {seed}: build failed") };
|
||||
assert_eq!(old.pts, new.pts, "seed {seed}");
|
||||
assert_eq!(old.adj, new.adj, "seed {seed}");
|
||||
assert_eq!(old.src, new.src, "seed {seed}");
|
||||
assert_eq!(old.edge_src, new.edge_src, "seed {seed}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_segment_crossing_in_unsampled_cell_is_split() {
|
||||
// A diagonal long enough to skip the grid, and a short segment crossing it in a cell the old coarse sampling missed.
|
||||
|
||||
Reference in New Issue
Block a user