1#[cfg(not(feature = "std"))]
9use crate::kurbo::common::FloatFuncs as _;
10use crate::kurbo::{CubicBez, Line, ParamCurve, ParamCurveNearest, PathEl, Point, QuadBez};
11use crate::{
12 flatten::{SQRT_TOL, TOL, TOL_2},
13 geometry::RectU16,
14 kurbo::Affine,
15 tile::Tile,
16};
17use alloc::vec::Vec;
18use bytemuck::{Pod, Zeroable};
19use fearless_simd::*;
20
21pub(crate) enum LinePathEl {
33 MoveTo(Point),
34 LineTo(Point),
35}
36
37pub(crate) trait Callback {
41 fn callback(&mut self, el: LinePathEl);
42}
43
44#[inline(always)]
49pub(crate) fn flatten<S: Simd>(
50 simd: S,
51 path: impl IntoIterator<Item = PathEl>,
52 affine: Affine,
57 callback: &mut impl Callback,
58 flatten_ctx: &mut FlattenCtx,
59 cull_bbox: RectU16,
60) {
61 flatten_ctx.flattened_cubics.clear();
62
63 let left = cull_bbox.x0 as f64;
84 let top = ((cull_bbox.y0 / Tile::HEIGHT) * Tile::HEIGHT) as f64;
85 let right = cull_bbox.x1 as f64;
86 let bottom = cull_bbox.y1 as f64;
87
88 let mut path = path.into_iter();
89 let Some(first_el) = path.next() else {
90 return;
91 };
92 let first_el = affine * first_el;
93 let PathEl::MoveTo(start_pt) = first_el else {
94 debug_assert!(
95 matches!(first_el, PathEl::MoveTo(_)),
96 "Non-empty paths must begin with `PathEl::MoveTo`, got {first_el:?}"
97 );
98 return;
99 };
100
101 let mut start_pt = start_pt;
102 let mut last_pt = start_pt;
103 callback.callback(LinePathEl::MoveTo(start_pt));
104
105 for el in path {
106 match affine * el {
107 PathEl::MoveTo(p) => {
108 if last_pt != start_pt {
109 callback.callback(LinePathEl::LineTo(start_pt));
110 }
111 last_pt = p;
112 start_pt = p;
113 callback.callback(LinePathEl::MoveTo(p));
114 }
115 PathEl::LineTo(p) => {
116 last_pt = p;
117 callback.callback(LinePathEl::LineTo(p));
118 }
119 PathEl::QuadTo(p1, p2) => {
120 let p0 = last_pt;
121 let line = Line::new(p0, p2);
122 if [p0, p1, p2].into_iter().all(|p| p.x > right)
127 || [p0, p1, p2].into_iter().all(|p| p.y < top)
128 || [p0, p1, p2].into_iter().all(|p| p.y > bottom)
129 {
130 callback.callback(LinePathEl::MoveTo(p2));
131 }
132 else if [p0, p1, p2].into_iter().all(|p| p.x < left)
153 || line.nearest(p1, 0.).distance_sq <= 4. * TOL_2
154 {
155 callback.callback(LinePathEl::LineTo(p2));
156 } else {
157 let q = QuadBez::new(p0, p1, p2);
158 let params = q.estimate_subdiv(SQRT_TOL);
159 let n = ((0.5 / SQRT_TOL * params.val).ceil() as usize).max(1);
160 let step = 1.0 / (n as f64);
161 for i in 1..n {
162 let u = (i as f64) * step;
163 let t = q.determine_subdiv_t(¶ms, u);
164 let p = q.eval(t);
165 callback.callback(LinePathEl::LineTo(p));
166 }
167 callback.callback(LinePathEl::LineTo(p2));
168 }
169 last_pt = p2;
170 }
171 PathEl::CurveTo(p1, p2, p3) => {
172 let p0 = last_pt;
173 let line = Line::new(p0, p3);
174 if [p0, p1, p2, p3].into_iter().all(|p| p.x > right)
179 || [p0, p1, p2, p3].into_iter().all(|p| p.y < top)
180 || [p0, p1, p2, p3].into_iter().all(|p| p.y > bottom)
181 {
182 callback.callback(LinePathEl::MoveTo(p3));
183 }
184 else if [p0, p1, p2, p3].into_iter().all(|p| p.x < left)
208 || f64::max(
209 line.nearest(p1, 0.).distance_sq,
210 line.nearest(p2, 0.).distance_sq,
211 ) <= 16. / 9. * TOL_2
212 {
213 callback.callback(LinePathEl::LineTo(p3));
214 } else {
215 let c = CubicBez::new(p0, p1, p2, p3);
216 let max = flatten_cubic_simd(simd, c, flatten_ctx);
217
218 for p in &flatten_ctx.flattened_cubics[1..max] {
219 callback.callback(LinePathEl::LineTo(Point::new(p.x as f64, p.y as f64)));
220 }
221 }
222 last_pt = p3;
223 }
224 PathEl::ClosePath => {
225 if last_pt != start_pt {
226 callback.callback(LinePathEl::LineTo(start_pt));
227
228 last_pt = start_pt;
234 }
235 }
236 }
237 }
238
239 if last_pt != start_pt {
240 callback.callback(LinePathEl::LineTo(start_pt));
241 }
242}
243
244fn approx_parabola_integral(x: f64) -> f64 {
250 const D: f64 = 0.67;
251 x / (1.0 - D + (D.powi(4) + 0.25 * x * x).sqrt().sqrt())
252}
253
254fn approx_parabola_inv_integral(x: f64) -> f64 {
256 const B: f64 = 0.39;
257 x * (1.0 - B + (B * B + 0.25 * x * x).sqrt())
258}
259
260impl FlattenParamsExt for QuadBez {
261 #[inline(always)]
262 fn estimate_subdiv(&self, sqrt_tol: f64) -> FlattenParams {
263 let d01 = self.p1 - self.p0;
265 let d12 = self.p2 - self.p1;
266 let dd = d01 - d12;
267 let cross = (self.p2 - self.p0).cross(dd);
268 let x0 = d01.dot(dd) * cross.recip();
269 let x2 = d12.dot(dd) * cross.recip();
270 let scale = (cross / (dd.hypot() * (x2 - x0))).abs();
271
272 let a0 = approx_parabola_integral(x0);
274 let a2 = approx_parabola_integral(x2);
275 let val = if scale.is_finite() {
276 let da = (a2 - a0).abs();
277 let sqrt_scale = scale.sqrt();
278 if x0.signum() == x2.signum() {
279 da * sqrt_scale
280 } else {
281 let xmin = sqrt_tol / sqrt_scale;
283 sqrt_tol * da / approx_parabola_integral(xmin)
284 }
285 } else {
286 0.0
287 };
288 let u0 = approx_parabola_inv_integral(a0);
289 let u2 = approx_parabola_inv_integral(a2);
290 let uscale = (u2 - u0).recip();
291 FlattenParams {
292 a0,
293 a2,
294 u0,
295 uscale,
296 val,
297 }
298 }
299
300 #[inline(always)]
301 fn determine_subdiv_t(&self, params: &FlattenParams, x: f64) -> f64 {
302 let a = params.a0 + (params.a2 - params.a0) * x;
303 let u = approx_parabola_inv_integral(a);
304 (u - params.u0) * params.uscale
305 }
306}
307
308trait FlattenParamsExt {
309 fn estimate_subdiv(&self, sqrt_tol: f64) -> FlattenParams;
310 fn determine_subdiv_t(&self, params: &FlattenParams, x: f64) -> f64;
311}
312
313#[derive(Clone, Copy, Debug, Default, Zeroable, Pod)]
320#[repr(C)]
321struct Point32 {
322 x: f32,
323 y: f32,
324}
325
326struct FlattenParams {
327 a0: f64,
328 a2: f64,
329 u0: f64,
330 uscale: f64,
331 val: f64,
333}
334
335const MAX_QUADS: usize = 16;
339
340#[derive(Default, Debug)]
342pub struct FlattenCtx {
343 even_pts: [Point32; MAX_QUADS + 4],
345 odd_pts: [Point32; MAX_QUADS],
346 a0: [f32; MAX_QUADS],
347 da: [f32; MAX_QUADS],
348 u0: [f32; MAX_QUADS],
349 uscale: [f32; MAX_QUADS],
350 val: [f32; MAX_QUADS],
351 n_quads: usize,
352 flattened_cubics: Vec<Point32>,
354}
355
356#[inline(always)]
357fn is_finite_simd<S: Simd>(x: f32x4<S>) -> mask32x4<S> {
358 let simd = x.simd;
359
360 let x_abs = x.abs();
361 let reinterpreted = u32x4::from_bytes(x_abs.to_bytes());
362 simd.simd_lt_u32x4(reinterpreted, u32x4::splat(simd, 0x7f80_0000))
363}
364
365#[inline(always)]
371fn approx_parabola_integral_simd<S: Simd, F: SimdFloat<S, Element = f32>>(x: F) -> F {
372 let simd = x.witness();
373
374 const D: f32 = 0.67;
375 const D_POWI_4: f32 = 0.201_511_2;
376
377 let temp = F::splat(x.witness(), 0.25)
378 .mul_add(x * x, F::splat(simd, D_POWI_4))
379 .sqrt()
380 .sqrt();
381 let denom = temp + (1. - D);
382 x / denom
383}
384
385#[inline(always)]
389fn approx_parabola_inv_integral_simd<S: Simd>(x: f32x8<S>) -> f32x8<S> {
390 let simd = x.simd;
391
392 const B: f32 = 0.39;
393 const ONE_MINUS_B: f32 = 1.0 - B;
394
395 let temp = f32x8::splat(simd, 0.25).mul_add(x * x, B * B).sqrt();
396 let factor = f32x8::splat(simd, ONE_MINUS_B) + temp;
397 x * factor
398}
399
400#[inline(always)]
401fn pt_splat_simd<S: Simd>(simd: S, pt: Point32) -> f32x8<S> {
402 let p_f64: f64 = bytemuck::cast(pt);
403 f64x4::splat(simd, p_f64).bitcast()
404}
405
406#[inline(always)]
407fn eval_cubics_simd<S: Simd>(simd: S, c: &CubicBez, n: usize, result: &mut FlattenCtx) {
408 result.n_quads = n;
409 let dt = 0.5 / n as f32;
410
411 let p0p1 = f32x4::from_slice(
413 simd,
414 &[c.p0.x as f32, c.p0.y as f32, c.p1.x as f32, c.p1.y as f32],
415 );
416 let p2p3 = f32x4::from_slice(
417 simd,
418 &[c.p2.x as f32, c.p2.y as f32, c.p3.x as f32, c.p3.y as f32],
419 );
420
421 let split_single = |input: f32x4<S>| {
422 let t1: f64x2<S> = input.bitcast();
423 let p0 = simd.zip_low_f64x2(t1, t1);
424 let p1 = simd.zip_high_f64x2(t1, t1);
425
426 let p0: f32x4<S> = p0.bitcast();
427 let p1: f32x4<S> = p1.bitcast();
428
429 (f32x8::block_splat(p0), f32x8::block_splat(p1))
430 };
431
432 let (p0_128, p1_128) = split_single(p0p1);
433 let (p2_128, p3_128) = split_single(p2p3);
434
435 let coeff_a = (p1_128 - p2_128).mul_add(3.0, p3_128 - p0_128);
440 let coeff_b = p1_128.mul_add(-2.0, p0_128 + p2_128) * 3.0;
441 let coeff_c = (p1_128 - p0_128) * 3.0;
442 let coeff_d = p0_128;
443
444 let iota = f32x8::from_slice(simd, &[0.0, 0.0, 2.0, 2.0, 1.0, 1.0, 3.0, 3.0]);
445 let step = iota * dt;
446 let mut t = step;
447 let t_inc = f32x8::splat(simd, 4.0 * dt);
448
449 let even_pts: &mut [f32] = bytemuck::cast_slice_mut(&mut result.even_pts);
450 let odd_pts: &mut [f32] = bytemuck::cast_slice_mut(&mut result.odd_pts);
451
452 for i in 0..n.div_ceil(2) {
453 let evaluated = (coeff_a.mul_add(t, coeff_b))
454 .mul_add(t, coeff_c)
455 .mul_add(t, coeff_d);
456
457 let (low, high) = simd.split_f32x8(evaluated);
458
459 low.store_slice(&mut even_pts[i * 4..][..4]);
460 high.store_slice(&mut odd_pts[i * 4..][..4]);
461
462 t += t_inc;
463 }
464
465 p3_128.store_slice(&mut even_pts[n * 2..][..8]);
466}
467
468#[inline(always)]
469fn estimate_subdiv_simd<S: Simd>(simd: S, sqrt_tol: f32, ctx: &mut FlattenCtx) {
470 let n = ctx.n_quads;
471
472 let even_pts: &mut [f32] = bytemuck::cast_slice_mut(&mut ctx.even_pts);
473 let odd_pts: &mut [f32] = bytemuck::cast_slice_mut(&mut ctx.odd_pts);
474
475 for i in 0..n.div_ceil(4) {
476 let p0 = f32x8::from_slice(simd, &even_pts[i * 8..][..8]);
477 let p_onehalf = f32x8::from_slice(simd, &odd_pts[i * 8..][..8]);
478 let p2 = f32x8::from_slice(simd, &even_pts[(i * 8 + 2)..][..8]);
479 let x = p0 * -0.5;
480 let x1 = p_onehalf.mul_add(2.0, x);
481 let p1 = p2.mul_add(-0.5, x1);
482
483 p1.store_slice(&mut odd_pts[(i * 8)..][..8]);
484
485 let d01 = p1 - p0;
486 let d12 = p2 - p1;
487 let d01x = simd.unzip_low_f32x8(d01, d01);
488 let d01y = simd.unzip_high_f32x8(d01, d01);
489 let d12x = simd.unzip_low_f32x8(d12, d12);
490 let d12y = simd.unzip_high_f32x8(d12, d12);
491 let ddx = d01x - d12x;
492 let ddy = d01y - d12y;
493 let d02x = d01x + d12x;
494 let d02y = d01y + d12y;
495 let cross = ddx.mul_add(-d02y, d02x * ddy);
497
498 let x0_x2_a = {
499 let (d01x_low, _) = simd.split_f32x8(d01x);
500 let (d12x_low, _) = simd.split_f32x8(d12x);
501
502 simd.combine_f32x4(d12x_low, d01x_low) * ddx
503 };
504 let temp1 = {
505 let (d12y_low, _) = simd.split_f32x8(d12y);
506 let (d01y_low, _) = simd.split_f32x8(d01y);
507
508 simd.combine_f32x4(d12y_low, d01y_low)
509 };
510 let x0_x2_num = temp1.mul_add(ddy, x0_x2_a);
511 let x0_x2 = x0_x2_num / cross;
512 let (ddx_low, _) = simd.split_f32x8(ddx);
513 let (ddy_low, _) = simd.split_f32x8(ddy);
514 let dd_hypot = ddy_low.mul_add(ddy_low, ddx_low * ddx_low).sqrt();
515 let (x0, x2) = simd.split_f32x8(x0_x2);
516 let scale_denom = dd_hypot * (x2 - x0);
517 let (temp2, _) = simd.split_f32x8(cross);
518 let scale = (temp2 / scale_denom).abs();
519 let a0_a2 = approx_parabola_integral_simd(x0_x2);
520 let (a0, a2) = simd.split_f32x8(a0_a2);
521 let da = a2 - a0;
522 let da_abs = da.abs();
523 let sqrt_scale = scale.sqrt();
524 let temp3 = simd.or_i32x4(x0.bitcast(), x2.bitcast());
525 let mask = simd.simd_ge_i32x4(temp3, i32x4::splat(simd, 0));
526 let noncusp = da_abs * sqrt_scale;
527 let xmin = sqrt_tol / sqrt_scale;
529 let approxint = approx_parabola_integral_simd(xmin);
530 let cusp = (sqrt_tol * da_abs) / approxint;
531 let val_raw = simd.select_f32x4(mask, noncusp, cusp);
532 let finite_mask = is_finite_simd(val_raw);
533 let val = simd.select_f32x4(finite_mask, val_raw, f32x4::splat(simd, 0.0));
534 let u0_u2 = approx_parabola_inv_integral_simd(a0_a2);
535 let (u0, u2) = simd.split_f32x8(u0_u2);
536 let uscale_a = u2 - u0;
537 let uscale = 1.0 / uscale_a;
538
539 a0.store_slice(&mut ctx.a0[i * 4..][..4]);
540 da.store_slice(&mut ctx.da[i * 4..][..4]);
541 u0.store_slice(&mut ctx.u0[i * 4..][..4]);
542 uscale.store_slice(&mut ctx.uscale[i * 4..][..4]);
543 val.store_slice(&mut ctx.val[i * 4..][..4]);
544 }
545}
546
547#[inline(always)]
548fn output_lines_simd<S: Simd>(
549 simd: S,
550 ctx: &mut FlattenCtx,
551 i: usize,
552 x0: f32,
553 dx: f32,
554 n: usize,
555 start_idx: usize,
556) {
557 let p0 = pt_splat_simd(simd, ctx.even_pts[i]);
558 let p1 = pt_splat_simd(simd, ctx.odd_pts[i]);
559 let p2 = pt_splat_simd(simd, ctx.even_pts[i + 1]);
560
561 const IOTA2: [f32; 8] = [0., 0., 1., 1., 2., 2., 3., 3.];
562 let iota2 = f32x8::from_slice(simd, IOTA2.as_ref());
563 let x = iota2.mul_add(dx, f32x8::splat(simd, x0));
564 let da = f32x8::splat(simd, ctx.da[i]);
565 let mut a = da.mul_add(x, f32x8::splat(simd, ctx.a0[i]));
566 let a_inc = 4.0 * dx * da;
567 let uscale = f32x8::splat(simd, ctx.uscale[i]);
568 let u0 = f32x8::splat(simd, ctx.u0[i]);
569
570 let coeff_a = p1.mul_add(-2.0, p0) + p2;
572 let coeff_b = (p1 - p0) * 2.0;
573 let coeff_c = p0;
574
575 let out: &mut [f32] = bytemuck::cast_slice_mut(&mut ctx.flattened_cubics[start_idx..]);
576
577 for j in 0..n.div_ceil(4) {
578 let u = approx_parabola_inv_integral_simd(a);
579 let t = (u - u0) * uscale;
580 let p = coeff_a.mul_add(t, coeff_b).mul_add(t, coeff_c);
581 p.store_slice(&mut out[j * 8..][..8]);
582 a += a_inc;
583 }
584}
585
586#[inline(always)]
587fn flatten_cubic_simd<S: Simd>(simd: S, c: CubicBez, ctx: &mut FlattenCtx) -> usize {
588 let n_quads = estimate_num_quads(c, TOL as f32);
589 eval_cubics_simd(simd, &c, n_quads, ctx);
590 let tol = (TOL as f32) * (1.0 - TO_QUAD_TOL);
591 let sqrt_tol = tol.sqrt();
592 estimate_subdiv_simd(simd, sqrt_tol, ctx);
593 let sum: f32 = ctx.val[..n_quads].iter().sum();
594 let n = ((0.5 * sum / sqrt_tol).ceil() as usize).max(1);
595 let target_len = n + 4;
596 if target_len > ctx.flattened_cubics.len() {
597 ctx.flattened_cubics.resize(target_len, Point32::default());
598 }
599
600 let step = sum / (n as f32);
601 let step_recip = 1.0 / step;
602 let mut val_sum = 0.0;
603 let mut last_n = 0;
604 let mut x0base = 0.0;
605
606 for i in 0..n_quads {
607 let val = ctx.val[i];
608 val_sum += val;
609 let this_n = val_sum * step_recip;
610 let this_n_next = 1.0 + this_n.floor();
611 let dn = this_n_next as usize - last_n;
612 if dn > 0 {
613 let dx = step / val;
614 let x0 = x0base * dx;
615 output_lines_simd(simd, ctx, i, x0, dx, dn, last_n);
616 }
617 x0base = this_n_next - this_n;
618 last_n = this_n_next as usize;
619 }
620
621 ctx.flattened_cubics[n] = ctx.even_pts[n_quads];
622
623 n + 1
624}
625
626#[inline(always)]
627fn estimate_num_quads(c: CubicBez, accuracy: f32) -> usize {
628 let q_accuracy = (accuracy * TO_QUAD_TOL) as f64;
629 let max_hypot2 = 432.0 * q_accuracy * q_accuracy;
630 let p1x2 = c.p1.to_vec2() * 3.0 - c.p0.to_vec2();
631 let p2x2 = c.p2.to_vec2() * 3.0 - c.p3.to_vec2();
632 let err = (p2x2 - p1x2).hypot2();
633 let err_div = err / max_hypot2;
634
635 estimate(err_div)
636}
637
638const TO_QUAD_TOL: f32 = 0.1;
639
640#[inline(always)]
641fn estimate(err_div: f64) -> usize {
642 const LUT: [f64; MAX_QUADS] = [
652 1.0, 64.0, 729.0, 4096.0, 15625.0, 46656.0, 117649.0, 262144.0, 531441.0, 1000000.0,
653 1771561.0, 2985984.0, 4826809.0, 7529536.0, 11390625.0, 16777216.0,
654 ];
655
656 #[expect(clippy::needless_range_loop, reason = "better clarity")]
657 for i in 0..MAX_QUADS {
658 if err_div <= LUT[i] {
659 return i + 1;
660 }
661 }
662
663 MAX_QUADS
664}
665
666#[cfg(test)]
667mod tests {
668 use crate::flatten_simd::{MAX_QUADS, estimate};
669
670 fn old_estimate(err_div: f64) -> usize {
671 let n_quads = (err_div.powf(1. / 6.0).ceil() as usize).max(1);
672 n_quads.min(MAX_QUADS)
673 }
674
675 #[test]
677 #[ignore]
678 fn accuracy() {
679 for i in 0..u32::MAX {
680 let num = f32::from_bits(i);
681
682 if num.is_finite() {
683 assert_eq!(old_estimate(num as f64), estimate(num as f64), "{num}");
684 }
685 }
686 }
687}