-
Notifications
You must be signed in to change notification settings - Fork 6k
[Impeller] use point/line primitve for hairlines. #55230
Conversation
Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change). If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review. |
I also adjusted some of the alpha scaling params since this doesn't do MSAA specific scaling anymore. |
@@ -17,43 +17,36 @@ using VS = SolidFillVertexShader; | |||
|
|||
namespace { | |||
|
|||
template <typename VertexWriter> | |||
using CapProc = std::function<void(VertexWriter& vtx_builder, | |||
class PositionWriter { |
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 removed the template code here because we never specialize.
I don't know how many times I've mashed rerun but the bot failure seems unrelated. |
@@ -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 |
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.
"for vertices or"? "for vertices for"?
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.
Done
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.
It still looks odd. "vertices or either"? Should this be
"Generator for vertices for either filled or stroked circles"?
Or, more simply
"Vertex generator for either filled or stroked circles"?
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.
That's better, I'll use that
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; |
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.
What if max_basis is 0? There used to be protection for that...
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.
Maybe returning 0 wasn't the answer, but will returning inf cause issues?
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.
Hmmm, if the basis is 0, we might not get here if we notice the degenerate transform elsewhere, but are we sure that happens?
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.
PointFieldGeometry also bails on basis == 0.
|
||
// 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_hairline && path_.IsSingleContour()) { |
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.
Does IsSingleContour guarantee no overdraw?
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.
Why would it need to?
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.
To prevent overdraw of alpha lines. If you stroke it as a path then the overdraw of a contour self-intersection is prevented, but breaking it into multiple line primitives would bring back that overdraw.
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.
You'd need this test to be IsSingleNonSelfIntersectingContour()
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.
Right, I still need to use the prevent overdraw setting
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.
Done
.transform = entity.GetShaderTransform(pass), | ||
}; | ||
} | ||
// For harline strokes that must be tessellated, switch to the cheaper |
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.
hairline
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.
Done
Cap cap = stroke_cap_; | ||
if (is_hairline) { | ||
join = Join::kBevel; | ||
cap = Cap::kButt; |
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 fear we might miss pixels/MSAA-samples with these. Round is really the one you want to avoid as that will attempt to generate a tesselated circle. Square caps shouldn't be a problem as it is just one additional vector add. Miter is more than bevel, but by how much?
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.
The caps/joins are made of triangles, but this change makes hairline strokes visible via line strip primitives. We can't draw the cap/join with connected points, and its not clear that we need to as they'd certainly be subpixel already.
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.
butt cap are 1 line width shorter than square caps. Bevel might miss any samples on the vertex that the true outline of a miter or round join would capture. These could produce squares that are missing corners depending on the sub-pixel registration of the line.
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.
hmm, I could bump the ends out by about that much then? So that would be add 1px to each end for square/round and nothing for butt?
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'm just going to remove this bit, its not really that important.
@@ -18,6 +18,7 @@ in mediump vec4 color; | |||
out mediump vec4 v_color; | |||
|
|||
void main() { | |||
gl_PointSize = 1.0; |
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.
Future opt - what does gl_PointSize render if the size > 1.0? Does it mimic any of the cap-styled points we need? Could we use this for non-hairline points of particular cap-styles?
There is also apparently a glLineWidth setting.
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.
What gl_Point does at larger sizes it somewhat backend dependent. on Vulkan for example, we can draw larger points but they are drawn as squares. GL has an optional extension to anti alias the point, but we can't guarantee its supported.
practically, we can only use this for hairline drawPoints
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.
gl_LineWidth is similar in that we can't guarantee anti aliased lines, and unfortunately its a pipeline setting so we'd need a new PSO per width which isn't practical
Golden file changes are available for triage from new commit, Click here to view. |
Some of the golden failures don't make sense as they look like something that shouldn't have been affected. I think some of the other failures could be due to pixel registration. What exactly is the equation for a line primitive? Does it extend half a pixel on both sides of the line? |
I suspect some of the goldens are just from switching to MaxBasisXY in the fill code. I'm removing that for now. |
Golden file changes are available for triage from new commit, Click here to view. |
I pulled the max basis change into #55670 (thanks for the review), so whatever is left should just be from this change |
I don't see how this change could cause the diffs in |
Okay, this is up to date and I've disabled the optimization for round/square caps. Filled flutter/flutter#157618 . I need to do some refactoring of the start/end vector to handle the arbtirary path case. |
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.
Most of these are just questions and comments, but at least one looks like it might result in a bug.
std::pair<Scalar, bool> LineGeometry::ComputePixelHalfWidth(Scalar max_basis, | ||
Scalar width) { | ||
Scalar min_size = kMinStrokeSize / max_basis; | ||
return std::make_pair(std::max(width, min_size) * 0.5f, width <= min_size); |
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 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...
@@ -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 for or either filled or stroked circles |
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.
"for or" typo
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.
Fixed
auto [width, _] = LineGeometry::ComputePixelHalfWidth( | ||
transform.GetMaxBasisLengthXY(), stroke_width_); | ||
half_width = 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 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 called StrokedCircle
which 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:
if (filled) {
generator = FilledCircle(...)
} else {
half_width = ...;
generator = StrokedCircle(..., half_width);
}
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"...
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.
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); |
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'm still confused on this code.
So, 0.0 means 1.0.
And >= min stroke size means 1.0
But there is a range between 0 and min_stroke where the alpha is <1?
That would create a visual break when reducing the width down to 0, would it not?
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.
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.
I'm having a hard time tracking down the failures in this PR. Instead, I'm going to split this up into smaller changes so i can hopefully find where the error was introduced. |
Split off from #55230 . Adds getter that tracks if a Path is a single contour. ################################ flutter/flutter#152702
…ine#56340) Split off from flutter/engine#55230 . Adds getter that tracks if a Path is a single contour. ################################ flutter#152702
For hariline (<= 1.0 physial pixel) strokes and points, we can use the line and point geometry primitives instead of tessellation. This gives substantially better performance.
flutter/flutter#152702
Still need to make sure we only handle single contour lines this way, since we can't insert degenerate triangles to break a line strip. We may be able to do this via primitive restart, but we don't yet use that.