honeycomb_benches/
remesh.rs

1//! 2D remshing pipeline benchmark
2//!
3//! This benchmark execute a proxy-application of usual 2D remeshing pipelines. It is split into
4//! two parts:
5//!
6//! - initialization & first capture,
7//! - iterative remeshing.
8//!
9//! Time sampling is done on the second part. Currently only executes in sequential.
10
11use std::time::Instant;
12
13use honeycomb::{
14    kernels::{
15        grisubal::Clip,
16        remeshing::{
17            EdgeCollapseError, EdgeSwapError, capture_geometry, classify_capture, collapse_edge,
18            cut_inner_edge, cut_outer_edge, move_vertex_to_average, swap_edge,
19        },
20        triangulation::{TriangulateError, earclip_cell_countercw},
21        utils::{EdgeAnchor, FaceAnchor, VertexAnchor, is_orbit_orientation_consistent},
22    },
23    prelude::{CMap2, CoordsFloat, DartIdType, NULL_DART_ID, OrbitPolicy, SewError, Vertex2},
24    stm::{StmClosureResult, Transaction, abort, atomically, atomically_with_err, retry},
25};
26use rayon::{iter::Either, prelude::*};
27
28use crate::{
29    cli::RemeshArgs,
30    prof_start, prof_stop,
31    utils::{get_num_threads, hash_file},
32};
33
34pub fn bench_remesh<T: CoordsFloat>(args: RemeshArgs) -> CMap2<T> {
35    let input_map = args.input.to_str().unwrap();
36    let target_len = T::from(args.target_length).unwrap();
37
38    let n_threads = get_num_threads().unwrap_or(
39        std::thread::available_parallelism()
40            .map(|n| n.get())
41            .unwrap_or(1),
42    );
43
44    // load map from file
45    let input_hash = hash_file(input_map).expect("E: could not compute input hash"); // file id for posterity
46
47    // -- capture via grid overlap
48    let mut instant = Instant::now();
49    let mut map: CMap2<T> = capture_geometry(
50        input_map,
51        [T::from(args.lx).unwrap(), T::from(args.ly).unwrap()],
52        Clip::from(args.clip),
53    )
54    .unwrap();
55    let capture_time = instant.elapsed();
56
57    // -- classification
58    instant = Instant::now();
59    classify_capture(&map).unwrap();
60    let classification_time = instant.elapsed();
61
62    // -- triangulation
63    instant = Instant::now();
64    prof_start!("HCBENCH_REMESH_TRIANGULATION");
65    let n_tot = map
66        .iter_faces()
67        .map(|id| (map.orbit(OrbitPolicy::Face, id as DartIdType).count() - 3) * 2)
68        .sum();
69    let start = map.allocate_used_darts(n_tot);
70    // use a prefix sum starting from the newly allocated darts to associate free darts to each face
71    map.iter_faces()
72        .scan(start, |state, f| {
73            // compute the number of dart needed to triangulate this face
74            let n_d = (map.orbit(OrbitPolicy::Face, f as DartIdType).count() - 3) * 2;
75            *state += n_d as DartIdType;
76            Some((f, n_d, *state - n_d as DartIdType))
77        })
78        .filter(|(_, n_d, _)| *n_d != 0)
79        .for_each(|(f, n_d, start)| {
80            let new_darts = (start..start + n_d as DartIdType).collect::<Vec<_>>();
81            let anchor = map.force_remove_attribute::<FaceAnchor>(f);
82            // make sure new edges are anchored
83            if let Some(a) = anchor {
84                atomically(|t| {
85                    for &d in &new_darts {
86                        map.write_attribute(t, d, EdgeAnchor::from(a))?;
87                    }
88                    Ok(())
89                });
90            }
91            while let Err(e) =
92                atomically_with_err(|t| earclip_cell_countercw(t, &map, f, &new_darts))
93            {
94                match e {
95                    TriangulateError::UndefinedFace(_) | TriangulateError::OpFailed(_) => continue,
96                    TriangulateError::NoEar => panic!("E: cannot triangulate the geometry capture"),
97                    TriangulateError::AlreadyTriangulated
98                    | TriangulateError::NonFannable
99                    | TriangulateError::NotEnoughDarts(_)
100                    | TriangulateError::TooManyDarts(_) => {
101                        unreachable!()
102                    }
103                }
104            }
105            // make sure new faces are anchored
106            if let Some(a) = anchor {
107                atomically(|t| {
108                    for &d in &new_darts {
109                        let fid = map.face_id_tx(t, d)?;
110                        map.write_attribute(t, fid, a)?;
111                    }
112                    Ok(())
113                });
114            }
115        });
116    let triangulation_time = instant.elapsed();
117    prof_stop!("HCBENCH_REMESH_TRIANGULATION");
118
119    // check that the mesh is triangular, consistently oriented and fully classified
120    debug_assert!(
121        map.par_iter_faces()
122            .all(|f| map.orbit(OrbitPolicy::Face, f as DartIdType).count() == 3)
123    );
124    debug_assert!(map.par_iter_faces().all(|f| {
125        map.orbit(OrbitPolicy::FaceLinear, f).count() == 3
126            && atomically(|t| check_tri_orientation(t, &map, f as DartIdType))
127    }));
128    debug_assert!(
129        map.par_iter_vertices()
130            .all(|v| map.force_read_attribute::<VertexAnchor>(v).is_some())
131    );
132    debug_assert!(
133        map.par_iter_edges()
134            .all(|e| map.force_read_attribute::<EdgeAnchor>(e).is_some())
135    );
136    debug_assert!(
137        map.par_iter_faces()
138            .all(|f| map.force_read_attribute::<FaceAnchor>(f).is_some())
139    );
140
141    // TODO: print the whole config / args
142    println!("| remesh benchmark");
143    println!("|-> input      : {input_map} (hash: {input_hash:#0x})");
144    println!(
145        "|-> backend    : {:?} with {n_threads} thread(s)",
146        args.backend
147    );
148    println!("|-> target size: {target_len:?}");
149    println!("|-> capture time  : {}ms", capture_time.as_millis());
150    println!(
151        "|-> triangulation time  : {}ms",
152        triangulation_time.as_millis()
153    );
154    println!(
155        "|-> classification time  : {}ms",
156        classification_time.as_millis()
157    );
158
159    println!(
160        "Round | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {}",
161        "# of used darts",     // 15
162        "# of unused darts",   // 17
163        "Graph compute (s)",   // 17
164        "Relax (tot, s)",      // 14
165        "Ret cond (s)",        // 12
166        "Batch compute (s)",   // 17
167        "Dart prealloc (s)",   // 17
168        "Cut batch size",      // 14
169        "Cut edges (s)",       // 13
170        "Collapse batch size", // 19
171        "Collapse edges (s)",  // 18
172        "Swap edges (s)",      // 14
173    );
174    // -- main remeshing loop
175    // a. relax
176    // b. cut / collapse
177    // c. swap
178    // check for ending condition after each relax
179    prof_start!("HCBENCH_REMESH_MAINLOOP");
180    let mut n = 0;
181    let mut r;
182    loop {
183        print!("{:>5}", n);
184        // not using the map method because it uses a sequential iterator
185        let n_unused = (1..map.n_darts() as DartIdType)
186            .into_par_iter()
187            .filter(|d| map.is_unused(*d))
188            .count();
189        print!(" | {:>15}", map.n_darts() - n_unused);
190        print!(" | {:>17}", n_unused);
191
192        // -- build the vertex graph
193        prof_start!("HCBENCH_REMESH_GRAPH");
194        instant = Instant::now();
195        let nodes: Vec<(_, Vec<_>)> = map
196            .par_iter_vertices()
197            .filter_map(|v| {
198                let mut neigh = Vec::with_capacity(10);
199                for d in map.orbit(OrbitPolicy::Vertex, v as DartIdType) {
200                    let b2d = map.beta::<2>(d);
201                    if b2d == NULL_DART_ID {
202                        return None; // filter out vertices on the boundary
203                    } else {
204                        neigh.push(map.vertex_id(b2d));
205                    }
206                }
207                Some((v, neigh))
208            })
209            .collect();
210        prof_stop!("HCBENCH_REMESH_GRAPH");
211        print!(" | {:>17.6e}", instant.elapsed().as_secs_f64());
212
213        // -- relax
214        prof_start!("HCBENCH_REMESH_RELAX");
215        instant = Instant::now();
216        r = 0;
217        loop {
218            nodes.par_iter().for_each(|(vid, neighbors)| {
219                let _ = atomically_with_err(|t| {
220                    move_vertex_to_average(t, &map, *vid, &neighbors)?;
221                    if !is_orbit_orientation_consistent(t, &map, *vid)? {
222                        abort("E: resulting geometry is inverted")?;
223                    }
224                    Ok(())
225                });
226            });
227
228            r += 1;
229            if r >= args.n_relax_rounds.get() {
230                break;
231            }
232        }
233        prof_stop!("HCBENCH_REMESH_RELAX");
234        print!(" | {:>14.6e}", instant.elapsed().as_secs_f64());
235
236        debug_assert!(
237            map.par_iter_faces()
238                .all(|f| { atomically(|t| check_tri_orientation(t, &map, f as DartIdType)) })
239        );
240
241        // -- get edges to process
242        instant = Instant::now();
243        let (long_edges, short_edges): (Vec<_>, Vec<_>) = map
244            .par_iter_edges()
245            .filter_map(|e| {
246                let (l, r) = (e as DartIdType, map.beta::<1>(e as DartIdType));
247                let diff =
248                    atomically(|t| compute_diff_to_target(t, &map, l, r, args.target_length));
249                if diff.abs() > args.target_tolerance {
250                    Some((e, diff))
251                } else {
252                    None
253                }
254            })
255            .partition_map(|(e, diff)| {
256                if diff.is_sign_positive() {
257                    Either::Left(e)
258                } else {
259                    Either::Right(e)
260                }
261            });
262        let batch_time = instant.elapsed().as_secs_f64();
263        // -- check early return conds if enabled
264        if args.enable_er {
265            instant = Instant::now();
266            let n_e = map.par_iter_edges().count();
267            let n_e_outside_tol = long_edges.len() + short_edges.len();
268            // if 95%+ edges are in the target length tolerance range, finish early
269            if (n_e_outside_tol as f64 / n_e as f64) < args.target_tolerance {
270                print!(" | {:>12.6e}", instant.elapsed().as_millis());
271                print!(" | {:>13}", "n/a");
272                print!(" | {:>12}", "n/a");
273                println!(" | {:>8}", "n/a");
274                break;
275            }
276            print!(" | {:>12.6e}", instant.elapsed().as_secs_f64());
277        } else {
278            print!(" | {:>12}", "n/a");
279        }
280
281        // -- preallocate darts for cut phase
282        instant = Instant::now();
283        let n_e = long_edges.len();
284        let n_darts = 6 * n_e;
285        let new_darts = if n_unused < n_darts {
286            let tmp = map.allocate_unused_darts(n_darts);
287            (tmp..tmp + n_darts as DartIdType).collect::<Vec<_>>()
288        } else {
289            (1..map.n_darts() as DartIdType)
290                .into_par_iter()
291                .filter(|&d| map.is_unused(d))
292                .take_any(n_darts)
293                .collect()
294        };
295        let alloc_time = instant.elapsed().as_secs_f64();
296        print!(" | {:>17.6e}", batch_time);
297        print!(" | {:>17.6e}", alloc_time);
298
299        // -- cut
300        prof_start!("HCBENCH_REMESH_CUT");
301        print!(" | {:>14}", n_e);
302        instant = Instant::now();
303        long_edges
304            .into_par_iter()
305            .zip(new_darts.par_chunks_exact(6))
306            .for_each(|(e, sl)| {
307                let &[d1, d2, d3, d4, d5, d6] = sl else {
308                    unreachable!()
309                };
310
311                if map.is_i_free::<2>(e as DartIdType) {
312                    while let Err(er) = atomically_with_err(|t| {
313                        let nds = [d1, d2, d3];
314                        for d in nds {
315                            map.claim_dart_tx(t, d)?;
316                        }
317                        cut_outer_edge(t, &map, e, nds)?;
318
319                        if !is_orbit_orientation_consistent(t, &map, d1)? {
320                            abort(SewError::BadGeometry(1, 0, 0))?;
321                        }
322
323                        Ok(())
324                    }) {
325                        match er {
326                            // non-recoverable
327                            SewError::BadGeometry(1, _, _) => {
328                                break;
329                            }
330                            // inconsistency-related, a retry should work
331                            SewError::BadGeometry(_, _, _)
332                            | SewError::FailedLink(_)
333                            | SewError::FailedAttributeOp(_) => continue,
334                        }
335                    }
336                } else {
337                    while let Err(er) = atomically_with_err(|t| {
338                        let nds = [d1, d2, d3, d4, d5, d6];
339                        for d in nds {
340                            map.claim_dart_tx(t, d)?;
341                        }
342
343                        cut_inner_edge(t, &map, e, nds)?;
344
345                        if !is_orbit_orientation_consistent(t, &map, d1)? {
346                            abort(SewError::BadGeometry(1, 0, 0))?;
347                        }
348
349                        Ok(())
350                    }) {
351                        match er {
352                            // non-recoverable
353                            SewError::BadGeometry(1, _, _) => {
354                                break;
355                            }
356                            // inconsistency-related, a retry should work
357                            SewError::BadGeometry(_, _, _)
358                            | SewError::FailedLink(_)
359                            | SewError::FailedAttributeOp(_) => continue,
360                        }
361                    }
362                }
363            });
364        prof_stop!("HCBENCH_REMESH_CUT");
365        print!(" | {:>13.6e}", instant.elapsed().as_secs_f64());
366
367        // -- collapse
368        prof_start!("HCBENCH_REMESH_COLLAPSE");
369        print!(" | {:>19}", short_edges.len());
370        instant = Instant::now();
371        short_edges.into_par_iter().for_each(|e| {
372            while let Err(er) = atomically_with_err(|t| {
373                if map.is_unused_tx(t, e as DartIdType)? {
374                    // needed as some operations may remove some edges besides the one processed
375                    return Ok(());
376                }
377                let e = map.edge_id_tx(t, e)?;
378
379                let (l, r) = (e as DartIdType, map.beta_tx::<1>(t, e as DartIdType)?);
380                let diff = compute_diff_to_target(t, &map, l, r, args.target_length)?;
381                if diff.abs() < args.target_tolerance {
382                    // edge is within target length tolerance; skip the cut/process phase
383                    return Ok(());
384                }
385
386                collapse_edge(t, &map, e)?;
387                Ok(())
388            }) {
389                match er {
390                    // non-recoverable
391                    EdgeCollapseError::FailedCoreOp(SewError::BadGeometry(_, _, _))
392                    | EdgeCollapseError::NonCollapsibleEdge(_)
393                    | EdgeCollapseError::InvertedOrientation => break,
394                    // inconsistency-related, a retry should work
395                    EdgeCollapseError::FailedCoreOp(_)
396                    | EdgeCollapseError::FailedDartRelease(_)
397                    | EdgeCollapseError::BadTopology => continue,
398                    // unreachable due to the first `if` of the tx
399                    EdgeCollapseError::NullEdge => unreachable!(),
400                }
401            }
402        });
403        prof_stop!("HCBENCH_REMESH_COLLAPSE");
404        print!(" | {:>18.6e}", instant.elapsed().as_secs_f64());
405
406        // -- swap
407        prof_start!("HCBENCH_REMESH_SWAP");
408        instant = Instant::now();
409        map.par_iter_edges()
410            .map(|e| {
411                let (l, r) = (e as DartIdType, map.beta::<1>(e as DartIdType));
412                let diff =
413                    atomically(|t| compute_diff_to_target(t, &map, l, r, args.target_length));
414                (e, diff)
415            })
416            .filter(|(_, diff)| diff.abs() > args.target_tolerance)
417            .for_each(|(e, diff)| {
418                let (l, r) = (e as DartIdType, map.beta::<2>(e as DartIdType));
419                if r != NULL_DART_ID {
420                    debug_assert!(
421                        map.force_read_attribute::<FaceAnchor>(map.face_id(l))
422                            .is_some()
423                    );
424                    debug_assert!(
425                        map.force_read_attribute::<FaceAnchor>(map.face_id(r))
426                            .is_some()
427                    );
428                    if let Err(er) = atomically_with_err(|t| {
429                        let (b0l, b0r) = (map.beta_tx::<0>(t, l)?, map.beta_tx::<0>(t, r)?);
430                        let new_diff =
431                            compute_diff_to_target(t, &map, b0l, b0r, args.target_length)?;
432
433                        // if the swap gets the edge length closer to target value, do it
434                        if new_diff.abs() < diff.abs() {
435                            swap_edge(t, &map, e)?;
436                        }
437
438                        // ensure the swap doesn't invert geometry
439                        if !check_tri_orientation(t, &map, l)?
440                            || !check_tri_orientation(t, &map, r)?
441                        {
442                            abort(EdgeSwapError::NotSwappable("swap inverts orientation"))?;
443                        }
444
445                        Ok(())
446                    }) {
447                        match er {
448                            EdgeSwapError::NotSwappable(_)
449                            | EdgeSwapError::FailedCoreOp(_)
450                            | EdgeSwapError::BadTopology => {} // continue
451                            EdgeSwapError::NullEdge | EdgeSwapError::IncompleteEdge => {
452                                unreachable!()
453                            }
454                        }
455                    }
456
457                    debug_assert!(
458                        map.force_read_attribute::<FaceAnchor>(map.face_id(l))
459                            .is_some()
460                    );
461                    debug_assert!(
462                        map.force_read_attribute::<FaceAnchor>(map.face_id(r))
463                            .is_some()
464                    );
465                }
466            });
467        prof_stop!("HCBENCH_REMESH_SWAP");
468        println!(" | {:>14.6e}", instant.elapsed().as_secs_f64());
469
470        debug_assert!(map.par_iter_faces().all(|f| {
471            map.orbit(OrbitPolicy::FaceLinear, f).count() == 3
472                && atomically(|t| check_tri_orientation(t, &map, f as DartIdType))
473        }));
474
475        n += 1;
476        if n >= args.n_rounds.get() {
477            break;
478        }
479    }
480    prof_stop!("HCBENCH_REMESH_MAINLOOP");
481
482    debug_assert!(map.par_iter_faces().all(|f| {
483        map.orbit(OrbitPolicy::FaceLinear, f).count() == 3
484            && atomically(|t| check_tri_orientation(t, &map, f as DartIdType))
485    }));
486
487    map
488}
489
490#[inline]
491fn compute_diff_to_target<T: CoordsFloat>(
492    t: &mut Transaction,
493    map: &CMap2<T>,
494    l: DartIdType,
495    r: DartIdType,
496    target: f64,
497) -> StmClosureResult<f64> {
498    let (vid1, vid2) = (map.vertex_id_tx(t, l)?, map.vertex_id_tx(t, r)?);
499    let (v1, v2) =
500        if let (Some(v1), Some(v2)) = (map.read_vertex(t, vid1)?, map.read_vertex(t, vid2)?) {
501            (v1, v2)
502        } else {
503            retry()?
504        };
505    Ok(((v2 - v1).norm().to_f64().unwrap() - target) / target)
506}
507
508#[inline]
509fn check_tri_orientation<T: CoordsFloat>(
510    t: &mut Transaction,
511    map: &CMap2<T>,
512    d: DartIdType,
513) -> StmClosureResult<bool> {
514    let vid1 = map.vertex_id_tx(t, d)?;
515    let b1 = map.beta_tx::<1>(t, d)?;
516    let vid2 = map.vertex_id_tx(t, b1)?;
517    let b1b1 = map.beta_tx::<1>(t, b1)?;
518    let vid3 = map.vertex_id_tx(t, b1b1)?;
519    let v1 = if let Ok(Some(v)) = map.read_vertex(t, vid1) {
520        v
521    } else {
522        return retry()?;
523    };
524    let v2 = if let Ok(Some(v)) = map.read_vertex(t, vid2) {
525        v
526    } else {
527        return retry()?;
528    };
529    let v3 = if let Ok(Some(v)) = map.read_vertex(t, vid3) {
530        v
531    } else {
532        return retry()?;
533    };
534    Ok(Vertex2::cross_product_from_vertices(&v1, &v2, &v3) > T::zero())
535}