Avoid hardcoding a tolerance in Vello API, instead require the user to provide it#1376
Avoid hardcoding a tolerance in Vello API, instead require the user to provide it#1376DJMcNab merged 14 commits intolinebender:mainfrom
Conversation
taj-p
left a comment
There was a problem hiding this comment.
Looks much better!! 🎉
The only comment is the use of the Shape super trait.
| /// Use an approximated shape where an [`ExactPathElements`] is required, by approximating it to | ||
| /// within the given tolerance. | ||
| /// | ||
| /// This is useful for drawing shapes such as [`Circle`](kurbo::Circle)s and [`RoundedRect`](kurbo::RoundedRect)s | ||
| /// to renderers which use Vello API. | ||
| /// | ||
| /// As the user of this function, you are responsible for determining the correct tolerance for your use case. | ||
| /// A reasonable approach might be to select a tolerance which allows scaling up ("zooming in") by 4x to be | ||
| /// within your intended tolernace bound, then recomputing the [`Scene`](crate::Scene) from your base data | ||
| /// representation once that is exceeded. | ||
| /// If you know that the shape will not be scaled, you can use [`UNSCALED_TOLERANCE`]. | ||
| /// The resulting path will be within 1/10th of a pixel of the actual shape, which is a negligible | ||
| /// difference for rendering. |
There was a problem hiding this comment.
Edited by @DJMcNab to make suggestion take up less space.
Already applied suggestion
| /// Use an approximated shape where an [`ExactPathElements`] is required, by approximating it to | |
| /// within the given tolerance. | |
| /// | |
| /// This is useful for drawing shapes such as [`Circle`](kurbo::Circle)s and [`RoundedRect`](kurbo::RoundedRect)s | |
| /// to renderers which use Vello API. | |
| /// | |
| /// As the user of this function, you are responsible for determining the correct tolerance for your use case. | |
| /// A reasonable approach might be to select a tolerance which allows scaling up ("zooming in") by 4x to be | |
| /// within your intended tolernace bound, then recomputing the [`Scene`](crate::Scene) from your base data | |
| /// representation once that is exceeded. | |
| /// If you know that the shape will not be scaled, you can use [`UNSCALED_TOLERANCE`]. | |
| /// The resulting path will be within 1/10th of a pixel of the actual shape, which is a negligible | |
| /// difference for rendering. | |
| /// Use an approximated shape where an [`ExactPathElements`] is required, by approximating it to | |
| /// within the given tolerance. | |
| /// | |
| /// *WARNING*: Unlike [`ExactPathElements`], which will produce correct renderings for any scale, this | |
| /// approximation is only valid for a fixed range of transforms. | |
| /// | |
| /// As the user of this function, you are responsible for determining the correct tolerance for your use case. | |
| /// A reasonable approach might be to select a tolerance which allows scaling up ("zooming in") by 4x (for example, 0.00001) to be | |
| /// within your intended tolerance bound, then recomputing the [`Scene`](crate::Scene) from your base data | |
| /// representation once that is exceeded. | |
| /// If you know that the shape will not be scaled, you can use [`UNSCALED_TOLERANCE`]. | |
| /// The resulting path will be within 1/10th of a pixel of the actual shape, which is a negligible | |
| /// difference for rendering. | |
| /// | |
| /// This is useful for drawing shapes such as [`Circle`](kurbo::Circle)s and [`RoundedRect`](kurbo::RoundedRect)s. |
There was a problem hiding this comment.
I have one concern here, which is the 0.00001 bound. I believe that would support a
There was a problem hiding this comment.
I've removed the 0.00001 for now; we can always restore it in a follow-up.
sparse_strips/vello_cpu/src/api.rs
Outdated
| } | ||
|
|
||
| fn fill_path(&mut self, transform: Affine, fill_rule: Fill, path: impl Shape) { | ||
| fn fill_path(&mut self, transform: Affine, fill_rule: Fill, path: impl ExactPathElements) { |
There was a problem hiding this comment.
It is a little confusing that we take an owned ExactPathElements instead of a borrowed type. For hybrid and single threaded CPU, we only require borrowed data. For multithreading, we still only require borrowed data IIUC since we copy the path into the allocation group.
Should this be &impl ExactPathElements to better reflect that only borrowed data is required from the caller? I believe Vello Classic took this approach - is there context for why impl ExactPathElements is better?
There was a problem hiding this comment.
I thought I was copying Vello Classic, but a revisit of the docs for vello::Scene shows that my memory was mistaken. I'll change this; I think it makes as much sense to do it in this PR.
There was a problem hiding this comment.
In applying this change, it raises a small ergonomics question; for the vast majority of non-BezPath shapes, the extra & isn't doing anything meaningful here. A conceivable compromise here would be impl ExactPathElements + Copy, which would not require the & for shapes where it isn't consuming, but would force it for e.g. BezPath.
In this PR, I've changed it to be & for simplicity/to avoid blocking the rest of this work on that question. But it's something I'll consider for a follow-up.
This represents shapes which can be turned into exact paths Co-Authored-By: Tom Churchman <thomas@churchman.nl>
Co-authored-by: Taj Pereira <taj@canva.com>
I don't know what I was trying to say :)
| self.render_context.fill_path(&path.into_path(0.1)); | ||
| // This tolerance parameter is meaningless, because this is an `ExactPathElements` | ||
| // However, using `to_path` avoids allocation in some cases. | ||
| // TODO: Tweak inner API to accept an `ExactPathElements` (or at least, the resultant iterator) |
There was a problem hiding this comment.
We probably want to just pass the iterator so we don't monomorphise across every trait impl
There was a problem hiding this comment.
I believe it would still in most cases be a separate impl for each Shape, as each Shape has their own iterator type (unless I guess we used something like &mut dyn Iterator?)
|
Which shapes cannot implement Is it primarily those based on circular geometry? (Arc, Circle, Ellipse, etc?). If so, might it make sense to create an extended version of Or are (infinitely many) other shape types that we may need to support such that this wouldn't work? |
That's right. More specifically, only shapes that can be represented by cubic polynomial segments (including line and quadratic segments) can be
I don't think so. The issue with something like a circular Edit: with your edits I now understand you meant keeping those extended path elements around until such time the actual transforming is done. Something like that is possible, but at that point may be quite similar to just keeping an enumeration of shapes or a |
Yeah, that's the idea. Ellitical arcs (and/or perhaps even hyper-elliptical arcs - people love their squircles) would make sense to me. It would be similar to keeping around Shape, but a more practical way of implementing it I think. Storing |
|
Skia has e.g. |
Co-authored-by: Taj Pereira <taj@canva.com>
Co-authored-by: Taj Pereira <taj@canva.com>
This was one of the key points raised for follow-up in #1360, to make this API harder to misuse.
The implementation strategy is to have a new
ExactPathElementstrait, which is implemented forShapes which don't require approximation.To allow shapes which do require a tolerance to be used without allocation, the
withinfunction can be used, which creates an exact shape from an inexact shape within the provided tolerance.This ensures that the common-case of rectangles and similar is ergonomic, whilst the case where tolerance is needed is handled by the user. This still completely avoids per-path allocations.
The
ExactPathElementsis based on code by @tomcur from #vello > Determining correct `Shape` tolerance. There are reasonable arguments thatExactPathElementsactually belongs in Kurbo, but I also think that, if this is the direction we decide to go, migrating that code into Kurbo would (relatively) straightforward.