use crate::error::DagResult;
use crate::meshlet_builder::{build_meshlets, MeshletBuildResult};
use crate::simplify::cluster_simplify;
use crate::types::{
IndexedGeometry, OUTPUT_TRIANGLE_BUDGET_DEFAULT,
};
use crate::validation::{budget, validate_input};
#[derive(Debug, Clone)]
#[derive(Default)]
pub struct DagOptions {
pub levels: Option<u32>,
pub max_triangles: Option<u32>,
pub max_vertices: Option<u32>,
pub output_triangle_budget: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct DagLevel {
pub level: u32,
pub error: f64,
pub positions: Vec<f32>,
pub indices: Vec<u32>,
pub meshlet_count: usize,
pub max_vertices: u32,
pub max_triangles: u32,
pub descriptors: Vec<u32>,
pub vertex_remap: Vec<u32>,
pub local_triangle_indices: Vec<u32>,
pub bounds: Vec<f32>,
pub source_triangles: Vec<u32>,
pub cluster_source_spans: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct MeshletDag {
pub levels: Vec<DagLevel>,
pub parents_by_level: Vec<Vec<u32>>,
}
pub fn build_meshlet_dag(geometry: &IndexedGeometry, options: &DagOptions) -> DagResult<MeshletDag> {
let level_count = (options.levels.unwrap_or(4)).clamp(1, 8);
let output_triangle_budget = options.output_triangle_budget.unwrap_or(OUTPUT_TRIANGLE_BUDGET_DEFAULT);
let max_triangles = Some(options.max_triangles.unwrap_or(64));
let input = validate_input(geometry, options.max_vertices, max_triangles)?;
let base = build_meshlets(geometry, options.max_vertices, max_triangles)?;
let identity_source: Vec<u32> = (0..geometry.triangle_count() as u32).collect();
let base_spans = base.cluster_output_spans();
let mut levels = vec![DagLevel {
level: 0,
error: 0.0,
positions: input.geometry.positions.clone(),
indices: input.geometry.indices.clone(),
meshlet_count: base.meshlet_count,
max_vertices: input.max_vertices,
max_triangles: input.max_triangles,
descriptors: base.descriptors.clone(),
vertex_remap: base.vertex_remap.clone(),
local_triangle_indices: base.local_triangle_indices.clone(),
bounds: base.bounds.clone(),
source_triangles: identity_source.clone(),
cluster_source_spans: base_spans.clone(),
}];
let mut current_positions: Vec<f32> = input.geometry.positions.clone();
let mut current_indices: Vec<u32> = input.geometry.indices.clone();
let mut current_error = 0.0f64;
let mut current_cluster_spans = base_spans;
let mut parents_by_level: Vec<Vec<u32>> = Vec::new();
for level in 1..level_count {
let quantized = cluster_simplify(¤t_positions, ¤t_indices, 2.0)?;
if quantized.indices.len() / 3 >= current_indices.len() / 3 {
break; }
budget(
(quantized.indices.len() / 3) as u64,
output_triangle_budget,
"dag level triangles",
)?;
current_error = if current_error == 0.0 {
quantized.max_displacement
} else {
current_error + quantized.max_displacement
};
let built: MeshletBuildResult = build_meshlets(
&IndexedGeometry {
positions: quantized.positions.clone(),
indices: quantized.indices.clone(),
},
options.max_vertices,
max_triangles,
)?;
let coarse_spans = built.cluster_output_spans();
levels.push(DagLevel {
level,
error: current_error,
positions: quantized.positions.clone(),
indices: quantized.indices.clone(),
meshlet_count: built.meshlet_count,
max_vertices: input.max_vertices,
max_triangles: input.max_triangles,
descriptors: built.descriptors.clone(),
vertex_remap: built.vertex_remap.clone(),
local_triangle_indices: built.local_triangle_indices.clone(),
bounds: built.bounds.clone(),
source_triangles: quantized.source_triangles.clone(),
cluster_source_spans: coarse_spans.clone(),
});
let parents =
assign_parents(¤t_cluster_spans, &coarse_spans, &quantized.source_triangles);
parents_by_level.push(parents);
current_positions = quantized.positions;
current_indices = quantized.indices;
current_cluster_spans = coarse_spans;
}
Ok(MeshletDag { levels, parents_by_level })
}
fn assign_parents(fine_spans: &[u32], coarse_spans: &[u32], source_triangles: &[u32]) -> Vec<u32> {
let fine_count = fine_spans.len() / 2;
let coarse_count = coarse_spans.len() / 2;
let mut votes: Vec<std::collections::HashMap<u32, u64>> = vec![std::collections::HashMap::new(); fine_count];
for p in 0..coarse_count {
let start = coarse_spans[p * 2] as usize;
let end = coarse_spans[p * 2 + 1] as usize;
for &src in &source_triangles[start..end] {
if let Some(fine_idx) = binary_search_span(fine_spans, src) {
*votes[fine_idx].entry(p as u32).or_insert(0) += 1;
}
}
}
let mut parent_of_fine = vec![u32::MAX; fine_count];
for (f, per_coarse) in votes.iter().enumerate() {
let mut best = -1i64;
let mut best_votes = -1i64;
for (&coarse, &v) in per_coarse {
if (v as i64) > best_votes || ((v as i64) == best_votes && (coarse as i64) < best) {
best = coarse as i64;
best_votes = v as i64;
}
}
if best >= 0 {
parent_of_fine[f] = best as u32;
}
}
parent_of_fine
}
pub(crate) fn binary_search_span(spans: &[u32], src: u32) -> Option<usize> {
if spans.is_empty() {
return None;
}
let mut lo = 0usize;
let mut hi = spans.len() / 2 - 1;
while lo < hi {
let mid = (lo + hi) / 2;
if spans[mid * 2 + 1] <= src {
lo = mid + 1;
} else {
hi = mid;
}
}
(src >= spans[lo * 2] && src < spans[lo * 2 + 1]).then_some(lo)
}
#[cfg(test)]
mod tests {
use super::*;
fn sphere_geometry(segments: usize, rings: usize) -> IndexedGeometry {
let (positions, indices) = crate::simplify::test_support::test_sphere(segments, rings);
IndexedGeometry { positions, indices }
}
#[test]
fn monotone_levels() {
let dag = build_meshlet_dag(&sphere_geometry(24, 12), &DagOptions { levels: Some(4), ..Default::default() })
.expect("dag");
assert!(dag.levels.len() >= 2);
assert_eq!(dag.levels[0].level, 0);
assert_eq!(dag.levels[0].error, 0.0);
for i in 1..dag.levels.len() {
let (prev, cur) = (&dag.levels[i - 1], &dag.levels[i]);
assert!(cur.indices.len() < prev.indices.len(), "triangles must shrink");
assert!(cur.error > prev.error, "error must grow");
assert!(cur.meshlet_count < prev.meshlet_count, "cluster count must shrink");
}
}
#[test]
fn level0_matches_direct_build() {
let g = sphere_geometry(16, 8);
let dag = build_meshlet_dag(&g, &DagOptions { levels: Some(3), ..Default::default() }).expect("dag");
let direct = build_meshlets(&g, None, Some(64)).expect("build");
assert_eq!(dag.levels[0].meshlet_count, direct.meshlet_count);
assert_eq!(dag.levels[0].descriptors, direct.descriptors);
}
#[test]
fn parents_are_surjective_single_parent() {
let dag = build_meshlet_dag(&sphere_geometry(24, 12), &DagOptions { levels: Some(4), ..Default::default() })
.expect("dag");
for (k, parents) in dag.parents_by_level.iter().enumerate() {
let fine = dag.levels[k].meshlet_count;
let coarse = dag.levels[k + 1].meshlet_count;
assert_eq!(parents.len(), fine);
let mut covered = vec![false; coarse];
for &parent in parents {
assert!(parent < coarse as u32, "parent out of range");
covered[parent as usize] = true;
}
assert!(covered.iter().all(|&c| c), "every coarse cluster must have children");
}
}
#[test]
fn empty_mesh_produces_single_level() {
let dag = build_meshlet_dag(&IndexedGeometry { positions: vec![], indices: vec![] }, &DagOptions::default())
.expect("empty dag");
assert_eq!(dag.levels.len(), 1);
assert_eq!(dag.levels[0].meshlet_count, 0);
assert!(dag.parents_by_level.is_empty());
}
#[test]
fn degenerate_only_mesh_stays_at_level0() {
let g = IndexedGeometry {
positions: vec![0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.0, 0.0, 3.0, 0.0, 0.0],
indices: vec![0, 1, 2, 1, 2, 3],
};
let dag = build_meshlet_dag(&g, &DagOptions::default()).expect("dag");
for level in &dag.levels {
assert_eq!(level.descriptors.len() / 4, level.meshlet_count);
}
}
#[test]
fn level_clamped_to_eight() {
let dag = build_meshlet_dag(&sphere_geometry(64, 32), &DagOptions { levels: Some(99), ..Default::default() })
.expect("dag");
assert!(dag.levels.len() <= 8);
}
#[test]
fn binary_search_span_basics() {
let spans = [0, 3, 3, 5, 5, 9];
assert_eq!(binary_search_span(&spans, 0), Some(0));
assert_eq!(binary_search_span(&spans, 2), Some(0));
assert_eq!(binary_search_span(&spans, 3), Some(1));
assert_eq!(binary_search_span(&spans, 4), Some(1));
assert_eq!(binary_search_span(&spans, 8), Some(2));
assert_eq!(binary_search_span(&spans, 9), None);
assert_eq!(binary_search_span(&[], 0), None);
}
}