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