-
Notifications
You must be signed in to change notification settings - Fork 6k
[Impeller] use point/line primitve for hairlines. #55230
Changes from 6 commits
813d4bd
1c362df
beee6ae
e3cc898
2d8f1eb
032cb2f
a968e8a
ce71dab
544cb56
2f3c2a6
eb7f841
07017d1
6ff4f7f
e4340b5
32779af
4c75e18
201b54b
0c6c969
7f98866
185291f
4385efc
2eeee37
897ff2f
a91974b
126f665
b7152ee
1fd08d6
9975825
d76a6cc
322ede8
44f5082
011e724
1b4da9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,8 +9,7 @@ | |
|
|
||
| namespace impeller { | ||
|
|
||
| // Geometry class that can generate vertices (with or without texture | ||
| // coordinates) for either filled or stroked circles | ||
| /// @brief Generator for vertices or either filled or stroked circles | ||
|
||
| class CircleGeometry final : public Geometry { | ||
| public: | ||
| explicit CircleGeometry(const Point& center, Scalar radius); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -133,13 +133,11 @@ bool Geometry::CanApplyMaskFilter() const { | |
| Scalar Geometry::ComputeStrokeAlphaCoverage(const Matrix& transform, | ||
| Scalar stroke_width) { | ||
| Scalar scaled_stroke_width = transform.GetMaxBasisLengthXY() * stroke_width; | ||
| // If the stroke width is 0 or greater than kMinStrokeSizeMSAA, don't apply | ||
| // any additional alpha. This is intended to match Skia behavior. | ||
| if (scaled_stroke_width == 0.0 || scaled_stroke_width >= kMinStrokeSizeMSAA) { | ||
| if (scaled_stroke_width == 0.0 || scaled_stroke_width >= kMinStrokeSize) { | ||
flar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return 1.0; | ||
| } | ||
| // This scalling is eyeballed from Skia. | ||
| return std::clamp(scaled_stroke_width * 2.0f, 0.f, 1.f); | ||
| return std::clamp(scaled_stroke_width, 0.1f, 1.f); | ||
flar marked this conversation as resolved.
Show resolved
Hide resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still confused on this code. So, 0.0 means 1.0. That would create a visual break when reducing the width down to 0, would it not?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes there will be a visual break, but this is the behavior of skia. 0 means explicitly 1 pixel, whereas smaller widths are simulated by modulating alpha. |
||
| } | ||
|
|
||
| } // namespace impeller | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| // found in the LICENSE file. | ||
|
|
||
| #include "impeller/entity/geometry/line_geometry.h" | ||
| #include "impeller/core/formats.h" | ||
| #include "impeller/entity/geometry/geometry.h" | ||
|
|
||
| namespace impeller { | ||
|
|
@@ -12,22 +13,15 @@ LineGeometry::LineGeometry(Point p0, Point p1, Scalar width, Cap cap) | |
| FML_DCHECK(width >= 0); | ||
| } | ||
|
|
||
| Scalar LineGeometry::ComputePixelHalfWidth(const Matrix& transform, | ||
| Scalar width, | ||
| bool msaa) { | ||
| Scalar max_basis = transform.GetMaxBasisLengthXY(); | ||
| if (max_basis == 0) { | ||
| return {}; | ||
| } | ||
|
|
||
| Scalar min_size = (msaa ? kMinStrokeSize : kMinStrokeSizeMSAA) / max_basis; | ||
| return std::max(width, min_size) * 0.5f; | ||
| std::pair<Scalar, bool> LineGeometry::ComputePixelHalfWidth(Scalar max_basis, | ||
| Scalar width) { | ||
| Scalar min_size = kMinStrokeSize / max_basis; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if max_basis is 0? There used to be protection for that...
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe returning 0 wasn't the answer, but will returning inf cause issues?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm, if the basis is 0, we might not get here if we notice the degenerate transform elsewhere, but are we sure that happens?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PointFieldGeometry also bails on basis == 0. |
||
| return std::make_pair(std::max(width, min_size) * 0.5f, width <= min_size); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is called "compute pixel ...", but it is returning a local space width (same as before). A confusing name, but one we've been living with already... |
||
| } | ||
|
|
||
| Vector2 LineGeometry::ComputeAlongVector(const Matrix& transform, | ||
| bool allow_zero_length, | ||
| bool msaa) const { | ||
| Scalar stroke_half_width = ComputePixelHalfWidth(transform, width_, msaa); | ||
| Vector2 LineGeometry::ComputeAlongVector(Scalar max_basis, | ||
| bool allow_zero_length) const { | ||
| auto [stroke_half_width, _] = ComputePixelHalfWidth(max_basis, width_); | ||
| if (stroke_half_width < kEhCloseEnough) { | ||
| return {}; | ||
| } | ||
|
|
@@ -46,10 +40,9 @@ Vector2 LineGeometry::ComputeAlongVector(const Matrix& transform, | |
| } | ||
|
|
||
| bool LineGeometry::ComputeCorners(Point corners[4], | ||
| const Matrix& transform, | ||
| bool extend_endpoints, | ||
| bool msaa) const { | ||
| auto along = ComputeAlongVector(transform, extend_endpoints, msaa); | ||
| Scalar max_basis, | ||
| bool extend_endpoints) const { | ||
| auto along = ComputeAlongVector(max_basis, extend_endpoints); | ||
| if (along.IsZero()) { | ||
| return false; | ||
| } | ||
|
|
@@ -77,24 +70,41 @@ GeometryResult LineGeometry::GetPositionBuffer(const ContentContext& renderer, | |
| RenderPass& pass) const { | ||
| using VT = SolidFillVertexShader::PerVertexData; | ||
|
|
||
| auto& transform = entity.GetTransform(); | ||
| auto radius = ComputePixelHalfWidth( | ||
| transform, width_, pass.GetSampleCount() == SampleCount::kCount4); | ||
| Scalar max_basis = entity.GetTransform().GetMaxBasisLengthXY(); | ||
| auto& host_buffer = renderer.GetTransientsBuffer(); | ||
|
|
||
| if (max_basis == 0) { | ||
| return {}; | ||
| } | ||
|
|
||
| auto [radius, is_harline] = ComputePixelHalfWidth(max_basis, width_); | ||
|
|
||
| // This is a harline stroke and can be drawn directly with line primitives, | ||
| // which avoids extra tessellation work, cap/joins, and overdraw prevention. | ||
| if (is_harline) { | ||
| Point points[2] = {p0_, p1_}; | ||
| return GeometryResult{ | ||
| .type = PrimitiveType::kLineStrip, | ||
| .vertex_buffer = {.vertex_buffer = host_buffer.Emplace( | ||
| points, sizeof(points), alignof(Point)), | ||
| .vertex_count = 2, | ||
| .index_type = IndexType::kNone}, | ||
| .transform = entity.GetShaderTransform(pass), | ||
| }; | ||
| } | ||
|
|
||
| if (cap_ == Cap::kRound) { | ||
| auto& transform = entity.GetTransform(); | ||
| std::shared_ptr<Tessellator> tessellator = renderer.GetTessellator(); | ||
| auto generator = tessellator->RoundCapLine(transform, p0_, p1_, radius); | ||
| return ComputePositionGeometry(renderer, generator, entity, pass); | ||
| } | ||
|
|
||
| Point corners[4]; | ||
| if (!ComputeCorners(corners, transform, cap_ == Cap::kSquare, | ||
| pass.GetSampleCount() == SampleCount::kCount4)) { | ||
| if (!ComputeCorners(corners, max_basis, cap_ == Cap::kSquare)) { | ||
| return kEmptyResult; | ||
| } | ||
|
|
||
| auto& host_buffer = renderer.GetTransientsBuffer(); | ||
|
|
||
| size_t count = 4; | ||
| BufferView vertex_buffer = host_buffer.Emplace( | ||
| count * sizeof(VT), alignof(VT), [&corners](uint8_t* buffer) { | ||
|
|
@@ -120,8 +130,8 @@ GeometryResult LineGeometry::GetPositionBuffer(const ContentContext& renderer, | |
|
|
||
| std::optional<Rect> LineGeometry::GetCoverage(const Matrix& transform) const { | ||
| Point corners[4]; | ||
| // Note: MSAA boolean doesn't matter for coverage computation. | ||
| if (!ComputeCorners(corners, transform, cap_ != Cap::kButt, /*msaa=*/false)) { | ||
| if (!ComputeCorners(corners, transform.GetMaxBasisLengthXY(), | ||
| cap_ != Cap::kButt)) { | ||
| return {}; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering why we pass along the 0.0 here and just noticed a bug.
The StrokedCircle generator below says it will convert to a filled circle if the inner radius is <= 0, but if you look at the implementation it converts if the half_width is < 0... which I don't think happens ever? So, there's a bug there.
But, fixing that bug or not, it calls into question why we pass along a 0 in the first place. Why isn't <=0 clamped at the minimum pixel width?
We can file a bug against the StrokedCircle generator for fixing separately, but I think the passing along of a 0 here results in a thin circle turning into a filled circle (whereas passing in the min_width would not), so this calculation is suspect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And if we don't pass it along to the stroke generator, does it generate an empty shape when tessellating?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the comment just wrong? a quick read through and I think this code is doing reasonable things? half_width is always 0 for fills though its confusing that we go through the stroke code to get there...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I see now, this is totally confusing. So the local field
stroke_width_is set to -1 to indicate a filled circle. The?:code used to convert this to 0 and the half_width would always be >0 for the other case (stroked). So, yes, for the truly filled case we are calling a method calledStrokedCirclewhich is odd and confusing, but the comment indicates that the called method will also do a filled circle for the case where the inner radius disappears - and it does no such thing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code would make a lot more sense if there was a more explicit indicator of filled and it simply said:
Fixing StrokedCircle to detect the degenerate case would be a separate fix, but until then, the comment below is just a red herring, maybe if it said "should simplify" instead of "will simplify"...
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or, it would be OK if that pseudo-code used "stroke_width_ < 0" as the test, as long as it doesn't try to fold the cases together via strange values in
half_width...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I re-arranged this code.