use std::sync::OnceLock;
use std::time::{Duration, Instant};
use glam::Vec2;
use polyanya::{Mesh, Path, PolyanyaFile};
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng(seed ^ 0x9E37_79B9_7F4A_7C15)
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn f32(&mut self) -> f32 {
(self.next_u64() >> 40) as f32 / (1u32 << 24) as f32
}
fn range(&mut self, min: f32, max: f32) -> f32 {
min + self.f32() * (max - min)
}
fn below(&mut self, n: usize) -> usize {
(self.next_u64() % n as u64) as usize
}
}
fn seed() -> u64 {
std::env::var("POLYANYA_FUZZ_SEED")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0x5EED_C0FF_EE00_1234)
}
fn iterations(default: usize) -> usize {
std::env::var("POLYANYA_FUZZ_ITERATIONS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(default)
}
fn budget() -> Option<Duration> {
std::env::var("POLYANYA_FUZZ_SECONDS")
.ok()
.and_then(|s| s.parse().ok())
.map(Duration::from_secs)
}
const SAMPLE_EVERY: f32 = 0.25;
fn tolerance(length: f32) -> f32 {
1e-3_f32.max(length.abs() * 1e-4)
}
fn close(a: f32, b: f32) -> bool {
(a - b).abs() <= tolerance(a.abs().max(b.abs()))
}
fn bounds(mesh: &Mesh) -> (Vec2, Vec2) {
let mut min = Vec2::splat(f32::MAX);
let mut max = Vec2::splat(f32::MIN);
for layer in &mesh.layers {
for vertex in &layer.vertices {
min = min.min(vertex.coords + layer.offset);
max = max.max(vertex.coords + layer.offset);
}
}
(min, max)
}
fn strictly_on_mesh(mesh: &Mesh, point: Vec2) -> bool {
mesh.get_point_layer(point).iter().any(|coords| {
let Some(layer) = coords.layer().and_then(|l| mesh.layers.get(l as usize)) else {
return false;
};
let local = point - layer.offset;
let corners = &layer.polygons[coords.polygon() as usize].vertices;
let n = corners.len();
n >= 3
&& (0..n).all(|i| {
let a = layer.vertices[corners[i] as usize].coords;
let b = layer.vertices[corners[(i + 1) % n] as usize].coords;
(b - a).perp_dot(local - a) >= -1e-5
})
})
}
fn point_by_rejection(mesh: &Mesh, rng: &mut Rng, (min, max): (Vec2, Vec2)) -> Option<Vec2> {
for _ in 0..64 {
let point = Vec2::new(rng.range(min.x, max.x), rng.range(min.y, max.y));
if strictly_on_mesh(mesh, point) {
return Some(point);
}
}
None
}
fn point_in_random_polygon(mesh: &Mesh, rng: &mut Rng) -> Option<Vec2> {
let layer_index = rng.below(mesh.layers.len());
let layer = &mesh.layers[layer_index];
for _ in 0..16 {
let polygon = &layer.polygons[rng.below(layer.polygons.len())];
if polygon.vertices.is_empty() {
continue;
}
let corners = polygon
.vertices
.iter()
.map(|v| layer.vertices[*v as usize].coords + layer.offset)
.collect::<Vec<_>>();
let point = match rng.below(4) {
0 => corners[rng.below(corners.len())],
1 => {
let first = rng.below(corners.len());
(corners[first] + corners[(first + 1) % corners.len()]) / 2.0
}
_ => {
let weights = corners.iter().map(|_| rng.f32()).collect::<Vec<_>>();
let total: f32 = weights.iter().sum();
if total <= 0.0 {
continue;
}
corners
.iter()
.zip(&weights)
.fold(Vec2::ZERO, |acc, (corner, weight)| acc + *corner * *weight)
/ total
}
};
if strictly_on_mesh(mesh, point) {
return Some(point);
}
}
None
}
fn random_point(mesh: &Mesh, rng: &mut Rng, bounds: (Vec2, Vec2)) -> Option<Vec2> {
if rng.below(2) == 0 {
point_by_rejection(mesh, rng, bounds)
} else {
point_in_random_polygon(mesh, rng)
}
}
fn pinches_here(mesh: &Mesh, point: Vec2) -> bool {
mesh.get_point_layer(point).iter().any(|coords| {
let layer_index = coords.layer().unwrap_or(0);
let Some(layer) = mesh.layers.get(layer_index as usize) else {
return false;
};
let Some(vertex) = layer.polygons[coords.polygon() as usize]
.vertices
.iter()
.map(|index| &layer.vertices[*index as usize])
.find(|vertex| (vertex.coords + layer.offset).distance_squared(point) < 1.0e-6)
else {
return false;
};
let around = vertex
.polygons
.iter()
.filter(|polygon| **polygon != u32::MAX && (**polygon >> 24) as u8 == layer_index)
.map(|polygon| (polygon & 0x00FF_FFFF) as usize)
.collect::<Vec<_>>();
if around.len() < 2 {
return false;
}
let shares_an_edge = |a: usize, b: usize| {
let (a, b) = (&layer.polygons[a].vertices, &layer.polygons[b].vertices);
a.iter().filter(|vertex| b.contains(vertex)).count() >= 2
};
let mut reached = vec![false; around.len()];
reached[0] = true;
let mut spreading = true;
while spreading {
spreading = false;
for from in 0..around.len() {
if !reached[from] {
continue;
}
for to in 0..around.len() {
if !reached[to] && shares_an_edge(around[from], around[to]) {
reached[to] = true;
spreading = true;
}
}
}
}
reached.iter().any(|reached| !reached)
})
}
struct Query {
mesh: &'static str,
seed: u64,
from: Vec2,
to: Vec2,
}
impl std::fmt::Display for Query {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} from {:?} to {:?} (replay with POLYANYA_FUZZ_SEED={})",
self.mesh, self.from, self.to, self.seed
)
}
}
fn check_path(mesh: &Mesh, from: Vec2, to: Vec2, path: &Path, query: &Query) {
assert!(
path.length.is_finite(),
"length is {}, {query}",
path.length
);
let straight = from.distance(to);
assert!(
path.length >= straight - tolerance(straight),
"length {} is shorter than the straight line {straight}, {query}",
path.length
);
assert!(!path.path.is_empty(), "empty path, {query}");
let last = *path.path.last().unwrap();
assert!(
close(last.x, to.x) && close(last.y, to.y),
"path ends at {last:?} instead of the goal, {query}"
);
let summed = path
.path
.iter()
.fold((0.0, from), |(total, previous), point| {
(total + previous.distance(*point), *point)
})
.0;
assert!(
close(summed, path.length),
"length {} but the path is {summed} long, {query}",
path.length
);
let mut previous = from;
for point in &path.path {
assert!(
mesh.point_in_mesh(*point),
"waypoint {point:?} is off the mesh, {query}"
);
let steps = ((previous.distance(*point) / SAMPLE_EVERY).ceil() as usize).max(1);
for step in 1..steps {
let sample = previous.lerp(*point, step as f32 / steps as f32);
assert!(
mesh.point_in_mesh(sample),
"the path leaves the mesh at {sample:?}, between {previous:?} and \
{point:?}, {query}"
);
}
previous = *point;
}
let polygons = path.polygons();
assert!(
!polygons.is_empty(),
"path goes through no polygon, {query}"
);
for (layer_index, polygon_index) in &polygons {
let layer = mesh
.layers
.get(*layer_index as usize)
.unwrap_or_else(|| panic!("path goes through unknown layer {layer_index}, {query}"));
assert!(
(*polygon_index as usize) < layer.polygons.len(),
"path goes through polygon {polygon_index} of layer {layer_index}, \
which only has {} polygons, {query}",
layer.polygons.len()
);
}
}
fn check_query(mesh: &Mesh, query: &Query, third: Option<Vec2>) {
let (from, to) = (query.from, query.to);
let Some(path) = mesh.path(from, to) else {
assert!(
mesh.path(to, from).is_none(),
"no path one way but a path the other way, {query}"
);
return;
};
check_path(mesh, from, to, &path, query);
let back = mesh
.path(to, from)
.unwrap_or_else(|| panic!("path one way but none back, {query}"));
check_path(
mesh,
to,
from,
&back,
&Query {
from: to,
to: from,
..*query
},
);
assert!(
close(path.length, back.length),
"costs {} one way and {} back, {query}",
path.length,
back.length
);
if path.path.len() > 1 && !pinches_here(mesh, path.path[0]) {
let turn = path.path[0];
let remaining = mesh
.path(turn, to)
.unwrap_or_else(|| panic!("no path from the turn {turn:?} to the goal, {query}"));
let expected = path.length - from.distance(turn);
assert!(
remaining.length >= expected - tolerance(expected),
"the leg from the first turn {turn:?} costs {}, less than the {expected} the \
full path leaves for it, so the full path is not optimal, {query}",
remaining.length
);
}
if let Some(third) = third {
if let (Some(first), Some(second)) = (mesh.path(from, third), mesh.path(third, to)) {
let detour = first.length + second.length;
assert!(
path.length <= detour + tolerance(detour),
"going through {third:?} costs {detour}, less than the direct {}, {query}",
path.length
);
}
}
}
fn fuzz_mesh(name: &'static str, mesh: &Mesh, count: usize) {
let seed = seed();
let deadline = budget().map(|budget| Instant::now() + budget);
match deadline {
Some(_) => eprintln!(
"fuzzing {name} for {:?}, POLYANYA_FUZZ_SEED={seed}",
budget().unwrap()
),
None => eprintln!("fuzzing {name} with {count} queries, POLYANYA_FUZZ_SEED={seed}"),
}
let count = if deadline.is_some() {
usize::MAX
} else {
count
};
let mut rng = Rng::new(seed);
let bounds = bounds(mesh);
let mut attempted = 0;
let mut ran = 0;
for i in 0..count {
if let (0, Some(deadline)) = (i % 64, deadline) {
if Instant::now() >= deadline {
break;
}
}
attempted += 1;
let (Some(from), Some(to)) = (
random_point(mesh, &mut rng, bounds),
random_point(mesh, &mut rng, bounds),
) else {
continue;
};
let third = random_point(mesh, &mut rng, bounds);
check_query(
mesh,
&Query {
mesh: name,
seed,
from,
to,
},
third,
);
ran += 1;
}
eprintln!("{name}: {ran} queries checked");
assert!(
ran > 0 && ran > attempted / 2,
"only {ran} of {attempted} queries could be generated on {name}"
);
}
fn mesh_from(path: &str) -> Mesh {
PolyanyaFile::from_file(path).try_into().unwrap()
}
#[test]
fn fuzz_arena() {
fuzz_mesh(
"arena",
&mesh_from("meshes/v2/arena.mesh"),
iterations(2000),
);
}
#[test]
fn fuzz_scene_mp_2p_01() {
fuzz_mesh(
"scene_mp_2p_01",
&mesh_from("meshes/v3/scene_mp_2p_01.mesh"),
iterations(500),
);
}
#[test]
fn fuzz_aurora() {
fuzz_mesh("aurora", aurora_mesh(), iterations(200));
}
fn aurora_mesh() -> &'static Mesh {
static AURORA: OnceLock<Mesh> = OnceLock::new();
AURORA.get_or_init(|| mesh_from("meshes/v2/aurora-merged.mesh"))
}
fn point_just_off_mesh(mesh: &Mesh, rng: &mut Rng) -> Option<Vec2> {
let inside = point_in_random_polygon(mesh, rng)?;
let angle = rng.range(0.0, std::f32::consts::TAU);
let direction = Vec2::new(angle.cos(), angle.sin());
let mut step = 0.002;
while step < 4.0 {
if !strictly_on_mesh(mesh, inside + direction * step) {
let point = inside + direction * (step + rng.range(0.0, 0.05));
return (!strictly_on_mesh(mesh, point) && mesh.point_in_mesh(point)).then_some(point);
}
step *= 1.4;
}
None
}
#[test]
fn fuzz_goals_just_off_mesh() {
let seed = seed();
eprintln!("fuzzing goals just off the mesh, POLYANYA_FUZZ_SEED={seed}");
let mut rng = Rng::new(seed);
let mesh = &mesh_from("meshes/v3/scene_mp_2p_01.mesh");
let deadline = budget().map(|budget| Instant::now() + budget);
let count = if deadline.is_some() {
usize::MAX
} else {
iterations(2000)
};
let mut ran = 0;
for i in 0..count {
if let (0, Some(deadline)) = (i % 64, deadline) {
if Instant::now() >= deadline {
break;
}
}
let (Some(from), Some(to)) = (
point_in_random_polygon(mesh, &mut rng),
point_just_off_mesh(mesh, &mut rng),
) else {
continue;
};
let Some(path) = mesh.path(from, to) else {
continue;
};
ran += 1;
let query = Query {
mesh: "scene_mp_2p_01",
seed,
from,
to,
};
assert!(!path.path.is_empty(), "empty path, {query}");
let last = *path.path.last().unwrap();
assert!(
close(last.x, to.x) && close(last.y, to.y),
"path ends at {last:?} instead of the goal, {query}"
);
let summed = path
.path
.iter()
.fold((0.0, from), |(total, previous), point| {
(total + previous.distance(*point), *point)
})
.0;
assert!(
close(summed, path.length),
"length {} but the path is {summed} long, {query}",
path.length
);
}
eprintln!("goals just off the mesh: {ran} queries checked");
assert!(ran > 0, "no query could be generated");
}
#[test]
fn fuzz_off_mesh_queries() {
let seed = seed();
eprintln!("fuzzing off-mesh queries, POLYANYA_FUZZ_SEED={seed}");
let mut rng = Rng::new(seed);
let mesh = aurora_mesh();
let (min, max) = bounds(mesh);
let size = max - min;
let deadline = budget().map(|budget| Instant::now() + budget);
let count = if deadline.is_some() {
usize::MAX
} else {
iterations(500)
};
for i in 0..count {
if let (0, Some(deadline)) = (i % 64, deadline) {
if Instant::now() >= deadline {
break;
}
}
let outside = Vec2::new(
rng.range(max.x + size.x, max.x + 10.0 * size.x),
rng.range(max.y + size.y, max.y + 10.0 * size.y),
);
assert!(
!mesh.point_in_mesh(outside),
"{outside:?} is somehow in the mesh, POLYANYA_FUZZ_SEED={seed}"
);
let inside = point_in_random_polygon(mesh, &mut rng).unwrap();
assert_eq!(
mesh.path(outside, inside),
None,
"path from outside {outside:?} to {inside:?}, POLYANYA_FUZZ_SEED={seed}"
);
assert_eq!(
mesh.path(inside, outside),
None,
"path from {inside:?} to outside {outside:?}, POLYANYA_FUZZ_SEED={seed}"
);
}
}