avoid alloc for curve path & slight bezier optimization
This commit is contained in:
+71
-77
@@ -24,16 +24,13 @@ impl BezierBuffers {
|
|||||||
fn new(len: usize) -> Self {
|
fn new(len: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
buf1: vec![Pos2::zero(); len],
|
buf1: vec![Pos2::zero(); len],
|
||||||
buf2: vec![Pos2::zero(); (len - 1) * 2 + 1],
|
buf2: vec![Pos2::zero(); len],
|
||||||
buf3: vec![Pos2::zero(); len],
|
buf3: vec![Pos2::zero(); len],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove Default when cleaned up
|
|
||||||
#[derive(Default)]
|
|
||||||
struct CircularArcProperties {
|
struct CircularArcProperties {
|
||||||
is_valid: bool,
|
|
||||||
theta_start: f32,
|
theta_start: f32,
|
||||||
theta_range: f32,
|
theta_range: f32,
|
||||||
direction: f32,
|
direction: f32,
|
||||||
@@ -121,19 +118,14 @@ impl Curve {
|
|||||||
let segment_vertices = &vertices[start..i + 1];
|
let segment_vertices = &vertices[start..i + 1];
|
||||||
let segment_kind = points[start].kind.unwrap_or(PathType::Linear);
|
let segment_kind = points[start].kind.unwrap_or(PathType::Linear);
|
||||||
|
|
||||||
// TODO: push onto `path` directly?
|
Self::calculate_subpath(&mut path, segment_vertices, segment_kind);
|
||||||
let sub_path = Self::calculate_subpath(segment_vertices, segment_kind);
|
|
||||||
|
|
||||||
for t in sub_path {
|
|
||||||
if path.last().filter(|&l| l == &t).is_none() {
|
|
||||||
path.push(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// * Start the new segment at the current vertex
|
// * Start the new segment at the current vertex
|
||||||
start = i;
|
start = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path.dedup();
|
||||||
|
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,37 +195,29 @@ impl Curve {
|
|||||||
cumulative_len
|
cumulative_len
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_subpath(sub_points: &[Pos2], kind: PathType) -> Vec<Pos2> {
|
fn calculate_subpath(path: &mut Vec<Pos2>, sub_points: &[Pos2], kind: PathType) {
|
||||||
match kind {
|
match kind {
|
||||||
PathType::Bezier => Self::approximate_bezier(sub_points),
|
PathType::Bezier => Self::approximate_bezier(path, sub_points),
|
||||||
PathType::Catmull => Self::approximate_catmull(sub_points),
|
PathType::Catmull => Self::approximate_catmull(path, sub_points),
|
||||||
PathType::Linear => Self::approximate_linear(sub_points),
|
PathType::Linear => Self::approximate_linear(path, sub_points),
|
||||||
PathType::PerfectCurve => {
|
PathType::PerfectCurve => {
|
||||||
if let [a, b, c] = sub_points {
|
if let [a, b, c] = sub_points {
|
||||||
let sub_path = Self::approximate_circular_arc(*a, *b, *c);
|
Self::approximate_circular_arc(path, *a, *b, *c)
|
||||||
|
} else {
|
||||||
if !sub_path.is_empty() {
|
Self::approximate_bezier(path, sub_points)
|
||||||
return sub_path;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::approximate_bezier(sub_points)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn approximate_bezier(points: &[Pos2]) -> Vec<Pos2> {
|
fn approximate_bezier(path: &mut Vec<Pos2>, points: &[Pos2]) {
|
||||||
let mut path = Vec::new(); // TODO: argument?
|
|
||||||
let mut bufs = BezierBuffers::new(points.len()); // TODO: argument?
|
let mut bufs = BezierBuffers::new(points.len()); // TODO: argument?
|
||||||
|
|
||||||
Self::approximate_bspline(&mut path, points, &mut bufs);
|
Self::approximate_bspline(path, points, &mut bufs);
|
||||||
|
|
||||||
path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn approximate_catmull(points: &[Pos2]) -> Vec<Pos2> {
|
fn approximate_catmull(path: &mut Vec<Pos2>, points: &[Pos2]) {
|
||||||
// TODO: argument?
|
path.reserve_exact((points.len() - 1) * CATMULL_DETAIL * 2);
|
||||||
let mut result = Vec::with_capacity((points.len() - 1) * CATMULL_DETAIL * 2);
|
|
||||||
|
|
||||||
let catmull_detail = CATMULL_DETAIL as f32;
|
let catmull_detail = CATMULL_DETAIL as f32;
|
||||||
|
|
||||||
@@ -252,24 +236,21 @@ impl Curve {
|
|||||||
let p1 = Self::catmull_find_point(v1, v2, v3, v4, c as f32 / catmull_detail);
|
let p1 = Self::catmull_find_point(v1, v2, v3, v4, c as f32 / catmull_detail);
|
||||||
let p2 = Self::catmull_find_point(v1, v2, v3, v4, (c + 1) as f32 / catmull_detail);
|
let p2 = Self::catmull_find_point(v1, v2, v3, v4, (c + 1) as f32 / catmull_detail);
|
||||||
|
|
||||||
result.push(p1);
|
path.push(p1);
|
||||||
result.push(p2);
|
path.push(p2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn approximate_linear(points: &[Pos2]) -> Vec<Pos2> {
|
fn approximate_linear(path: &mut Vec<Pos2>, points: &[Pos2]) {
|
||||||
points.to_owned()
|
path.extend(points)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn approximate_circular_arc(a: Pos2, b: Pos2, c: Pos2) -> Vec<Pos2> {
|
fn approximate_circular_arc(path: &mut Vec<Pos2>, a: Pos2, b: Pos2, c: Pos2) {
|
||||||
let pr = Self::circular_arc_properties(a, b, c);
|
let pr = match Self::circular_arc_properties(a, b, c) {
|
||||||
|
Some(pr) => pr,
|
||||||
if !pr.is_valid {
|
None => return Self::approximate_bezier(path, &[a, b, c]),
|
||||||
return Self::approximate_bezier(&[a, b, c]);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// * We select the amount of points for the approximation by requiring the discrete curvature
|
// * We select the amount of points for the approximation by requiring the discrete curvature
|
||||||
// * to be smaller than the provided tolerance. The exact angle required to meet the tolerance
|
// * to be smaller than the provided tolerance. The exact angle required to meet the tolerance
|
||||||
@@ -284,22 +265,23 @@ impl Curve {
|
|||||||
((pr.theta_range / divisor).ceil() as usize).max(2)
|
((pr.theta_range / divisor).ceil() as usize).max(2)
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: argument?
|
path.reserve_exact(amount_points);
|
||||||
let mut output = Vec::with_capacity(amount_points);
|
let divisor = (amount_points - 1) as f32;
|
||||||
|
let directed_range = pr.direction * pr.theta_range;
|
||||||
|
|
||||||
for i in 0..amount_points {
|
let subpath = (0..amount_points).map(|i| {
|
||||||
let fract = i as f32 / (amount_points - 1) as f32;
|
let fract = i as f32 / divisor;
|
||||||
let theta = pr.theta_start + pr.direction * fract * pr.theta_range;
|
let theta = pr.theta_start + fract * directed_range;
|
||||||
let (sin, cos) = theta.sin_cos();
|
let (sin, cos) = theta.sin_cos();
|
||||||
let origin = Pos2 { x: cos, y: sin };
|
let origin = Pos2 { x: cos, y: sin };
|
||||||
|
|
||||||
output.push(pr.centre + origin * pr.radius);
|
pr.centre + origin * pr.radius
|
||||||
}
|
});
|
||||||
|
|
||||||
output
|
path.extend(subpath);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn approximate_bspline(result: &mut Vec<Pos2>, points: &[Pos2], bufs: &mut BezierBuffers) {
|
fn approximate_bspline(path: &mut Vec<Pos2>, points: &[Pos2], bufs: &mut BezierBuffers) {
|
||||||
let p = points.len();
|
let p = points.len();
|
||||||
|
|
||||||
let mut to_flatten = Vec::new();
|
let mut to_flatten = Vec::new();
|
||||||
@@ -323,7 +305,7 @@ impl Curve {
|
|||||||
// * an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation
|
// * an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation
|
||||||
// * of the bezier curve represented by our control points, consisting of the same amount
|
// * of the bezier curve represented by our control points, consisting of the same amount
|
||||||
// * of points as there are control points.
|
// * of points as there are control points.
|
||||||
Self::bezier_approximate(&parent, result, bufs);
|
Self::bezier_approximate(&parent, path, bufs);
|
||||||
free_bufs.push(parent);
|
free_bufs.push(parent);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -349,7 +331,7 @@ impl Curve {
|
|||||||
to_flatten.push(parent);
|
to_flatten.push(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push(points[p - 1]);
|
path.push(points[p - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bezier_is_flat_enough(points: &[Pos2]) -> bool {
|
fn bezier_is_flat_enough(points: &[Pos2]) -> bool {
|
||||||
@@ -362,9 +344,8 @@ impl Curve {
|
|||||||
.any(|((&prev, &curr), &next)| (prev - curr * 2.0 + next).length_squared() > limit)
|
.any(|((&prev, &curr), &next)| (prev - curr * 2.0 + next).length_squared() > limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bezier_subdivide(points: &[Pos2], l: &mut [Pos2], r: &mut [Pos2], buf: &mut [Pos2]) {
|
fn bezier_subdivide(points: &[Pos2], l: &mut [Pos2], r: &mut [Pos2], midpoints: &mut [Pos2]) {
|
||||||
let count = points.len();
|
let count = points.len();
|
||||||
let midpoints = buf;
|
|
||||||
midpoints[..count].copy_from_slice(&points[..count]);
|
midpoints[..count].copy_from_slice(&points[..count]);
|
||||||
|
|
||||||
for i in (1..count).rev() {
|
for i in (1..count).rev() {
|
||||||
@@ -381,25 +362,40 @@ impl Curve {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// * https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm
|
// * https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm
|
||||||
fn bezier_approximate(points: &[Pos2], output: &mut Vec<Pos2>, bufs: &mut BezierBuffers) {
|
fn bezier_approximate(points: &[Pos2], path: &mut Vec<Pos2>, bufs: &mut BezierBuffers) {
|
||||||
let count = points.len();
|
let count = points.len();
|
||||||
let r = &mut bufs.buf1;
|
|
||||||
let l = &mut bufs.buf2;
|
|
||||||
|
|
||||||
Self::bezier_subdivide(points, l, r, &mut bufs.buf3);
|
let BezierBuffers {
|
||||||
l[count..2 * count - 1].copy_from_slice(&r[1..count]);
|
buf1: l,
|
||||||
output.push(points[0]);
|
buf2: r,
|
||||||
|
buf3: midpoints,
|
||||||
|
} = bufs;
|
||||||
|
|
||||||
let new_points = l
|
Self::bezier_subdivide(points, l, r, midpoints);
|
||||||
|
path.push(points[0]);
|
||||||
|
|
||||||
|
let l = &l[..count];
|
||||||
|
let r = &r[1..count];
|
||||||
|
|
||||||
|
let left_iter = l
|
||||||
.iter()
|
.iter()
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.zip(l.iter().skip(2))
|
.zip(l.iter().skip(2))
|
||||||
.zip(l.iter().skip(3))
|
.zip(l.iter().skip(3))
|
||||||
.step_by(2)
|
.step_by(2);
|
||||||
.take(count.saturating_sub(2))
|
|
||||||
|
let right_iter = r
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.zip(r.iter().skip(2))
|
||||||
|
.zip(r.iter().skip(3))
|
||||||
|
.step_by(2);
|
||||||
|
|
||||||
|
let subpath = left_iter
|
||||||
|
.chain(right_iter)
|
||||||
.map(|((&prev, &curr), &next)| (prev + curr * 2.0 + next) * 0.25);
|
.map(|((&prev, &curr), &next)| (prev + curr * 2.0 + next) * 0.25);
|
||||||
|
|
||||||
output.extend(new_points);
|
path.extend(subpath);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn catmull_find_point(v1: Pos2, v2: Pos2, v3: Pos2, v4: Pos2, t: f32) -> Pos2 {
|
fn catmull_find_point(v1: Pos2, v2: Pos2, v3: Pos2, v4: Pos2, t: f32) -> Pos2 {
|
||||||
@@ -421,22 +417,21 @@ impl Curve {
|
|||||||
Pos2 { x, y }
|
Pos2 { x, y }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn circular_arc_properties(a: Pos2, b: Pos2, c: Pos2) -> CircularArcProperties {
|
fn circular_arc_properties(a: Pos2, b: Pos2, c: Pos2) -> Option<CircularArcProperties> {
|
||||||
// * If we have a degenerate triangle where a side-length is almost zero,
|
// * If we have a degenerate triangle where a side-length is almost zero,
|
||||||
// * then give up and fallback to a more numerically stable method.
|
// * then give up and fallback to a more numerically stable method.
|
||||||
if ((b.y - a.y) * (c.x - a.x) - (b.x - a.x) * (c.y - a.y)).abs() <= f32::EPSILON {
|
if ((b.y - a.y) * (c.x - a.x) - (b.x - a.x) * (c.y - a.y)).abs() <= f32::EPSILON {
|
||||||
// * Implicitly sets `is_valid` to false
|
return None;
|
||||||
return CircularArcProperties::default();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let d = 2.0 * (a.x * (b - c).y + b.x * (c - a).y + c.x * (a - b).y);
|
let d = 2.0 * (a.x * (b - c).y + b.x * (c - a).y + c.x * (a - b).y);
|
||||||
let a_s_q = a.length_squared();
|
let a_sq = a.length_squared();
|
||||||
let b_s_q = b.length_squared();
|
let b_sq = b.length_squared();
|
||||||
let c_s_q = c.length_squared();
|
let c_sq = c.length_squared();
|
||||||
|
|
||||||
let centre = Pos2 {
|
let centre = Pos2 {
|
||||||
x: (a_s_q * (b - c).y + b_s_q * (c - a).y + c_s_q * (a - b).y) / d,
|
x: (a_sq * (b - c).y + b_sq * (c - a).y + c_sq * (a - b).y) / d,
|
||||||
y: ((c - b).x + b_s_q * (a - c).x + c_s_q * (b - a).x) / d,
|
y: ((c - b).x + b_sq * (a - c).x + c_sq * (b - a).x) / d,
|
||||||
};
|
};
|
||||||
|
|
||||||
let d_a = a - centre;
|
let d_a = a - centre;
|
||||||
@@ -468,14 +463,13 @@ impl Curve {
|
|||||||
theta_range = 2.0 * PI - theta_range;
|
theta_range = 2.0 * PI - theta_range;
|
||||||
}
|
}
|
||||||
|
|
||||||
CircularArcProperties {
|
Some(CircularArcProperties {
|
||||||
is_valid: true,
|
|
||||||
theta_start,
|
theta_start,
|
||||||
theta_range,
|
theta_range,
|
||||||
direction,
|
direction,
|
||||||
radius,
|
radius,
|
||||||
centre,
|
centre,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
fn custom_osu() {
|
fn custom_osu() {
|
||||||
use std::{fs::File, time::Instant};
|
use std::{fs::File, time::Instant};
|
||||||
|
|
||||||
let path = "E:Games/osu!/beatmaps/2753127.osu";
|
let path = "./maps/2753127.osu";
|
||||||
// let path = "E:Games/osu!/beatmaps/2571051.osu";
|
// let path = "E:Games/osu!/beatmaps/2571051.osu";
|
||||||
let file = File::open(path).unwrap();
|
let file = File::open(path).unwrap();
|
||||||
|
|
||||||
@@ -352,14 +352,15 @@ fn custom_osu() {
|
|||||||
let iters = 100;
|
let iters = 100;
|
||||||
let accum = start.elapsed();
|
let accum = start.elapsed();
|
||||||
|
|
||||||
let mut accum = accum;
|
// * Tiny benchmark for map parsing
|
||||||
|
// let mut accum = accum;
|
||||||
|
|
||||||
for _ in 0..iters {
|
// for _ in 0..iters {
|
||||||
let file = File::open(path).unwrap();
|
// let file = File::open(path).unwrap();
|
||||||
let start = Instant::now();
|
// let start = Instant::now();
|
||||||
let _map = Beatmap::parse(file).unwrap();
|
// let _map = Beatmap::parse(file).unwrap();
|
||||||
accum += start.elapsed();
|
// accum += start.elapsed();
|
||||||
}
|
// }
|
||||||
|
|
||||||
println!("Parsing average: {:?}", accum / iters);
|
println!("Parsing average: {:?}", accum / iters);
|
||||||
|
|
||||||
@@ -369,7 +370,7 @@ fn custom_osu() {
|
|||||||
let iters = 500;
|
let iters = 500;
|
||||||
let accum = start.elapsed();
|
let accum = start.elapsed();
|
||||||
|
|
||||||
// * Tiny benchmark
|
// * Tiny benchmark for pp calculation
|
||||||
// let mut accum = accum;
|
// let mut accum = accum;
|
||||||
|
|
||||||
// for _ in 0..iters {
|
// for _ in 0..iters {
|
||||||
|
|||||||
Reference in New Issue
Block a user