Accelerate custom map collision with bounded ordered query clusters

This commit is contained in:
chazmm committed 2026-09-09 00:05:41 +01:00
1 parent 52295937df
commit 8d35f5267b
2 files changed
+90 -7

No files matched your search

+71 -7
View File
@@ -322,19 +322,28 @@ pub(crate) fn collision_world(
.ok_or("Invalid SKATE collision volume")?,
);
}
let bounds = Bounds::from_points(triangles.iter().flat_map(|t| t.triangle.vertices))
.ok_or("SKATE collision bounds empty")?;
let metadata = QueryMetadata {
packed_surfaces,
meshes: vec![QueryMesh {
// Portable maps have no native cluster hierarchy. Bound contiguous ranges
// once at load time so the existing BVH can reject distant geometry. Keep
// triangle order and mesh identity/filter values: contact tie-breaking,
// packed surfaces and adjacency must not change with this acceleration.
let mut meshes = Vec::new();
for start in (0..triangles.len()).step_by(64) {
let end = (start + 64).min(triangles.len());
let bounds = Bounds::from_points(triangles[start..end].iter().flat_map(|t| t.triangle.vertices))
.ok_or("SKATE collision bounds empty")?;
meshes.push(QueryMesh {
geometry: 0, rejection_flags: 0,
triangle_range: 0..triangles.len(),
triangle_range: start..end,
local_to_world: RetailAffineTransform::IDENTITY,
world_to_local: RetailAffineTransform::IDENTITY,
local_bounds: bounds,
matching_group: -1,
pool: QueryPool::Ground,
}],
});
}
let metadata = QueryMetadata {
packed_surfaces,
meshes,
static_edges: vec![],
island_flags: 0,
};
@@ -676,6 +685,61 @@ pub(crate) fn spawn(
#[cfg(test)]
mod tests {
use super::*;
fn compare_cluster_queries(world: &BoardWorld) -> (usize, usize) {
let mut metadata = world.query_metadata().unwrap().clone();
let mut single = metadata.meshes[0].clone();
single.triangle_range = 0..world.triangles().len();
single.local_bounds = Bounds::from_points(world.triangles().iter().flat_map(|t| t.triangle.vertices)).unwrap();
metadata.meshes = vec![single];
let baseline = BoardWorld::with_query_metadata(world.triangles().to_vec(), metadata).unwrap();
let mut visited = [0usize; 2];
let mut times = [std::time::Duration::ZERO; 2];
for triangle in world.triangles().iter().step_by((world.triangles().len() / 256).max(1)) {
let p = triangle.triangle.vertices[0];
let start = Vector3::new(p.x, p.y + 1., p.z);
let end = Vector3::new(p.x, p.y - 1., p.z);
let mut candidates = Vec::new();
for (i, scene) in [&baseline, world].into_iter().enumerate() {
let bounds = scene.line_candidate_bounds(start, end, 0.1);
visited[i] += scene.candidate_ranges(bounds).iter().map(|r| r.len()).sum::<usize>();
let now = std::time::Instant::now();
candidates.push(scene.line_candidates(start, end, 0.1).map(|(id, _)| id).collect::<Vec<_>>());
times[i] += now.elapsed();
}
assert_eq!(candidates[0], candidates[1], "candidate identity/order changed");
}
eprintln!("CUSTOM_QUERY_BENCH baseline_triangles={} clustered_triangles={} baseline_ms={} clustered_ms={} clusters={}",
visited[0], visited[1], times[0].as_secs_f64()*1000., times[1].as_secs_f64()*1000.,
world.query_metadata().unwrap().meshes.len());
(visited[0], visited[1])
}
#[test]
fn portable_collision_clusters_preserve_queries_and_cull_distant_geometry() {
let mut map = demo();
let source = map.geometry.collision.remove(0);
map.geometry.collision.clear();
for i in 0..256 {
map.geometry.collision.push(skate_data::skate_map::Collision {
points: source.points.map(|p| [p[0] + i as f32 * 100., p[1], p[2]]),
surface: source.surface, material: source.material, native_edges: source.native_edges,
});
}
let world = collision_world(&map, material()).unwrap();
assert_eq!(world.query_metadata().unwrap().meshes.len(), 4);
let (before, after) = compare_cluster_queries(&world);
assert!(after < before / 2);
}
#[test]
#[ignore = "requires a supplied map; static collision-query benchmark only"]
fn supplied_custom_map_collision_query_benchmark() {
let path = std::env::var("SKATE_TEST_CUSTOM_MAP").unwrap();
let map = SkateMap::load(std::path::Path::new(&path)).unwrap();
let world = collision_world(&map, material()).unwrap();
let (before, after) = compare_cluster_queries(&world);
assert!(after < before);
}
fn demo() -> SkateMap {
SkateMap::parse(include_bytes!("../../../maps/format-demo.skate")).unwrap()
}
+19
View File
@@ -90,3 +90,22 @@ XYZ/radius. Six contact-toolkit tests pass. The existing
`embedded_static_rwcm_hits_distinct_actor_query_ids` test fails its metadata
expectation (`matching_group == -1 && geometry == 0`) before executing the
changed query path; that unrelated fixture expectation remains unchanged.
## Custom collision performance
Portable map collision previously occupied one query mesh, so nearby queries
scanned the entire map. It now uses contiguous 64-triangle bounds in the
existing static hierarchy, built once during loading. Triangle order, geometry
IDs, filters, packed surfaces and adjacency remain unchanged. Native RWCM
clusters retain their existing path.
A static query benchmark on the supplied custom map (89,613 triangles) sampled
257 local segments with identical candidate IDs/order before and after.
Triangle checks fell from 23,030,541 to 4,198,157; measured candidate-query time
fell from 16.51 ms to 3.82 ms total (about 4.3x). A 16-triangle partition was
also tried, but had higher query time (4.69 ms) despite fewer triangle checks.
These are CPU microbenchmark results, not gameplay FPS or GPU measurements.
Ten map tests passed. The supplied-map benchmark is opt-in via
`SKATE_TEST_CUSTOM_MAP`. Frame timing can be captured with the existing
`SKATE_PERF_REPORT` environment setting (10-second warmup, report/exit after
25 seconds); no automated gameplay was run.