8000 First cut at least-squares offset curves by raphlinus · Pull Request #427 · linebender/kurbo · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

First cut at least-squares offset curves #427

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

Conversation

raphlinus
Copy link
Contributor

Replace the quartic solver with the new least-squares one.

The robustness story is not complete. For now, it relies on the existing "regularize" hack, but it would be better to be robust by construction.

The stroke options are not respected. It tries to do a better job with subdivision but does not attempt an absolutely minimal number of segments as before. That could be re-added.

The old offset methods are still exported. They should probably be deprecated.

Progress toward #317

Replace the quartic solver with the new least-squares one.

The robustness story is not complete. For now, it relies on the existing "regularize" hack, but it would be better to be robust by construction.

The stroke options are not respected. It tries to do a better job with subdivision but does not attempt an absolutely minimal number of segments as before. That could be re-added.

The old offset methods are still exported. They should probably be deprecated.

Progress toward #317
@raphlinus raphlinus marked this pull request as ready for review March 20, 2025 14:13
Copy link
Member
@tomcur tomcur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial observations inline based on a first dive into this.

src/offset.rs Outdated
tolerance: f64,
}

// We never let cusp values haven an absolute value smaller than
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// We never let cusp values haven an absolute value smaller than
// We never let cusp values have an absolute value smaller than

src/offset.rs Outdated
Comment on lines 390 to 393
let a_n = 3.0 * mt * t * mt * rec.utan0.dot(n);
let a_t = 3.0 * mt * t * mt * rec.utan0.cross(n);
let b_n = 3.0 * mt * t * t * rec.utan1.dot(n);
let b_t = 3.0 * mt * t * t * rec.utan1.cross(n);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On x86 it may be very slightly faster to group the squarings here: https://godbolt.org/z/b1xa96Kff

            let a_n = 3.0 * (mt * mt) * t * rec.utan0.dot(n);

src/stroke.rs Outdated
Comment on lines 199 to 204
pub fn stroke(
path: impl IntoIterator<Item = PathEl>,
style: &Stroke,
opts: &StrokeOpts,
_opts: &StrokeOpts,
tolerance: f64,
) -> BezPath {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an API change (not for this PR), we could consider taking &mut BezPath here, allowing reusing the allocation.

src/offset.rs Outdated
((self.c2 * t + self.c1) * t + self.c0) / (ds2 * ds2.sqrt()) + 1.0
}

/// Compute cusp and unit tangent of endpoint.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Compute cusp and unit tangent of endpoint.
/// Compute cusp and unit tangent of endpoint.
///
/// See [`Self::cusp_sign`] for the meaning of the cusp calculation.

src/offset.rs Outdated
Comment on lines 247 to 248
// Compute a function which has a zero-crossing at cusps, and is
// positive at low curvatures on the source curve.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An explanation here could be helpful. It took me some reading of some curve properties (one, two) to come to (what I hope is) the right explanation. (As a minor observation relative to the equations given on Wikipedia, IIUC, we calculate negative curvature here due to the order of the cross product, which cancels out with another negative in the "cusp" calculation.)

Suggested change
// Compute a function which has a zero-crossing at cusps, and is
// positive at low curvatures on the source curve.
/// Compute a function which has a zero-crossing at cusps on the parallel curve,
/// and is positive at low curvatures on the source curve.
///
/// This is based on the geometric property that given a source curve with
/// a curvature of `k(t)`, the parallel curve at an offset `d/2` potentially
/// has a cusp when `1 + d/2 k(t) = 0`.
///
/// Note that given a curve `c(t)`, its signed curvature is
/// `k(t) = c''(t) x c'(t) / ||c'(t)||^3`. This is encoded (including the distance
/// offset) in `c0`, `c1` and `c2`.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically right, but I have a couple of nits. First, it doesn't "potentially" have a cusp, I think it's fair to say that when the curvature is equal to reciprocal half-width, there is a cusp. Second, the "this" in "this is encoded" is an unclear referent, I'd say, "the cross product is a quadratic polynomial, and c0, c1, and c2 encode its coefficients."

I don't generally carefully work out sign conventions, I just make it come out right. That's something that potentially can be cleaned up.

src/offset.rs Outdated
let mut max_err = 0.0;
for (i, t) in ts.iter_mut().enumerate() {
let mut ta = *t;
// TODO: probably should also store in rec, we always need this
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a vestigial TODO?

src/offset.rs Outdated
}

fn apply(&self, rec: &OffsetRec, a: f64, b: f64) -> CubicBez {
// wondering if p0 and p3 should be in rec
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would also prevent the doubling of those calculations for the midpoint when subdividing.

ajakubowicz-canva added a commit to raphlinus/fearless_simd that referenced this pull request Jun 18, 2025
> [!NOTE]
> The measurements in this PR were done very rough via a quick Chrome profile. Because the numbers are small, until we run a rigorous benchmark it'll be hard to validate the impact. Especially across browser engines, and with JS engines optimising WASM and fusing operators.

## Overview

This PR adds initial WASM SIMD support to `fearless_simd`, implementing enough operations to enable WASM S
8000
IMD in linebender/vello#1053. Rather than implementing all operations in one large PR, this focuses on the essential subset needed for Vello and breaks up an otherwise huge change. There's also some tricky operations to implement using the small amount of WASM instructions 😅 .

## Performance Impact

Tested with Ghost Tiger rendering in Vello:

### Without fast kurbo (baseline):
- **Without SIMD**: ~42.31ms per frame
- **With SIMD**: ~39.91ms per frame  
- **Improvement**: ~5% faster

### With fast kurbo (linebender/kurbo#427):
- **Without SIMD**: ~6ms per frame
- **With SIMD**: ~4ms per frame
- **Improvement**: ~30% faster (Take this with a huge grain of salt)

*Test methodology: I linked `vello` locally to `fearless_simd` via `path` reference, and modified `vello_hybrid` to use the WASM SIMD level.*

## Changes

- **New architecture**: Added WASM SIMD128 support
- **Operations implemented**: Core subset including:
  - Binary ops: `add`, `sub`, `mul`, `min`, `max`
  - Comparison ops: `simd_eq`, `simd_ne`, `simd_lt`, etc.
  - Math ops: `sqrt`, `madd`
- **Testing**: Added parity tests ensuring Fallback and WASM SIMD produce identical results
- **Bug fix**: Fixed incorrect mask generation in Fallback comparison operations (was returning `0/1` instead of `0/-1`)

## Test Plan

Added `test_wasm_simd_parity!` macro that verifies operations produce identical results across Fallback and WASM SIMD implementations.

I only tested a small subset. Maybe in the future we code-gen the tests as well?

## Next Steps

Future PRs will add more operations to achieve full WASM SIMD coverage.
Instead of linearized least error, which can generate incorrect approximations (especially in near-cusp situations), use arc drawing. Also make error evaluation more robust, specifically to reject non-finite solutions.

Sample points are equally spaced on the approximation rather than on the source curve. Thus, the Newton step refines t values on the source curve.

The error report is expanded so that intermediate results can be reused in least square error refinement, as an optimization.
Subdivide by integral of absolute curvature, which should improve near-cusp behavior.

I'm trying to make the core algorithm so robust it generates correct results even without the regularization step. However, that approach is not looking especially promising. A major sticking point is that it's difficult to ensure that both sides make consistent decisions.

Also adds a stroke example adapted from Vello "tricky strokes", which in turn is adapted from Skia.
Use existing methods rather than doing rotation by hand. Fix typo.
@LaurenzV
Copy link
Contributor

See my Zulip message for two suggestions for performance improvements: https://xi.zulipchat.com/#narrow/channel/260979-kurbo/topic/New.20stroker.20for.20sparse.20strips/near/525882812

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0