diff --git a/.gitignore b/.gitignore index f8b85dbf..39f77076 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ debug/ target/ +/AGENTS.md + # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock diff --git a/iOverlay/Cargo.toml b/iOverlay/Cargo.toml index 696ea793..9234f115 100644 --- a/iOverlay/Cargo.toml +++ b/iOverlay/Cargo.toml @@ -12,15 +12,14 @@ readme = "README.md" categories = ["algorithms", "graphics", "science::geo", "mathematics", "no-std"] [dependencies] -i_tree = { version = "^0.19.0" } -i_key_sort = { version = "^0.11.0" } +i_tree = { version = "^0.19" } +i_key_sort = { version = "^0.11" } i_float = { version = "^5.0.0"} i_shape = { version = "^5.0.0"} #i_float = { path = "../../iFloat"} #i_shape = { path = "../../iShape"} -#i_tree = { path = "../../iTree" } #i_key_sort = { path = "../../iKeySort" } rayon = { optional = true, version = "^1.10" } diff --git a/iOverlay/src/split/grid_layout.rs b/iOverlay/src/split/grid_layout.rs index 01813f12..e7999c88 100644 --- a/iOverlay/src/split/grid_layout.rs +++ b/iOverlay/src/split/grid_layout.rs @@ -40,7 +40,7 @@ impl FragmentBuffer { let i0 = self.layout.index(s.a.x); if s.a.x < s.b.x { let i1 = self.layout.index(s.b.x - I::ONE); - for count in counts.iter_mut().take(i1).skip(i0) { + for count in counts.iter_mut().take(i1 + 1).skip(i0) { *count += 1; } } else { @@ -304,11 +304,68 @@ mod tests { use crate::geom::x_segment::XSegment; use crate::split::grid_layout::vec; use crate::split::grid_layout::{FragmentBuffer, GridLayout}; + use alloc::vec::Vec; use i_float::int::point::IntPoint; use i_float::int::rect::IntRect; use i_float::triangle::Triangle; use rand::RngExt; + fn check_fragment_reservation() { + // Column width is two. Include one column, several columns, exact + // endpoints, both slopes, and verticals on and off a border. + for (edge, bins, border) in [ + ([0, 1, 1, 1], vec![0], false), + ([1, 1, 2, 1], vec![0], false), + ([1, 0, 2, 3], vec![0], false), + ([0, 1, 8, 1], vec![0, 1, 2, 3], false), + ([1, 0, 9, 5], vec![0, 1, 2, 3, 4], false), + ([1, 5, 9, 0], vec![0, 1, 2, 3, 4], false), + ([0, 0, 0, 3], vec![0], false), + ([3, 0, 3, 3], vec![1], false), + ([4, 0, 4, 3], vec![2], true), + ([12, 0, 12, 3], vec![6], true), + ] { + let [ax, ay, bx, by] = edge.map(I::from_usize); + let segment = XSegment { + a: IntPoint::new(ax, ay), + b: IntPoint::new(bx, by), + }; + let mut buffer = FragmentBuffer::new(GridLayout { + min_x: I::ZERO, + max_x: I::from_usize(12), + power: 1, + }); + for count in [37, 113] { + buffer.init_fragment_buffer(core::iter::repeat_n(segment, count)); + let capacities: Vec<_> = buffer.groups.iter().map(Vec::capacity).collect(); + for &bin in &bins { + assert!(capacities[bin] >= count); + } + for index in 0..count { + buffer.add_segment(index, segment); + } + for (bin, group) in buffer.groups.iter().enumerate() { + assert_eq!(group.len(), if bins.contains(&bin) { count } else { 0 }); + assert_eq!(group.capacity(), capacities[bin]); + } + assert_eq!(buffer.on_border.len(), if border { count } else { 0 }); + buffer.clear(); + assert!(buffer.on_border.is_empty()); + assert!(buffer.groups.iter().all(Vec::is_empty)); + } + } + } + + #[test] + fn fragment_reservation_i32() { + check_fragment_reservation::(); + } + + #[test] + fn fragment_reservation_i64() { + check_fragment_reservation::(); + } + #[test] fn i64_column_positions_with_large_shift() { let layout = GridLayout { diff --git a/iOverlay/src/split/solver_fragment.rs b/iOverlay/src/split/solver_fragment.rs index 266afbee..46be0599 100644 --- a/iOverlay/src/split/solver_fragment.rs +++ b/iOverlay/src/split/solver_fragment.rs @@ -144,10 +144,10 @@ where return false; } - if self.marks.capacity() < size { - let additional = size - self.marks.capacity(); - self.marks.reserve(additional); - } + self.marks.reserve(size); + + #[cfg(test)] + assert!(self.marks.capacity() - self.marks.len() >= size); for mut result in results.into_iter() { self.marks.append(&mut result.marks); @@ -259,6 +259,70 @@ mod tests { use alloc::vec::Vec; use i_float::int::point::IntPoint; + #[cfg(feature = "allow_multithreading")] + fn check_parallel_mark_merge() { + use crate::split::grid_layout::FragmentBuffer; + use i_float::int::number::uint::UIntNumber; + + let mut splitter = SplitSolver::::new(); + splitter.marks.reserve(64); + // Derive the input from the actual capacity, without assuming the + // allocator gives exactly the requested amount. The merge must reserve + // all marks before append, also when a reused empty Vec is too small. + let count = splitter.marks.capacity() * 3 / 4; + for count in [count, count * 3] { + splitter.marks.clear(); + let mut edges = Vec::new(); + for y in 1..=count { + edges.push(XSegment { + a: IntPoint::new(I::ZERO, I::from_usize(y)), + b: IntPoint::new(I::from_usize(100), I::from_usize(y)), + }); + } + edges.push(XSegment { + a: IntPoint::new(I::from_usize(25), I::ZERO), + b: IntPoint::new(I::from_usize(25), I::from_usize(count + 1)), + }); + let mut buffer = + FragmentBuffer::new(GridLayout::new(edges.iter().copied(), edges.len()).unwrap()); + buffer.init_fragment_buffer(edges.iter().copied()); + for (index, edge) in edges.into_iter().enumerate() { + buffer.add_segment(index, edge); + } + splitter.parallel_split(I::WideUInt::ONE, &mut buffer); + assert_eq!(splitter.marks.len(), 2 * count); + splitter + .marks + .sort_unstable_by_key(|mark| (mark.index, mark.point)); + for (index, mark) in splitter.marks[..count].iter().enumerate() { + assert_eq!(mark.index, index); + assert_eq!( + mark.point, + IntPoint::new(I::from_usize(25), I::from_usize(index + 1)) + ); + } + for (index, mark) in splitter.marks[count..].iter().enumerate() { + assert_eq!(mark.index, count); + assert_eq!( + mark.point, + IntPoint::new(I::from_usize(25), I::from_usize(index + 1)) + ); + } + } + } + + #[cfg(feature = "allow_multithreading")] + #[test] + fn parallel_mark_merge_i32() { + check_parallel_mark_merge::(); + } + + #[cfg(feature = "allow_multithreading")] + #[test] + fn parallel_mark_merge_i64() { + check_parallel_mark_merge::(); + } + #[test] fn collinear_overlap_starts_in_a_later_column() { for (start, end, far_end) in [(80, 100, 150), (180, 300, 400)] { diff --git a/iOverlay/src/split/solver_tree.rs b/iOverlay/src/split/solver_tree.rs index 57f923d7..578b0248 100644 --- a/iOverlay/src/split/solver_tree.rs +++ b/iOverlay/src/split/solver_tree.rs @@ -64,8 +64,13 @@ where for (i, si) in segments.iter().enumerate() { let time = si.x_segment.a.x; - let si_range = si.x_segment.y_range().into(); + let si_y_range = si.x_segment.y_range(); + let si_range = si_y_range.clone().into(); for sj in tree.iter_by_range(si_range, time) { + if sj.x_segment.is_not_intersect_y_range(&si_y_range) { + continue; + } + let (this_index, scan_index, this, scan) = if si.x_segment < sj.x_segment { (i, sj.id, &si.x_segment, &sj.x_segment) } else { diff --git a/iOverlay/tests/tree_y_range_tests.rs b/iOverlay/tests/tree_y_range_tests.rs new file mode 100644 index 00000000..70d2c625 --- /dev/null +++ b/iOverlay/tests/tree_y_range_tests.rs @@ -0,0 +1,45 @@ +use i_float::int::point::IntPoint; +use i_overlay::core::fill_rule::FillRule; +use i_overlay::core::integer::OverlayInt; +use i_overlay::core::overlay::Overlay; +use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::Solver; + +fn check_tree_y_ranges + std::fmt::Debug>() { + let point = |x: i32, y: i32| IntPoint::::new(x.into(), y.into()); + let rect = |x0, y0, x1, y1| vec![point(x0, y0), point(x0, y1), point(x1, y1), point(x1, y0)]; + // The distant contour puts all nearby, disjoint Y bands in one + // coarse tree cell, while their X ranges overlap. + let mut subj = vec![rect(0, 0, 40, 10), rect(0, 30, 40, 40)]; + let mut clip = vec![rect(10, 10, 30, 20), rect(40, 2, 50, 8)]; + for y in (60..300).step_by(20) { + subj.push(rect(0, y, 40, y + 4)); + clip.push(rect(10, y + 8, 30, y + 12)); + } + subj.push(rect(0, 1_000_000, 40, 1_000_010)); + // Partial horizontal and vertical shared edges above require + // splitting at boundary Y values. These oblique crossings also + // exercise rounding and subsequent split passes. + clip.push(vec![point(-3, 29), point(43, 37), point(17, 43)]); + + for fill in [FillRule::EvenOdd, FillRule::NonZero] { + for rule in [OverlayRule::Union, OverlayRule::Intersect, OverlayRule::Xor] { + let list = Overlay::from_subj_and_clip_custom(&subj, &clip, Default::default(), Solver::LIST) + .overlay(rule, fill); + let tree = Overlay::from_subj_and_clip_custom(&subj, &clip, Default::default(), Solver::TREE) + .overlay(rule, fill); + assert!(!list.is_empty()); + assert_eq!(tree, list, "{rule:?}, {fill:?}"); + } + } +} + +#[test] +fn tree_y_ranges_i32() { + check_tree_y_ranges::(); +} + +#[test] +fn tree_y_ranges_i64() { + check_tree_y_ranges::(); +} diff --git a/performance/rust_app/.gitignore b/performance/rust_app/.gitignore new file mode 100644 index 00000000..0fc402cb --- /dev/null +++ b/performance/rust_app/.gitignore @@ -0,0 +1,3 @@ +# This executable pins its dependency resolution independently of the library. +!Cargo.lock +results/ diff --git a/performance/rust_app/Cargo.lock b/performance/rust_app/Cargo.lock new file mode 100644 index 00000000..743472b6 --- /dev/null +++ b/performance/rust_app/Cargo.lock @@ -0,0 +1,111 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "i_float" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecaff1ce29bcacd72cdf581ba6fce636ef4c4e0b2da7e7846dd0137d98141e2" +dependencies = [ + "libm", +] + +[[package]] +name = "i_key_sort" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6c58d0c60705e66264ce0f788a69a2f21472aeb8188559e7c8c619dbdc10fa" +dependencies = [ + "rayon", +] + +[[package]] +name = "i_overlay" +version = "9.0.1" +dependencies = [ + "i_float", + "i_key_sort", + "i_shape", + "i_tree", + "rayon", +] + +[[package]] +name = "i_shape" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa1878f91ef7921feaf1b2346253ebc5e661878b22d5715195c1d5dcf0f20ae5" +dependencies = [ + "i_float", +] + +[[package]] +name = "i_tree" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180cbc5c4af5d8f57b8096c3cf39efdac3642ece00ca54710db44db3536baed4" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rust_app" +version = "0.2.0" +dependencies = [ + "i_overlay", +] diff --git a/performance/rust_app/Cargo.toml b/performance/rust_app/Cargo.toml index efe8ac11..08c0dde2 100644 --- a/performance/rust_app/Cargo.toml +++ b/performance/rust_app/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "rust_app" -version = "0.1.0" +version = "0.2.0" edition = "2021" +rust-version = "1.88" +default-run = "rust_app" [profile.release] opt-level = 3 lto = false codegen-units = 1 -[dependencies] +[features] +default = [] +multithreading = ["i_overlay/allow_multithreading"] -#i_overlay = { path = "../../iOverlay", default-features = true } -#i_overlay = { path = "../../iOverlay", features = ["allow_multithreading"] } -#i_tree = { path = "../../../iTree" } -i_overlay = "7.0.0" -i_key_sort = "^0.10.1" -i_tree = "^0.19.0" \ No newline at end of file +[dependencies] +i_overlay = { path = "../../iOverlay", version = "^9" } diff --git a/performance/rust_app/build.rs b/performance/rust_app/build.rs new file mode 100644 index 00000000..6af87985 --- /dev/null +++ b/performance/rust_app/build.rs @@ -0,0 +1,166 @@ +use std::{ + env, fs, + path::Path, + process::Command, + time::{SystemTime, UNIX_EPOCH}, +}; + +fn command(program: &str, args: &[&str]) -> String { + Command::new(program) + .args(args) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_else(|| "unknown".into()) +} +fn hash_files(root: &Path, hash: &mut u64) { + let mut paths: Vec<_> = fs::read_dir(root) + .unwrap() + .map(|e| e.unwrap().path()) + .collect(); + paths.sort(); + for path in paths { + if path.is_dir() { + hash_files(&path, hash); + } else if path.extension().is_some_and(|e| e == "rs") + || path + .file_name() + .is_some_and(|n| n == "Cargo.toml" || n == "Cargo.lock") + { + // Include relative traversal names and delimiters as well as contents. + for byte in path + .file_name() + .unwrap() + .to_string_lossy() + .bytes() + .chain([0]) + .chain(fs::read(&path).unwrap()) + .chain([0]) + { + *hash = (*hash ^ u64::from(byte)).wrapping_mul(0x100000001b3); + } + } + } +} +fn emit(key: &str, value: impl std::fmt::Display) { + println!("cargo:rustc-env={key}={value}"); +} +fn main() { + let manifest = env::var("CARGO_MANIFEST_DIR").unwrap(); + let root = Path::new(&manifest); + let lib = root.join("../../iOverlay").canonicalize().unwrap(); + for path in [ + root.join("src"), + root.join("Cargo.toml"), + root.join("Cargo.lock"), + root.join("build.rs"), + lib.join("src"), + lib.join("Cargo.toml"), + ] { + println!("cargo:rerun-if-changed={}", path.display()); + } + for git_path in ["HEAD", "index", "packed-refs"] { + let path = command( + "git", + &[ + "-C", + lib.to_str().unwrap(), + "rev-parse", + "--path-format=absolute", + "--git-path", + git_path, + ], + ); + if path != "unknown" { + println!("cargo:rerun-if-changed={path}"); + } + } + let branch_ref = command( + "git", + &["-C", lib.to_str().unwrap(), "symbolic-ref", "-q", "HEAD"], + ); + if branch_ref != "unknown" { + let path = command( + "git", + &[ + "-C", + lib.to_str().unwrap(), + "rev-parse", + "--path-format=absolute", + "--git-path", + &branch_ref, + ], + ); + if path != "unknown" { + println!("cargo:rerun-if-changed={path}"); + } + } + let text = fs::read_to_string(lib.join("Cargo.toml")).unwrap(); + let version = text + .lines() + .find_map(|s| s.strip_prefix("version = ")) + .unwrap() + .trim_matches('"'); + emit("BENCH_LIBRARY_VERSION", version); + let mut hash = 0xcbf29ce484222325; + hash_files(&lib.join("src"), &mut hash); + for byte in text.bytes() { + hash = (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3); + } + emit("BENCH_LIBRARY_SOURCE", format!("{hash:016x}")); + let mut hash = 0xcbf29ce484222325; + hash_files(&root.join("src"), &mut hash); + for name in ["Cargo.toml", "Cargo.lock", "build.rs"] { + for byte in fs::read(root.join(name)).unwrap() { + hash = (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3); + } + } + emit("BENCH_RUNNER_SOURCE", format!("{hash:016x}")); + emit( + "BENCH_RUSTC", + command(&env::var("RUSTC").unwrap(), &["--version"]), + ); + emit("BENCH_TARGET", env::var("TARGET").unwrap()); + emit("BENCH_PROFILE", env::var("PROFILE").unwrap()); + emit("BENCH_OPT_LEVEL", env::var("OPT_LEVEL").unwrap()); + emit("BENCH_DEBUG", env::var("DEBUG").unwrap()); + emit( + "BENCH_RUSTFLAGS", + env::var("CARGO_ENCODED_RUSTFLAGS") + .unwrap_or_default() + .replace('\x1f', " "), + ); + emit( + "BENCH_BUILD_UNIX", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + ); + emit( + "BENCH_LIBRARY_REV", + command("git", &["-C", lib.to_str().unwrap(), "rev-parse", "HEAD"]), + ); + let status = command( + "git", + &[ + "-C", + lib.to_str().unwrap(), + "status", + "--porcelain", + "--", + ".", + ], + ); + emit( + "BENCH_LIBRARY_DIRTY", + if status == "unknown" { + "unknown" + } else if status.is_empty() { + "false" + } else { + "true" + }, + ); +} diff --git a/performance/rust_app/src/config.rs b/performance/rust_app/src/config.rs new file mode 100644 index 00000000..13acb2af --- /dev/null +++ b/performance/rust_app/src/config.rs @@ -0,0 +1,320 @@ +use std::{collections::BTreeMap, time::Duration}; + +#[derive(Debug)] +pub struct Config { + pub tests: Vec, + pub counts: Vec, + pub seed: u64, + pub strategies: Vec, + pub engines: Vec, + pub modes: Vec, + pub multithreading: bool, + pub label: String, + pub warmup: usize, + pub samples: usize, + pub iterations: usize, + pub budget: Option, + pub complex: bool, + pub orientation: String, + pub outlier: String, + pub spiral: String, + pub layout: String, +} +impl Config { + pub fn variants(&self, id: usize) -> Vec<&str> { + let (value, all): (&str, &[&str]) = match id { + 1 => (&self.layout, &["grid", "line"]), + 3 => (&self.spiral, &["float", "integer"]), + 9 => (&self.orientation, &["horizontal", "vertical"]), + 12 => (&self.outlier, &["cluster", "outlier"]), + _ => return vec!["default"], + }; + if value == "all" { + all.to_vec() + } else { + vec![value] + } + } +} +const HELP: &str = "iOverlay numbered performance tests (release)\n\ +--test 0..13|all (comma lists; default all) --count 8 (comma lists)\n\ +--complex false|true (true uses 2,4,8,16,32,64, caps quadratic tests)\n\ +--strategy auto|list|tree|frag|all (default all) --int i16|i32|i64|all (default i32)\n\ +--mode cold|reuse|all --multithreading false|true (requires feature multithreading)\n\ +--seed 0 --warmup 2 --samples 7 --iterations 3 --budget-seconds 30 (0 disables budget) --label local\n\ +--orientation horizontal|vertical|all (test 9; default all)\n\ +--outlier false|true|all (test 12; default all)\n\ +--spiral float|integer|all (test 3; default float)\n\ +--layout grid|line|all (test 1; default grid)\n\ +--list lists numbered tests. stdout is CSV; diagnostics are on stderr.\n"; + +pub fn parse(args: impl Iterator) -> Result, String> { + let mut args = args.peekable(); + let mut map = BTreeMap::new(); + while let Some(arg) = args.next() { + if arg == "--help" || arg == "-h" { + eprint!("{HELP}"); + return Ok(None); + } + if arg == "--list" { + for (id, name) in crate::test::NAMES.iter().enumerate() { + eprintln!("test_{id}_{name}"); + } + return Ok(None); + } + if arg == "--extended" { + map.insert("complex".to_string(), "true".to_string()); + continue; + } + let key = arg + .strip_prefix("--") + .or_else(|| arg.strip_prefix('-')) + .ok_or_else(|| format!("expected --option: {arg}"))?; + let key = if key == "size" { "count" } else { key }; + if ![ + "test", + "count", + "complex", + "seed", + "strategy", + "int", + "mode", + "multithreading", + "warmup", + "samples", + "iterations", + "budget-seconds", + "label", + "orientation", + "outlier", + "spiral", + "layout", + ] + .contains(&key) + { + return Err(format!("unknown option: {arg}")); + } + let value = if ["complex", "multithreading"].contains(&key) + && args.peek().is_none_or(|s| s.starts_with('-')) + { + "true".into() + } else { + args.next() + .filter(|s| !s.starts_with('-')) + .ok_or_else(|| format!("missing value for {arg}"))? + }; + if map.insert(key.to_string(), value).is_some() { + return Err(format!("duplicate option: {arg}")); + } + } + let get = |key: &str, default: &str| map.get(key).cloned().unwrap_or_else(|| default.into()); + let choices = |key: &str, default: &str, allowed: &[&str]| -> Result, String> { + let value = get(key, default); + let result: Vec = if value == "all" { + allowed.iter().map(|s| s.to_string()).collect() + } else { + value.split(',').map(str::to_string).collect() + }; + if result.iter().any(|s| !allowed.contains(&s.as_str())) { + return Err(format!("invalid {key}: {value}")); + } + let mut unique = result.clone(); + unique.sort(); + unique.dedup(); + if unique.len() != result.len() { + return Err(format!("duplicate {key} values")); + } + Ok(result) + }; + let number = |key: &str, default: &str, min: usize, max: usize| -> Result { + let n = get(key, default) + .parse::() + .map_err(|_| format!("invalid {key}"))?; + if !(min..=max).contains(&n) { + return Err(format!("{key} must be in {min}..={max}")); + } + Ok(n) + }; + let complex = get("complex", "false") + .parse::() + .map_err(|_| "invalid complex boolean".to_string())?; + let tests = if get("test", "all") == "all" { + (0..crate::test::NAMES.len()).collect() + } else { + get("test", "all") + .split(',') + .map(|s| { + s.parse::() + .map_err(|_| "invalid test number".to_string()) + }) + .collect::, _>>()? + }; + if tests.iter().any(|&id| id >= crate::test::NAMES.len()) { + return Err("test must be in 0..=13".into()); + } + let mut unique = tests.clone(); + unique.sort(); + unique.dedup(); + if unique.len() != tests.len() { + return Err("duplicate test numbers".into()); + } + let counts = if complex { + vec![2, 4, 8, 16, 32, 64] + } else { + get("count", "8") + .split(',') + .map(|s| s.parse::().map_err(|_| "invalid count".to_string())) + .collect::, _>>()? + }; + if counts.iter().any(|n| !(1..=4096).contains(n)) { + return Err("count must be in 1..=4096".into()); + } + let seed = get("seed", "0"); + let seed = if let Some(s) = seed.strip_prefix("0x") { + u64::from_str_radix(s, 16) + } else { + seed.parse() + } + .map_err(|_| "invalid seed".to_string())?; + let multithreading = get("multithreading", "false") + .parse::() + .map_err(|_| "invalid multithreading boolean".to_string())?; + if multithreading && !cfg!(feature = "multithreading") { + return Err("multithreading=true requires --features multithreading".into()); + } + let mut engines = choices("int", "i32", &["i16", "i32", "i64", "16", "32", "64"])?; + if get("int", "i32") == "all" { + engines = vec!["i16".into(), "i32".into(), "i64".into()]; + } + for e in &mut engines { + if !e.starts_with('i') { + *e = format!("i{e}"); + } + } + let mut engine_unique = engines.clone(); + engine_unique.sort(); + engine_unique.dedup(); + if engine_unique.len() != engines.len() { + return Err("duplicate integer engines".into()); + } + let single = |key: &str, default: &str, allowed: &[&str]| -> Result { + let v = get(key, default); + if v != "all" && !allowed.contains(&v.as_str()) { + return Err(format!("invalid {key}: {v}")); + } + Ok(v) + }; + let outlier = single("outlier", "all", &["false", "true"])?; + let outlier = match outlier.as_str() { + "false" => "cluster".into(), + "true" => "outlier".into(), + _ => outlier, + }; + let config = Config { + tests, + counts, + seed, + strategies: choices("strategy", "all", &["auto", "list", "tree", "frag"])?, + engines, + modes: choices("mode", "all", &["cold", "reuse"])?, + multithreading, + label: get("label", "local"), + warmup: number("warmup", "2", 1, 100)?, + samples: number("samples", "7", 3, 101)?, + iterations: number("iterations", "3", 1, 10000)?, + budget: match number("budget-seconds", "30", 0, 3600)? { + 0 => None, + seconds => Some(Duration::from_secs(seconds as u64)), + }, + complex, + orientation: single("orientation", "all", &["horizontal", "vertical"])?, + outlier, + spiral: single("spiral", "float", &["float", "integer"])?, + layout: single("layout", "grid", &["grid", "line"])?, + }; + if !complex { + for &id in &config.tests { + for variant in config.variants(id) { + for &n in &config.counts { + if n > crate::test::max_count(id, variant) { + return Err(format!( + "test {id} {variant}: count exceeds {}", + crate::test::max_count(id, variant) + )); + } + } + } + } + } + Ok(Some(config)) +} +#[cfg(test)] +mod checks { + use super::*; + fn p(args: &[&str]) -> Result, String> { + parse(args.iter().map(|s| s.to_string())) + } + #[test] + fn original_numeric_cli() { + let c = p(&[ + "--test", + "0", + "--count", + "32", + "--complex", + "false", + "--multithreading", + "false", + "--int", + "all", + ]) + .unwrap() + .unwrap(); + assert_eq!(c.tests, vec![0]); + assert_eq!(c.counts, vec![32]); + assert_eq!(c.engines, vec!["i16", "i32", "i64"]); + assert_eq!( + p(&["--test", "3"]).unwrap().unwrap().variants(3), + vec!["float"] + ); + assert_eq!( + p(&["--test", "1"]).unwrap().unwrap().variants(1), + vec!["grid"] + ); + assert_eq!( + p(&["-test", "9", "-count", "8", "-int", "32"]) + .unwrap() + .unwrap() + .engines, + vec!["i32"] + ); + } + #[test] + fn limits_and_variants() { + assert!(p(&["--budget-seconds", "0"]) + .unwrap() + .unwrap() + .budget + .is_none()); + assert_eq!( + p(&[]).unwrap().unwrap().budget, + Some(Duration::from_secs(30)) + ); + assert!(p(&["--test", "14"]).is_err()); + assert!(p(&["--test"]).is_err()); + assert!(p(&["--count", "0"]).is_err()); + assert!(p(&["--samples", "2"]).is_err()); + assert!(p(&["--test", "2", "--count", "65"]).is_err()); + assert_eq!( + p(&["--test", "9"]).unwrap().unwrap().variants(9), + vec!["horizontal", "vertical"] + ); + assert_eq!( + p(&["--test", "12", "--outlier", "true"]) + .unwrap() + .unwrap() + .variants(12), + vec!["outlier"] + ); + } +} diff --git a/performance/rust_app/src/main.rs b/performance/rust_app/src/main.rs index eccd745f..0afbf95c 100644 --- a/performance/rust_app/src/main.rs +++ b/performance/rust_app/src/main.rs @@ -12,6 +12,7 @@ use i_overlay::core::solver::{MultithreadOptions, Precision, Solver, Strategy}; use std::collections::HashMap; use std::env; +mod config; mod test; #[derive(Clone, Copy)] @@ -49,10 +50,10 @@ fn main() { while let Some(arg) = args_iter.next() { if let Some(key) = arg.strip_prefix("--").or_else(|| arg.strip_prefix('-')) { // If the next argument is also a key, store a boolean flag; otherwise, store the value. - let value = if args_iter.peek().map_or(false, |a| a.starts_with('-')) { + let value = if args_iter.peek().is_some_and(|a| a.starts_with('-')) { "true".to_string() } else { - args_iter.next().unwrap() + args_iter.next().unwrap_or_else(|| "true".to_string()) }; args_map.insert(key.to_owned(), value); } @@ -70,6 +71,23 @@ fn main() { } } + if args_map + .get("benchmark") + .is_some_and(|value| value == "true") + { + let args = args_map + .iter() + .filter(|(key, _)| key.as_str() != "benchmark") + .flat_map(|(key, value)| [format!("--{key}"), value.clone()]); + let result = + config::parse(args).and_then(|c| c.map(test::runner::run_benchmark).unwrap_or(Ok(()))); + if let Err(error) = result { + eprintln!("error: {error}"); + std::process::exit(2); + } + return; + } + let test_key = args_map.get("test").expect("Test number is not set"); let multithreading_key = args_map .get("multithreading") @@ -95,7 +113,17 @@ fn main() { }; let solver = Solver { - strategy: Strategy::Auto, + strategy: match args_map + .get("strategy") + .map(String::as_str) + .unwrap_or("auto") + { + "auto" => Strategy::Auto, + "list" => Strategy::List, + "tree" => Strategy::Tree, + "frag" => Strategy::Frag, + value => panic!("Unknown strategy: {value}. Use auto, list, tree, or frag"), + }, precision: Precision::HIGH, multithreading, }; @@ -155,6 +183,11 @@ fn run_selected_test_with_int( 8 => { run_test_8::(solver); } + 9..=13 => { + for count in [2, 4, 8, 16, 32, 64] { + test::runner::run_selected::(test, count, args_map, solver); + } + } _ => { println!("Test is not found"); } @@ -192,6 +225,9 @@ fn run_selected_test_with_int( 8 => { WindMillTest::run::(count, OverlayRule::Intersect, solver, 1.0); } + 9..=13 => { + test::runner::run_selected::(test, count, args_map, solver); + } _ => { println!("Test is not found"); } @@ -252,7 +288,7 @@ fn run_test_6(solver: Solver) { let mut n = 1; for _ in 1..11 { CorrosionTest::run::(n, OverlayRule::Difference, solver, 100.0); - n = n << 1; + n <<= 1; } } @@ -261,7 +297,7 @@ fn run_test_7(solver: Solver) { let mut n = 1; for _ in 1..11 { ConcentricTest::run::(n, OverlayRule::Intersect, solver, 100.0); - n = n << 1; + n <<= 1; } } @@ -270,7 +306,7 @@ fn run_test_8(solver: Solver) { let mut n = 1; for _ in 1..11 { WindMillTest::run::(n, OverlayRule::Difference, solver, 100.0); - n = n << 1; + n <<= 1; } WindMillTest::validate::(100, OverlayRule::Difference, solver); } diff --git a/performance/rust_app/src/test/data.rs b/performance/rust_app/src/test/data.rs new file mode 100644 index 00000000..c1dd8b9e --- /dev/null +++ b/performance/rust_app/src/test/data.rs @@ -0,0 +1,135 @@ +use super::runner::Engine; +use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::i_shape::int::path::IntPaths; +pub type Point = (i64, i64); +pub type Path = Vec; + +#[derive(Clone, Debug)] +pub struct Fixture { + pub subj: Vec, + pub clip: Vec, + pub rule: OverlayRule, +} + +pub fn rect(x: i64, y: i64, w: i64, h: i64) -> Path { + vec![(x, y), (x + w, y), (x + w, y + h), (x, y + h)] +} + +pub fn rotate(paths: &mut [Path]) { + for path in paths { + for (x, y) in path { + (*x, *y) = (-*y, *x); + } + } +} + +impl Fixture { + pub fn edges(&self) -> usize { + self.subj.iter().chain(&self.clip).map(Vec::len).sum() + } + + pub fn bounds(&self) -> (i64, i64, i64, i64) { + let mut b = (i64::MAX, i64::MAX, i64::MIN, i64::MIN); + for &(x, y) in self.subj.iter().chain(&self.clip).flatten() { + b = (b.0.min(x), b.1.min(y), b.2.max(x), b.3.max(y)); + } + b + } + + // FNV-1a over domain-separated little-endian u64 lengths and i64 coordinates. + // Contour order is intentionally significant for the INPUT fingerprint. + pub fn fingerprint(&self) -> u64 { + let mut hash = 0xcbf29ce484222325u64; + for paths in [&self.subj, &self.clip] { + hash_word(&mut hash, paths.len() as u64); + for path in paths { + hash_word(&mut hash, path.len() as u64); + for &(x, y) in path { + hash_word(&mut hash, x as u64); + hash_word(&mut hash, y as u64); + } + } + } + hash + } +} + +fn hash_word(hash: &mut u64, word: u64) { + for byte in word.to_le_bytes() { + *hash = (*hash ^ u64::from(byte)).wrapping_mul(0x100000001b3); + } +} + +/// SplitMix64, including zero seed. All arithmetic explicitly wraps modulo 2^64. +pub struct Prng(pub u64); +impl Prng { + pub fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); + z ^ (z >> 31) + } +} + +impl Fixture { + pub fn from_int(subj: IntPaths, clip: IntPaths, rule: OverlayRule) -> Self { + let cvt = |paths: IntPaths| { + paths + .into_iter() + .map(|p| p.into_iter().map(|p| (p.x, p.y)).collect()) + .collect() + }; + Self { + subj: cvt(subj), + clip: cvt(clip), + rule, + } + } + pub fn from_float( + subj: Vec>, + clip: Vec>, + rule: OverlayRule, + ) -> Self { + use i_overlay::i_float::adapter::FloatPointAdapter; + let adapter = FloatPointAdapter::<[f64; 2], I>::with_iter_conservative( + subj.iter().chain(&clip).flatten(), + ); + let cvt = |paths: Vec>| { + paths + .into_iter() + .map(|p| { + p.iter() + .map(|p| { + let q = adapter.float_to_int(p); + (q.x.to_i64(), q.y.to_i64()) + }) + .collect() + }) + .collect() + }; + Self { + subj: cvt(subj), + clip: cvt(clip), + rule, + } + } +} +#[cfg(test)] +mod checks { + use super::*; + #[test] + fn splitmix64_reference_vector() { + let mut r = Prng(0); + assert_eq!( + (0..5).map(|_| r.next()).collect::>(), + vec![ + 0xe220a8397b1dcdaf, + 0x6e789e6aa1b965f4, + 0x06c45d188009454f, + 0xf88bb8a8724c81ec, + 0x1b39896a51a8749b + ] + ); + } +} diff --git a/performance/rust_app/src/test/mod.rs b/performance/rust_app/src/test/mod.rs index bdac2a59..9454b69f 100644 --- a/performance/rust_app/src/test/mod.rs +++ b/performance/rust_app/src/test/mod.rs @@ -1,4 +1,10 @@ +pub(crate) mod data; +pub(crate) mod runner; pub(crate) mod test_0_checkerboard; +pub(crate) mod test_10_diagonal; +pub(crate) mod test_11_collinear; +pub(crate) mod test_12_cluster; +pub(crate) mod test_13_seeded; pub(crate) mod test_1_not_overlap; pub(crate) mod test_2_lines_net; pub(crate) mod test_3_spiral; @@ -7,4 +13,239 @@ pub(crate) mod test_5_nested_squares; pub(crate) mod test_6_corrosion; pub(crate) mod test_7_concentric; pub(crate) mod test_8_wind_mill; +pub(crate) mod test_9_orientation; pub(crate) mod util; + +pub const NAMES: &[&str] = &[ + "checkerboard", + "not_overlap", + "lines_net", + "spiral", + "windows", + "nested_squares", + "corrosion", + "concentric", + "wind_mill", + "orientation", + "diagonal", + "collinear", + "cluster", + "seeded", +]; + +pub fn max_count(id: usize, variant: &str) -> usize { + match id { + 0 | 2 | 4 | 6 | 8 => 64, + 1 if variant == "grid" => 64, + 7 => 16, + _ => 4096, + } +} +pub fn generate( + id: usize, + n: usize, + seed: u64, + variant: &str, +) -> Result { + if id >= NAMES.len() { + return Err(format!("unknown test: {id}")); + } + if n == 0 || n > max_count(id, variant) { + return Err(format!( + "test {id} {variant}: count must be in 1..={}", + max_count(id, variant) + )); + } + Ok(match id { + 0 => test_0_checkerboard::CheckerboardTest::geometry(n), + 1 => test_1_not_overlap::NotOverlapTest::geometry(n, variant == "line"), + 2 => test_2_lines_net::LinesNetTest::geometry(n), + 3 => test_3_spiral::SpiralTest::geometry::(n, variant == "integer"), + 4 => test_4_windows::WindowsTest::geometry(n), + 5 => test_5_nested_squares::CrossTest::geometry(n), + 6 => test_6_corrosion::CorrosionTest::geometry::(n), + 7 => test_7_concentric::ConcentricTest::geometry::(n), + 8 => test_8_wind_mill::WindMillTest::geometry(n), + 9 => test_9_orientation::OrientationTest::geometry(n, variant == "vertical"), + 10 => test_10_diagonal::DiagonalTest::geometry(n), + 11 => test_11_collinear::CollinearTest::geometry(n), + 12 => test_12_cluster::ClusterTest::geometry(n, seed, variant == "outlier"), + 13 => test_13_seeded::SeededTest::geometry(n, seed), + _ => unreachable!(), + }) +} + +#[cfg(test)] +mod checks { + use super::*; + use i_overlay::core::{fill_rule::FillRule, overlay::Overlay}; + use runner::{area2, canonical, convert, normalize, solver, Engine}; + + fn check() { + for id in 0..NAMES.len() { + let variants: &[&str] = match id { + 1 => &["grid", "line"], + 3 => &["float", "integer"], + 9 => &["horizontal", "vertical"], + 12 => &["cluster", "outlier"], + _ => &["default"], + }; + for variant in variants { + for n in [1, 4, 8] { + let f = generate::(id, n, 0, variant).unwrap(); + let (subj, clip) = convert::(&f).unwrap(); + let expected = canonical( + &Overlay::from_subj_and_clip_custom( + &subj, + &clip, + Default::default(), + solver("list", false), + ) + .overlay(f.rule, FillRule::NonZero), + ); + for strategy in ["auto", "tree", "frag"] { + let mut overlay = Overlay::from_subj_and_clip_custom( + &subj, + &clip, + Default::default(), + solver(strategy, false), + ); + assert_eq!( + canonical(&overlay.overlay(f.rule, FillRule::NonZero)), + expected, + "test {id} {variant} n={n} {strategy} i{}", + I::BITS + ); + overlay.reinit_with_subj_and_clip(&subj, &clip); + assert_eq!( + canonical(&overlay.overlay(f.rule, FillRule::NonZero)), + expected + ); + } + let area = match id { + 1 if *variant == "line" => Some(2 * n as i128 * 64), + 1 => Some((n * n + (n - 1) * (n - 1)) as i128 * 100), + 2 => Some(50 * n as i128 * (2 * n as i128 - 1)), + 4 => Some(n as i128 * n as i128 * 300), + 9 => Some(n as i128 * (96 * n as i128 + 24)), + _ => None, + }; + if let Some(a) = area { + assert_eq!(area2(&expected), 2 * a, "test {id} {variant}"); + } + if id == 8 { + assert_eq!(expected.len(), 4 * n * n); + } + if id == 9 { + let v = generate::( + 9, + n, + 0, + if *variant == "horizontal" { + "vertical" + } else { + "horizontal" + }, + ) + .unwrap(); + let (vs, vc) = convert::(&v).unwrap(); + let actual = canonical( + &Overlay::from_subj_and_clip_custom( + &vs, + &vc, + Default::default(), + solver("auto", false), + ) + .overlay(v.rule, FillRule::NonZero), + ); + let mut turned = expected.clone(); + data::rotate(&mut turned); + if *variant == "vertical" { + data::rotate(&mut turned); + data::rotate(&mut turned); + } + turned = turned.into_iter().map(normalize).collect(); + turned.sort(); + assert_eq!(turned, actual); + } + } + } + } + } + #[test] + fn geometry_i16() { + check::(); + } + #[test] + fn geometry_i32() { + check::(); + } + #[test] + fn geometry_i64() { + check::(); + } + #[test] + fn preserved_integer_geometry_fingerprints() { + for (id, variant, h) in [ + (0, "default", 0xa5ecab9f1142e330), + (1, "grid", 0xc4788e07a48e5030), + (2, "default", 0x1a29e4309422f995), + (4, "default", 0x7d79add12de90845), + (5, "default", 0xf9e56b15fab43ea5), + (8, "default", 0xa296ee90b1d68f8d), + ] { + for actual in [ + generate::(id, 8, 0, variant).unwrap(), + generate::(id, 8, 0, variant).unwrap(), + generate::(id, 8, 0, variant).unwrap(), + ] { + assert_eq!(actual.fingerprint(), h, "test {id}"); + } + } + for (id, variant, h) in [ + (1, "line", 0x5bc125b5c077b125), + (3, "integer", 0x15c955bcde8dd41b), + (9, "horizontal", 0x2e8379b513bb23a5), + (9, "vertical", 0x2e6025fd4dd7ea65), + (10, "default", 0xf5d2281a93ecb815), + (11, "default", 0x83e26e355c378485), + (12, "cluster", 0x4db2dda094b2b7a0), + (12, "outlier", 0x61db173897cc7820), + (13, "default", 0xbd5ec9746049328d), + ] { + assert_eq!( + generate::(id, 32, 0, variant).unwrap().fingerprint(), + h + ); + } + } + + #[test] + fn seeded_vectors_and_pair_structure() { + for (seed, h) in [ + (0, 0x77ac91e43630115d), + (42, 0x95eba339e7c58a49), + (u64::MAX, 0xb3bbd963a77647f5), + ] { + assert_eq!( + generate::(13, 8, seed, "default") + .unwrap() + .fingerprint(), + h + ); + } + let mut h = generate::(9, 32, 0, "horizontal").unwrap(); + data::rotate(&mut h.subj); + data::rotate(&mut h.clip); + let v = generate::(9, 32, 0, "vertical").unwrap(); + assert_eq!(h.subj, v.subj); + assert_eq!(h.clip, v.clip); + let a = generate::(12, 32, 0, "cluster").unwrap(); + let b = generate::(12, 32, 0, "outlier").unwrap(); + assert_eq!(a.edges(), b.edges()); + assert_eq!(&a.subj[..32], &b.subj[..32]); + assert_eq!(a.clip, b.clip); + assert!(generate::(2, 65, 0, "default").is_err()); + assert!(generate::(14, 8, 0, "default").is_err()); + } +} diff --git a/performance/rust_app/src/test/runner.rs b/performance/rust_app/src/test/runner.rs new file mode 100644 index 00000000..ab7d0908 --- /dev/null +++ b/performance/rust_app/src/test/runner.rs @@ -0,0 +1,438 @@ +use super::data::{self, Fixture, Path}; +use crate::config::Config; +use i_overlay::core::{ + fill_rule::FillRule, + integer::OverlayInt, + overlay::Overlay, + solver::{MultithreadOptions, Precision, Solver, Strategy}, +}; +use i_overlay::i_float::int::point::IntPoint; +use i_overlay::i_shape::int::{path::IntPaths, shape::IntShapes}; +use std::collections::HashMap; +use std::{ + hint::black_box, + io::{self, Write}, + time::Instant, +}; +pub trait Engine: OverlayInt { + fn from_i64(n: i64) -> Self; + fn to_i64(self) -> i64; +} +macro_rules! engine { + ($i:ty) => { + impl Engine for $i { + fn from_i64(n: i64) -> Self { + n as Self + } + fn to_i64(self) -> i64 { + self as i64 + } + } + }; +} +engine!(i16); +engine!(i32); +engine!(i64); + +pub fn solver(strategy: &str, multithreading: bool) -> Solver { + Solver { + strategy: match strategy { + "auto" => Strategy::Auto, + "list" => Strategy::List, + "tree" => Strategy::Tree, + "frag" => Strategy::Frag, + _ => unreachable!(), + }, + precision: Precision::HIGH, + multithreading: multithreading.then(MultithreadOptions::default), + } +} + +pub fn convert(f: &Fixture) -> Result<(IntPaths, IntPaths), String> { + let limit = 1i128 << (I::BITS - 2); + let b = f.bounds(); + if [b.0, b.1].iter().any(|&v| i128::from(v) < -limit) + || [b.2, b.3].iter().any(|&v| i128::from(v) >= limit) + { + return Err(format!( + "coordinates {:?} exceed i{} integer overlay range [-{},{}]", + b, + I::BITS, + limit, + limit - 1 + )); + } + let cvt = |paths: &[Path]| { + paths + .iter() + .map(|p| { + p.iter() + .map(|&(x, y)| IntPoint::new(I::from_i64(x), I::from_i64(y))) + .collect() + }) + .collect() + }; + Ok((cvt(&f.subj), cvt(&f.clip))) +} + +// Keep winding, remove cyclic-start and contour/shape ordering. Default output +// simplifies collinear vertices. Grouping is unnecessary for filled boundary equality. +pub fn canonical(shapes: &IntShapes) -> Vec { + let mut paths: Vec = shapes + .iter() + .flatten() + .map(|path| { + let p: Path = path.iter().map(|p| (p.x.to_i64(), p.y.to_i64())).collect(); + normalize(p) + }) + .collect(); + paths.sort(); + paths +} +pub fn normalize(p: Path) -> Path { + if p.is_empty() { + return p; + } + // Lexicographic cyclic minimum handles repeated vertices as well. + let start = (0..p.len()) + .min_by(|&a, &b| { + (0..p.len()) + .map(|i| p[(a + i) % p.len()]) + .cmp((0..p.len()).map(|i| p[(b + i) % p.len()])) + }) + .unwrap(); + (0..p.len()).map(|i| p[(start + i) % p.len()]).collect() +} +pub fn area2(paths: &[Path]) -> i128 { + paths + .iter() + .map(|p| { + p.iter() + .zip(p.iter().cycle().skip(1)) + .take(p.len()) + .map(|(&(x, y), &(u, v))| { + i128::from(x) * i128::from(v) - i128::from(y) * i128::from(u) + }) + .sum::() + }) + .sum() +} + +fn csv(out: &mut impl Write, values: &[String]) -> io::Result<()> { + writeln!( + out, + "{}", + values + .iter() + .map(|v| format!("\"{}\"", v.replace('"', "\"\""))) + .collect::>() + .join(",") + ) +} +pub const HEADER: &str = "schema,fixture_version,runner_version,library_version,library_rev,library_dirty,library_source,runner_source,build_unix,rustc,target,profile,opt_level,debug_info,rustflags,host_os,logical_cpus,run_unix,label,threads_env,mt_compiled,test,case,variant,size,seed,strategy,split_route_hint,fill_route_hint,int,multithreading,precision,rule,mode,input_edges,subj_contours,clip_contours,min_x,min_y,max_x,max_y,input_fingerprint,output_contours,output_edges,output_area2,validated,warmup,samples,iterations,median_ns,min_ns,max_ns,samples_ns"; + +// Hints only: the splitter receives normalized/merged segments, and may change +// strategy on subsequent passes; fill thresholds concern post-split geometry. +fn route(strategy: &str, edges: usize) -> &'static str { + match strategy { + "list" => "list", + "frag" => "frag", + "tree" if edges > 16000 => "frag", + "tree" => "tree", + "auto" if edges < 4000 => "list", + "auto" if edges <= 16000 => "tree", + _ => "frag", + } +} + +pub fn run( + config: &Config, + id: usize, + variant: &str, + size: usize, + f: &Fixture, + deadline: Option, + out: &mut impl Write, +) -> Result { + let case = super::NAMES[id]; + let (subj, clip) = match convert::(f) { + Ok(input) => input, + Err(e) => { + eprintln!("skip {case} size={size}: {e}"); + return Ok(true); + } + }; + // Reference and all requested strategies are checked outside timing. + let reference = canonical( + &Overlay::from_subj_and_clip_custom( + &subj, + &clip, + Default::default(), + solver("list", false), + ) + .overlay(f.rule, FillRule::NonZero), + ); + for strategy in &config.strategies { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { + return Ok(false); + } + let s = solver(strategy, config.multithreading); + let mut reusable = Overlay::from_subj_and_clip_custom(&subj, &clip, Default::default(), s); + let result = reusable.overlay(f.rule, FillRule::NonZero); + if canonical(&result) != reference { + return Err(format!( + "{case} size={size} i{} {strategy}: geometry differs from List reference", + I::BITS + )); + } + // Check reinit after the splitter has already modified the segment buffer. + reusable.reinit_with_subj_and_clip(&subj, &clip); + if canonical(&reusable.overlay(f.rule, FillRule::NonZero)) != reference { + return Err(format!("{case}: reinit result differs")); + } + if id == 9 { + let mut rotated = f.clone(); + data::rotate(&mut rotated.subj); + data::rotate(&mut rotated.clip); + let (rs, rc) = convert::(&rotated)?; + let actual = canonical( + &Overlay::from_subj_and_clip_custom(&rs, &rc, Default::default(), s) + .overlay(f.rule, FillRule::NonZero), + ); + let mut expected = reference.clone(); + data::rotate(&mut expected); + expected = expected.into_iter().map(normalize).collect(); + expected.sort(); + if actual != expected { + return Err(format!("{case}: exact rotation invariant failed")); + } + } + for mode in &config.modes { + let mut operation = || { + if mode == "cold" { + let mut overlay = Overlay::from_subj_and_clip_custom( + black_box(&subj), + black_box(&clip), + Default::default(), + s, + ); + black_box(overlay.overlay(black_box(f.rule), FillRule::NonZero)); + } else { + reusable.reinit_with_subj_and_clip(black_box(&subj), black_box(&clip)); + black_box(reusable.overlay(black_box(f.rule), FillRule::NonZero)); + } + }; + for _ in 0..config.warmup { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { + return Ok(false); + } + operation(); + } + let mut samples = Vec::with_capacity(config.samples); + for _ in 0..config.samples { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { + return Ok(false); + } + let start = Instant::now(); + for _ in 0..config.iterations { + operation(); + } + samples.push(start.elapsed().as_nanos() / config.iterations as u128); + } + let mut sorted = samples.clone(); + sorted.sort_unstable(); + let median = if sorted.len() % 2 == 0 { + (sorted[sorted.len() / 2 - 1] + sorted[sorted.len() / 2]) / 2 + } else { + sorted[sorted.len() / 2] + }; + let b = f.bounds(); + let fill_hint = match strategy.as_str() { + "list" => "list", + "auto" => "post_split_threshold_8000", + _ => "tree", + }; + let values = vec![ + "2".into(), + "2".into(), + env!("CARGO_PKG_VERSION").into(), + env!("BENCH_LIBRARY_VERSION").into(), + env!("BENCH_LIBRARY_REV").into(), + env!("BENCH_LIBRARY_DIRTY").into(), + env!("BENCH_LIBRARY_SOURCE").into(), + env!("BENCH_RUNNER_SOURCE").into(), + env!("BENCH_BUILD_UNIX").into(), + env!("BENCH_RUSTC").into(), + env!("BENCH_TARGET").into(), + env!("BENCH_PROFILE").into(), + env!("BENCH_OPT_LEVEL").into(), + env!("BENCH_DEBUG").into(), + env!("BENCH_RUSTFLAGS").into(), + std::env::consts::OS.into(), + std::thread::available_parallelism() + .map(|n| n.get().to_string()) + .unwrap_or_else(|_| "unknown".into()), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + .to_string(), + config.label.clone(), + std::env::var("RAYON_NUM_THREADS").unwrap_or_default(), + cfg!(feature = "multithreading").to_string(), + id.to_string(), + case.into(), + variant.into(), + size.to_string(), + config.seed.to_string(), + strategy.clone(), + route(strategy, f.edges()).into(), + fill_hint.into(), + format!("i{}", I::BITS), + config.multithreading.to_string(), + "HIGH".into(), + format!("{:?}", f.rule), + mode.clone(), + f.edges().to_string(), + f.subj.len().to_string(), + f.clip.len().to_string(), + b.0.to_string(), + b.1.to_string(), + b.2.to_string(), + b.3.to_string(), + format!("{:016x}", f.fingerprint()), + reference.len().to_string(), + reference.iter().map(Vec::len).sum::().to_string(), + area2(&reference).to_string(), + "true".into(), + config.warmup.to_string(), + config.samples.to_string(), + config.iterations.to_string(), + median.to_string(), + sorted[0].to_string(), + sorted.last().unwrap().to_string(), + samples + .iter() + .map(u128::to_string) + .collect::>() + .join(";"), + ]; + csv(out, &values).map_err(|e| e.to_string())?; + } + } + Ok(true) +} + +pub fn run_selected( + id: usize, + count: usize, + args_map: &HashMap, + solver: Solver, +) { + let args = args_map + .iter() + .filter(|(key, _)| { + !["test", "count", "complex", "int", "benchmark", "strategy"].contains(&key.as_str()) + }) + .flat_map(|(key, value)| [format!("--{key}"), value.clone()]) + .chain([ + "--test".into(), + id.to_string(), + "--count".into(), + count.to_string(), + "--complex".into(), + "false".into(), + "--strategy".into(), + format!("{:?}", solver.strategy).to_lowercase(), + ]); + let config = crate::config::parse(args) + .expect("Invalid benchmark options") + .expect("Missing test configuration"); + let deadline = config.budget.map(|budget| Instant::now() + budget); + let stdout = io::stdout(); + let mut out = io::BufWriter::new(stdout.lock()); + writeln!(out, "{}", HEADER).expect("Unable to write CSV header"); + for variant in config.variants(id) { + let fixture = super::generate::(id, count, config.seed, variant) + .expect("Unable to generate test input"); + if !run::(&config, id, variant, count, &fixture, deadline, &mut out) + .expect("Benchmark failed") + { + eprintln!("budget reached; remaining variants omitted"); + break; + } + } +} +pub fn run_benchmark(config: Config) -> Result<(), String> { + if cfg!(debug_assertions) { + return Err("use cargo run --release".into()); + } + let deadline = config.budget.map(|budget| Instant::now() + budget); + let stdout = io::stdout(); + let mut out = io::BufWriter::new(stdout.lock()); + writeln!(out, "{}", HEADER).map_err(|e| e.to_string())?; + for &id in &config.tests { + for variant in config.variants(id) { + for &n in &config.counts { + if n > super::max_count(id, variant) { + if config.complex { + eprintln!("skip test {id} {variant} count={n}: size cap"); + continue; + } + return Err(format!( + "test {id} {variant}: count exceeds {}", + super::max_count(id, variant) + )); + } + for engine in &config.engines { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { + eprintln!("budget reached; remaining combinations omitted"); + return Ok(()); + } + let completed = match engine.as_str() { + "i16" => { + let f = super::generate::(id, n, config.seed, variant)?; + run::(&config, id, variant, n, &f, deadline, &mut out)? + } + "i32" => { + let f = super::generate::(id, n, config.seed, variant)?; + run::(&config, id, variant, n, &f, deadline, &mut out)? + } + "i64" => { + let f = super::generate::(id, n, config.seed, variant)?; + run::(&config, id, variant, n, &f, deadline, &mut out)? + } + _ => unreachable!(), + }; + if !completed { + eprintln!("budget reached; incomplete row omitted"); + return Ok(()); + } + } + } + } + } + out.flush().map_err(|e| e.to_string()) +} + +#[cfg(test)] +mod checks { + use super::*; + use i_overlay::core::overlay_rule::OverlayRule; + #[test] + fn integer_coordinate_budget() { + let f = Fixture { + subj: vec![data::rect(16380, 0, 8, 8)], + clip: vec![], + rule: OverlayRule::Union, + }; + assert!(convert::(&f).is_err()); + assert!(convert::(&f).is_ok()); + let f = Fixture { + subj: vec![data::rect(-16384, -16384, 32767, 32767)], + clip: vec![], + rule: OverlayRule::Union, + }; + assert!(convert::(&f).is_ok()); + } +} diff --git a/performance/rust_app/src/test/test_0_checkerboard.rs b/performance/rust_app/src/test/test_0_checkerboard.rs index adeef5b2..08870cec 100644 --- a/performance/rust_app/src/test/test_0_checkerboard.rs +++ b/performance/rust_app/src/test/test_0_checkerboard.rs @@ -1,4 +1,6 @@ -use crate::test::util::{OverlayInt, Util}; +use super::data::Fixture; +use super::util::OverlayInt; +use super::util::Util; use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; @@ -7,7 +9,6 @@ use i_overlay::i_float::int::point::IntPoint; use std::time::Instant; pub(crate) struct CheckerboardTest; - /* test 0 CheckerboardTest @@ -98,11 +99,10 @@ multithreading off 1024(2095105 6.3) - 4.393880(0.6) */ -// A grid of overlapping squares forming a simple checkerboard pattern. impl CheckerboardTest { pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { // 1000 - if Util::skip_if_out_of_range::(n, 15 * n + 10) { + if Util::skip_if_out_of_range::(n, 30 * n + 20) { return; } @@ -125,9 +125,13 @@ impl CheckerboardTest { let start = Instant::now(); for _i in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); } let duration = start.elapsed(); @@ -142,4 +146,10 @@ impl CheckerboardTest { n, polygons_count, count_log, time, time_log ); } + + pub(crate) fn geometry(n: usize) -> Fixture { + let subj = Util::many_squares(IntPoint::new(0i64, 0), 20, 30, n); + let clip = Util::many_squares(IntPoint::new(15i64, 15), 20, 30, n - 1); + Fixture::from_int(subj, clip, OverlayRule::Xor) + } } diff --git a/performance/rust_app/src/test/test_10_diagonal.rs b/performance/rust_app/src/test/test_10_diagonal.rs new file mode 100644 index 00000000..d9e7b5b0 --- /dev/null +++ b/performance/rust_app/src/test/test_10_diagonal.rs @@ -0,0 +1,28 @@ +use super::data::Fixture; +use i_overlay::core::overlay_rule::OverlayRule; +pub(crate) struct DiagonalTest; +impl DiagonalTest { + pub(crate) fn geometry(size: usize) -> Fixture { + let n = size as i64; + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + let l = 8 * n + 64; + for i in 0..n { + let y = 16 * i; + let dy = l / 2 + i % 3; + f.subj + .push(vec![(-l, y), (l, y + dy), (l, y + dy + 4), (-l, y + 4)]); + f.clip.push(vec![ + (-l, y + 2), + (l, y + dy + 2), + (l, y + dy + 6), + (-l, y + 6), + ]); + } + f.rule = OverlayRule::Intersect; + f + } +} diff --git a/performance/rust_app/src/test/test_11_collinear.rs b/performance/rust_app/src/test/test_11_collinear.rs new file mode 100644 index 00000000..7e3025f2 --- /dev/null +++ b/performance/rust_app/src/test/test_11_collinear.rs @@ -0,0 +1,21 @@ +use super::data::{rect, Fixture}; +use i_overlay::core::overlay_rule::OverlayRule; +pub(crate) struct CollinearTest; +impl CollinearTest { + pub(crate) fn geometry(size: usize) -> Fixture { + let n = size as i64; + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + for i in 0..n { + let x = 32 * i; + f.subj.push(rect(x, 0, 24, 12)); + f.clip.push(rect(x + 8, 0, 24, 12)); // Partial collinear overlap. + f.clip.push(rect(x, 12, 24, 12)); // Shared boundary. + } + f.rule = OverlayRule::Xor; + f + } +} diff --git a/performance/rust_app/src/test/test_12_cluster.rs b/performance/rust_app/src/test/test_12_cluster.rs new file mode 100644 index 00000000..3939bad9 --- /dev/null +++ b/performance/rust_app/src/test/test_12_cluster.rs @@ -0,0 +1,24 @@ +use super::data::{rect, Fixture, Prng}; +use i_overlay::core::overlay_rule::OverlayRule; +pub(crate) struct ClusterTest; +impl ClusterTest { + pub(crate) fn geometry(size: usize, seed: u64, outlier: bool) -> Fixture { + let n = size as i64; + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + let mut rng = Prng(seed); + for _ in 0..n { + let x = (rng.next() % 512) as i64 - 256; + let y = (rng.next() % 512) as i64 - 256; + f.subj.push(rect(x, y, 64, 64)); + f.clip.push(rect(x + 16, y + 16, 64, 64)); + } + // Both paired cases have exactly the same edge count. + let x = if outlier { 12000 } else { 0 }; + f.subj.push(rect(x, x, 8, 8)); + f + } +} diff --git a/performance/rust_app/src/test/test_13_seeded.rs b/performance/rust_app/src/test/test_13_seeded.rs new file mode 100644 index 00000000..38506c4d --- /dev/null +++ b/performance/rust_app/src/test/test_13_seeded.rs @@ -0,0 +1,28 @@ +use super::data::{rect, Fixture, Prng}; +use i_overlay::core::overlay_rule::OverlayRule; +pub(crate) struct SeededTest; +impl SeededTest { + pub(crate) fn geometry(size: usize, seed: u64) -> Fixture { + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + let mut rng = Prng(seed); + // Exactly four draws per rectangle, alternating subject and clip. + // Modulo mapping is intentional (no distribution/version dependency). + for i in 0..2 * size { + let x = (rng.next() % 4096) as i64 - 2048; + let y = (rng.next() % 4096) as i64 - 2048; + let w = 8 + (rng.next() % 256) as i64; + let h = 8 + (rng.next() % 256) as i64; + if i % 2 == 0 { + f.subj.push(rect(x, y, w, h)); + } else { + f.clip.push(rect(x, y, w, h)); + } + } + f.rule = OverlayRule::Xor; + f + } +} diff --git a/performance/rust_app/src/test/test_1_not_overlap.rs b/performance/rust_app/src/test/test_1_not_overlap.rs index d447e08f..0ba79d69 100644 --- a/performance/rust_app/src/test/test_1_not_overlap.rs +++ b/performance/rust_app/src/test/test_1_not_overlap.rs @@ -1,4 +1,6 @@ -use crate::test::util::{OverlayInt, Util}; +use super::data::Fixture; +use super::util::OverlayInt; +use super::util::Util; use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; @@ -102,7 +104,6 @@ multithreading off */ -// A grid of not overlapping squares. impl NotOverlapTest { pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { // 1000 @@ -129,9 +130,13 @@ impl NotOverlapTest { let start = Instant::now(); for _i in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); } let duration = start.elapsed(); @@ -141,4 +146,22 @@ impl NotOverlapTest { println!("{:.1} - {:.6}", polygons_count, time); } + + pub(crate) fn geometry(n: usize, line: bool) -> Fixture { + if line { + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + for i in 0..n as i64 { + f.subj.push(super::data::rect(32 * i, 0, 8, 8)); + f.clip.push(super::data::rect(32 * i + 16, 12, 8, 8)); + } + return f; + } + let subj = Util::many_squares(IntPoint::new(0i64, 0), 10, 30, n); + let clip = Util::many_squares(IntPoint::new(15i64, 15), 10, 30, n - 1); + Fixture::from_int(subj, clip, OverlayRule::Union) + } } diff --git a/performance/rust_app/src/test/test_2_lines_net.rs b/performance/rust_app/src/test/test_2_lines_net.rs index 3d98e92d..aee03b11 100644 --- a/performance/rust_app/src/test/test_2_lines_net.rs +++ b/performance/rust_app/src/test/test_2_lines_net.rs @@ -1,4 +1,6 @@ -use crate::test::util::{OverlayInt, Util}; +use super::data::Fixture; +use super::util::OverlayInt; +use super::util::Util; use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; @@ -6,7 +8,6 @@ use i_overlay::core::solver::Solver; use std::time::Instant; pub(crate) struct LinesNetTest; - /* test 2 LinesNetTest @@ -102,7 +103,6 @@ multithreading off */ -// A grid is formed by the intersection of a set of vertical and horizontal lines. impl LinesNetTest { pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { // 500 @@ -119,9 +119,13 @@ impl LinesNetTest { let start = Instant::now(); for _ in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); } let duration = start.elapsed(); @@ -131,4 +135,12 @@ impl LinesNetTest { println!("{} - {:.6}", polygons_count, time); } + + pub(crate) fn geometry(n: usize) -> Fixture { + Fixture::from_int( + Util::many_lines_x(20i64, n), + Util::many_lines_y(20i64, n), + OverlayRule::Intersect, + ) + } } diff --git a/performance/rust_app/src/test/test_3_spiral.rs b/performance/rust_app/src/test/test_3_spiral.rs index 7c2e8b8f..9b8a8b04 100644 --- a/performance/rust_app/src/test/test_3_spiral.rs +++ b/performance/rust_app/src/test/test_3_spiral.rs @@ -1,4 +1,7 @@ -use crate::test::util::{OverlayInt, Util}; +use super::data::Fixture; +use super::runner::Engine; +use super::util::OverlayInt; +use super::util::Util; use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay_rule::OverlayRule; use i_overlay::core::solver::Solver; @@ -7,7 +10,6 @@ use i_overlay::i_float::float::point::FloatPoint; use std::time::Instant; pub(crate) struct SpiralTest; - /* test 3 SpiralTest @@ -153,7 +155,6 @@ multithreading off */ -// Two irregular self-intersecting polygons are generated, the vertices of which are defined by a fixed radius and angle. impl SpiralTest { pub(crate) fn run(n: usize, solver: Solver, scale: f64) { // 1000 @@ -180,4 +181,54 @@ impl SpiralTest { println!("{} - {:.6}", polygons_count, time); } + + pub(crate) fn geometry(n: usize, integer: bool) -> Fixture { + if integer { + return Self::integer_geometry(n); + } + let path = Util::spiral(n, 100.0) + .into_iter() + .map(|p| [p.x, p.y]) + .collect(); + Fixture::from_float::(vec![path], vec![], OverlayRule::Subject) + } + fn integer_geometry(size: usize) -> Fixture { + let n = size as i64; + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Difference, + }; + // Square spiral ribbon: offset each orthogonal turn by 2 units. + let directions = [(1, 0), (0, 1), (-1, 0), (0, -1)]; + let mut center = vec![(0, 0)]; + for i in 0..size { + let (dx, dy) = directions[i % 4]; + let step = 16 * (i as i64 / 2 + 1); + let (x, y) = *center.last().unwrap(); + center.push((x + dx * step, y + dy * step)); + } + let mut left = Vec::new(); + let mut right = Vec::new(); + for (i, &(x, y)) in center.iter().enumerate() { + let before = directions[i.saturating_sub(1) % 4]; + let after = directions[i.min(size - 1) % 4]; + let normal = if i == 0 { + (-after.1, after.0) + } else if i == size { + (-before.1, before.0) + } else { + (-before.1 - after.1, before.0 + after.0) + }; + left.push((x + 2 * normal.0, y + 2 * normal.1)); + right.push((x - 2 * normal.0, y - 2 * normal.1)); + } + right.reverse(); + left.extend(right); + f.subj.push(left); + f.clip + .push(super::data::rect(-4, -16 * n - 4, 8, 32 * n + 8)); + f.rule = OverlayRule::Difference; + f + } } diff --git a/performance/rust_app/src/test/test_4_windows.rs b/performance/rust_app/src/test/test_4_windows.rs index 5c460c40..02b5a2b7 100644 --- a/performance/rust_app/src/test/test_4_windows.rs +++ b/performance/rust_app/src/test/test_4_windows.rs @@ -1,4 +1,6 @@ -use crate::test::util::{OverlayInt, Util}; +use super::data::Fixture; +use super::util::OverlayInt; +use super::util::Util; use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; @@ -92,7 +94,6 @@ i64 */ -// A grid of square frames, each with a smaller square cutout in the center. impl WindowsTest { pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { // 500 @@ -112,9 +113,13 @@ impl WindowsTest { let start = Instant::now(); for _ in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); } let duration = start.elapsed(); @@ -124,4 +129,10 @@ impl WindowsTest { println!("{} - {:.6}", polygons_count, time); } + + pub(crate) fn geometry(n: usize) -> Fixture { + let x = n as i64 * 30 / 2; + let (subj, clip) = Util::many_windows(IntPoint::new(-x, -x), 20, 10, 30, n); + Fixture::from_int(subj, clip, OverlayRule::Difference) + } } diff --git a/performance/rust_app/src/test/test_5_nested_squares.rs b/performance/rust_app/src/test/test_5_nested_squares.rs index 994aac98..78b80fff 100644 --- a/performance/rust_app/src/test/test_5_nested_squares.rs +++ b/performance/rust_app/src/test/test_5_nested_squares.rs @@ -1,4 +1,6 @@ -use crate::test::util::{OverlayInt, Util}; +use super::data::Fixture; +use super::util::OverlayInt; +use super::util::Util; use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; @@ -6,7 +8,6 @@ use i_overlay::core::solver::Solver; use std::time::Instant; pub(crate) struct CrossTest; - /* test 5 CrossTest @@ -119,7 +120,6 @@ i64 */ -// A series of concentric squares, each progressively larger than the last. impl CrossTest { pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { // 500 @@ -135,9 +135,13 @@ impl CrossTest { let start = Instant::now(); for _ in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); } let duration = start.elapsed(); let time = duration.as_secs_f64() / sq_it_count as f64; @@ -146,4 +150,9 @@ impl CrossTest { println!("{} - {:.6}", polygons_count, time); } + + pub(crate) fn geometry(n: usize) -> Fixture { + let (subj, clip) = Util::concentric_squares(2i64, n); + Fixture::from_int(subj, clip, OverlayRule::Xor) + } } diff --git a/performance/rust_app/src/test/test_6_corrosion.rs b/performance/rust_app/src/test/test_6_corrosion.rs index b7d7c951..85067b27 100644 --- a/performance/rust_app/src/test/test_6_corrosion.rs +++ b/performance/rust_app/src/test/test_6_corrosion.rs @@ -1,13 +1,15 @@ -use crate::test::util::OverlayInt; +use super::data::Fixture; +use super::runner::Engine; +use super::util::OverlayInt; use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay_rule::OverlayRule; use i_overlay::core::solver::Solver; use i_overlay::float::overlay::FloatOverlay; use std::f64::consts::PI; use std::time::Instant; +type FloatGeometry = (Vec>, Vec>); pub(crate) struct CorrosionTest; - /* test 6 CorrosionTest @@ -92,11 +94,10 @@ i64 512 - 5.351266 */ -// A series of concentric squares, each progressively larger than the last. impl CorrosionTest { pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { // 500 - let (subj_paths, clip_paths) = Self::geometry(100.0, n); + let (subj_paths, clip_paths) = Self::float_geometry(100.0, n); let it_count = ((scale / (n as f64)) as usize).max(1); let sq_it_count = it_count * it_count; @@ -118,7 +119,11 @@ impl CorrosionTest { println!("{} - {:.6}", n, time); } - fn geometry(size: f64, count: usize) -> (Vec>, Vec>) { + pub(crate) fn geometry(n: usize) -> Fixture { + let (subj, clip) = Self::float_geometry(100.0, n); + Fixture::from_float::(subj, clip, OverlayRule::Difference) + } + fn float_geometry(size: f64, count: usize) -> FloatGeometry { let subj_radius = 0.4 * size; let subj_step = size; @@ -137,13 +142,11 @@ impl CorrosionTest { let mut paths = Vec::with_capacity(count); for i in 0..count { - let mut index = i; let mut x = -offset; - for _ in 0..count { + for index in i..i + count { let count = (index % 5) + 3; paths.push(Self::shape([x, y], radius, count)); x += step; - index += 1; } y += step; } diff --git a/performance/rust_app/src/test/test_7_concentric.rs b/performance/rust_app/src/test/test_7_concentric.rs index 680c7d6f..38a0d8e8 100644 --- a/performance/rust_app/src/test/test_7_concentric.rs +++ b/performance/rust_app/src/test/test_7_concentric.rs @@ -1,13 +1,15 @@ -use crate::test::util::OverlayInt; +use super::data::Fixture; +use super::runner::Engine; +use super::util::OverlayInt; use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay_rule::OverlayRule; use i_overlay::core::solver::Solver; use i_overlay::float::overlay::FloatOverlay; use std::f64::consts::PI; use std::time::Instant; +type FloatGeometry = (Vec>, Vec>); pub(crate) struct ConcentricTest; - /* test 7 ConcentricTest @@ -94,10 +96,9 @@ i64 */ -// A series of concentric squares, each progressively larger than the last. impl ConcentricTest { pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { - let (subj_paths, clip_paths) = Self::geometry(100.0, n); + let (subj_paths, clip_paths) = Self::float_geometry(100.0, n); let it_count = ((scale / (n as f64)) as usize).max(1); let sq_it_count = it_count * it_count; @@ -119,7 +120,11 @@ impl ConcentricTest { println!("{} - {:.6}", n, time); } - fn geometry(size: f64, count: usize) -> (Vec>, Vec>) { + pub(crate) fn geometry(n: usize) -> Fixture { + let (subj, clip) = Self::float_geometry(100.0, n); + Fixture::from_float::(subj, clip, OverlayRule::Intersect) + } + fn float_geometry(size: f64, count: usize) -> FloatGeometry { let mut clip = Vec::with_capacity(count); let mut subj = Vec::with_capacity(count); diff --git a/performance/rust_app/src/test/test_8_wind_mill.rs b/performance/rust_app/src/test/test_8_wind_mill.rs index c3182724..42330566 100644 --- a/performance/rust_app/src/test/test_8_wind_mill.rs +++ b/performance/rust_app/src/test/test_8_wind_mill.rs @@ -1,4 +1,6 @@ -use crate::test::util::{OverlayInt, Util}; +use super::data::Fixture; +use super::util::OverlayInt; +use super::util::Util; use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; @@ -9,7 +11,6 @@ use i_overlay::i_shape::int::shape::IntContour; use std::time::Instant; pub(crate) struct WindMillTest; - /* test 8 WindMillTest @@ -99,7 +100,7 @@ impl WindMillTest { return; } - let (subj_paths, clip_paths) = Self::geometry(I::from_usize(80), n); + let (subj_paths, clip_paths) = Self::int_geometry(I::from_usize(80), n); let it_count = ((scale / (n as f64)) as usize).max(1); let sq_it_count = it_count * it_count; @@ -107,9 +108,13 @@ impl WindMillTest { let start = Instant::now(); for _ in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); } let duration = start.elapsed(); @@ -119,17 +124,28 @@ impl WindMillTest { } pub(crate) fn validate(n: usize, rule: OverlayRule, solver: Solver) { - let (subj_paths, clip_paths) = Self::geometry(I::from_usize(80), n); + let (subj_paths, clip_paths) = Self::int_geometry(I::from_usize(80), n); - let res = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); + let res = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); assert_eq!(res.len(), n * n); println!("result validation PASS"); } - fn geometry(size: I, count: usize) -> (Vec>, Vec>) { + pub(crate) fn geometry(n: usize) -> Fixture { + let (subj, clip) = Self::int_geometry(80i64, n); + Fixture::from_int(subj, clip, OverlayRule::Intersect) + } + fn int_geometry( + size: I, + count: usize, + ) -> (Vec>, Vec>) { let mut subj_paths = Vec::with_capacity(4 * count * count); let mut clip_paths = Vec::with_capacity(4 * count * count); diff --git a/performance/rust_app/src/test/test_9_orientation.rs b/performance/rust_app/src/test/test_9_orientation.rs new file mode 100644 index 00000000..9b0e5a2a --- /dev/null +++ b/performance/rust_app/src/test/test_9_orientation.rs @@ -0,0 +1,23 @@ +use super::data::{rect, rotate, Fixture}; +use i_overlay::core::overlay_rule::OverlayRule; +pub(crate) struct OrientationTest; +impl OrientationTest { + pub(crate) fn geometry(size: usize, vertical: bool) -> Fixture { + let n = size as i64; + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + for i in 0..n { + f.subj.push(rect(-8 * n, 16 * i, 16 * n, 6)); + f.clip.push(rect(-8 * n + 4, 16 * i + 3, 16 * n, 6)); + } + f.rule = OverlayRule::Xor; + if vertical { + rotate(&mut f.subj); + rotate(&mut f.clip); + } + f + } +} diff --git a/performance/rust_app/src/test/util.rs b/performance/rust_app/src/test/util.rs index 08e6da44..1c52fd3b 100644 --- a/performance/rust_app/src/test/util.rs +++ b/performance/rust_app/src/test/util.rs @@ -1,21 +1,17 @@ -use i_key_sort::sort::key::SortKey; use i_overlay::i_float::float::point::FloatPoint; use i_overlay::i_float::int::number::int::IntNumber; use i_overlay::i_float::int::point::IntPoint; use i_overlay::i_shape::base::data::Contour; use i_overlay::i_shape::int::path::IntPath; -use i_tree::{Expiration, LayoutNumber}; use std::f64::consts::PI; pub(super) struct Util; -pub(crate) trait OverlayInt: IntNumber + Expiration + LayoutNumber + SortKey {} - -impl OverlayInt for I where I: IntNumber + Expiration + LayoutNumber + SortKey {} +pub(crate) use super::runner::Engine as OverlayInt; impl Util { pub(super) fn skip_if_out_of_range(n: usize, max_abs_coord: usize) -> bool { - let max = I::MAX.to_usize(); + let max = I::MAX.to_usize() / 2; if max_abs_coord <= max { return false; }