-
Notifications
You must be signed in to change notification settings - Fork 28.6k
Severe performance issues when drawing transformed widgets that contain beziers. #78543
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
Comments
This is somewhat related to #72718. |
cc @flar |
This happens at the transition from hairline stroke widths to non-hairline stroke widths. At rest the stroke width is 0.3 pixels which is less than a pixel and a fast algorithm that draws paths that stroke only single pixels is used. As soon as the scaling causes that stroke width to exceed a pixel in size, complex geometric calculations must be performed to determine which pixels are affected by the stroking and by how much. Non-hairline stroking is a complex operation that comes at a large cost. Changing the strokeWidth in the example to 0.0 eliminates the performance loss, but the results will look different. Here are some screen shots of the tracing showing the difference in the operations: |
…y doesn't take into account scaling fully
@flar thanks for the feedback on the issue. I'm looking into similar at the flutter_Map vector support issue Git referenced previous to this, and I think we do get the same problem. Thin strokes are really quick, fatter strokes really slow. I'm just trying to figure any workarounds for this. Do you know if the problem is simply that the stroke is thick ? Or is it that it is transformed ? I.e I've been playing with pretransforming the paths (rather than for example a transform on the canvas), but it didn't seem to make any difference. Do you have any thoughts in design or workarounds that would avoid even hitting the problem (other than having hairline stroke widths only) ? Just wondering what other none flutter canvas solutions do to get around the problem ? |
@ibrierley the hairline renderer does something really fast that can't really be "pre"-pared out of it and it can only be used when the width of the path is less than a device pixel. It's basically the difference between grabbing a crayon and dragging it from point A to point B vs. drafting the edges of the "stroke" and then using the same crayon to carefully fill inside those edges. One action is nearly instantaneous, the other involves elbow grease and sweat. Whether a stroke is a hairline or not is basically "transform the width and see if it is >= 1.0 pixels". So, it's a product of stroke width and the scale of the transform. About the only way to keep in the "fast path" would be to try a stroke of 0, but when you zoom that in really far you will see the difference since the hairlines won't fill the space as much as a non-hairline would. |
Thanks a lot for the explanation. I think it's useful to know as there may be a few small optimisations I can make using it. I'm guessing there's no way to force it off (at the expense of accuracy), I had wondered if during animations and zooms it could have been forced off somehow (as you care less then), and then when settled switched for accuracy if that makes sense. I suppose I was thinking about things like SVGs "crispEdges" vs "optimizeSpeed" that has that trade off. |
Just to be clear, "forcing it off" is setting the width to 0. That seems to make it fast regardless of scale. |
I had the same issue with my The solution for me was to switch from path drawing to line drawing. This might not work for every use case and you can't fill the drawn area any more, but for me that worked and eliminated the lags. Just for the sake of completeness, this is what I did before which caused lags: for (int i = 0; i < pointList.length; i++) {
if (i == 0) {
path.moveTo(pointList.first.dx, pointList.first.dy);
} else {
path.lineTo(pointList[i].dx, pointList[i].dy);
if (i == pointList.length - 1 && closeShapes) {
path.lineTo(pointList.first.dx, pointList.first.dy);
}
}
canvas.drawPath(path, linePaint);
} This is what I do now -> no more lags for (int i = 0; i < pointList.length; i++) {
if (i != 0) {
canvas.drawLine(pointList[i - 1], pointList[i], linePaint);
if (i == pointList.length - 1 && closeShapes) {
canvas.drawLine(pointList.last, pointList.first, linePaint);
}
}
} |
Drawing a path with a lot of lines is time consuming as the stroke geometry has to be calculated. Did you try drawPoints as well? Here are the things you give up when drawing a bunch of line segments instead of making a path (or drawPoints polygon):
|
This is interesting, I had been looking for a drawLines method but couldn't find one (and was going to request if it could be exposed), had assumed drawLine would be slower due to needing multiple instructions, but maybe not. I'll probably give this a test when I get a chance as I draw a lot of lines. I'm a little confused though...why does a drawPath have stroke geomoetry to be calculated but multiple drawLine doesn't (if that is the case and assuming here we're talking about multiple lines and not curves), is this just because of corner joins ? Is there a possible optimisation to be done by exposing a drawLines option (i.e one instruction, lots of points, less geometry to be calculated, if you don't need fill or any of the other parts flar suggests)? |
@flar You're right about the drawbacks, but at least for my specific usecase the performance improvement is the clear winner. Now, I only have to sacrifice some styling options, before (with path) scaling was not possible at all. And yes, I use @ibrierley Yes, the |
The geometry of a single drawLine can be easily hardcoded. If the endcaps are BUTT or SQUARE then it is a simple rectangle aligned with the direction of the line. If the endcaps are ROUND, then it is a rounded rectangle aligned along the direction of the line. A few instructions just dispatches the appropriate primitive rendering. The geometry of 2 lines in the same path invokes a general algorithm which follows the path, computes joins and caps which involves a bit of trigonometry at each vertex for the joins, etc. It also expresses the outline of all the geometry in the path in a single resulting path so that it is filled in one single operation rather than in a number of separate operations so that there is no overdraw of any of the pixels resulting in the issues described above. Even if the path could identify "hey, I'm actually just a list of moveTo/lineTo pairs", that wouldn't be enough to alleviate the need to compute all of that and put it into a single path to fill because of the need to make it appear to be a single operation. They could maybe be broken out and performed individually if the path knew that it was a bunch of lines that don't overlap, but since knowing if they overlap involves knowing the stroke width, and since the path does not store the stroke width (it is in the Paint object), then such a revelation would require a lot of computation each time the path-of-lines is drawn - so you are back to lots of computation and thus the solution is to just go through the work of computing a stroke-path and filling it.
The drawPoints method is a single call, I'm not sure how that doesn't fit the bill here? |
Ah interesting again, thank you very much. I hadn't even spotted drawPoints
& PointMode.lines, I had naively assumed it was simply as it sounded, for
points, so it may indeed to what I wanted faster (makes me wonder if there
should be an alias drawLines for that now, I wonder how many others didn't
spot that, but don't want to sidetrack the issue).
I'll give it a go when I get chance in a few days and feed back anything
useful I find.
Thanks again.
…On Thu, Jul 22, 2021 at 7:32 PM Jim Graham ***@***.***> wrote:
I'm a little confused though...why does a drawPath have stroke geomoetry
to be calculated but multiple drawLine doesn't (if that is the case and
assuming here we're talking about multiple lines and not curves), is this
just because of corner joins ?
The geometry of a single drawLine can be easily hardcoded. If the endcaps
are BUTT or SQUARE then it is a simple rectangle aligned with the direction
of the line. If the endcaps are ROUND, then it is a rounded rectangle
aligned along the direction of the line. A few instructions just dispatches
the appropriate primitive rendering.
The geometry of 2 lines in the same path invokes a general algorithm which
follows the path, computes joins and caps which involves a bit of
trigonometry at each vertex for the joins, etc. It also expresses the
outline of all the geometry in the path in a single resulting path so that
it is filled in one single operation rather than in a number of separate
operations so that there is no overdraw of any of the pixels resulting in
the issues described above.
Even if the path could identify "hey, I'm actually just a list of
moveTo/lineTo pairs", that wouldn't be enough to alleviate the need to
compute all of that and put it into a single path to fill because of the
need to make it appear to be a single operation. They could maybe be broken
out and performed individually if the path knew that it was a bunch of
lines that don't overlap, but since knowing if they overlap involves
knowing the stroke width, and since the path does not store the stroke
width (it is in the Paint object), then such a revelation would require a
lot of computation each time the path-of-lines is drawn - so you are back
to lots of computation and thus the solution is to just go through the work
of computing a stroke-path and filling it.
Is there a possible optimisation to be done by exposing a drawLines option
(i.e one instruction, lots of points, less geometry to be calculated, if
you don't need fill or any of the other parts flar suggests)?
The drawPoints method is a single call, I'm not sure how that doesn't fit
the bill here?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#78543 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AA5YN5N7GXG7POQFPDQFJ5TTZBP4FANCNFSM4ZM2NDYA>
.
|
To be clear, drawPoints has 3 modes:
line and polygon mode differ only in how they pair the lines up. lines takes 2 points per line and polygon takes 1 and reuses the previous point for its lines |
Had a bit of a play, it seems a little faster to draw, but it also seems to crash sometimes when there's a lot of points, (with no error). Will post back if I can isolate it further. |
For what it's worth I've tracked it down to
I can use over 5x as much memory (checking using the profiler) using drawPath and it doesn't crash though, so not sure if there's some limitation with the drawPoints method and how much memory can be allocated to the gpu or something ? Not quite sure how it all ties together. I feel this is maybe a distraction to the original problem though, so I'll leave it there. |
@flar did you happen to look at this on Impeller? I'd expect it to not be as problematic there but I'm not sure I'm getting the full scope of the problem. |
I have not. |
Does this have to do anything with hardware acceleration? On android atleast I can confirm this.
Here, Saber is a flutter application. This is referenced from an issue here saber-notes/saber#472 Device: Samsung Galaxy Tab S6 Lite. |
any progress on this issue 👀 |
Or any workaround to reduce this latency? |
It is impacting performance strongly! In fact, some apps are barely useable. So, resolving this issue is of high priority I would argue! |
All work on path rendering is being done on the Impeller back end so it would be good to know how the performance is for your applications on that back end. |
Any workaround to test it on Android? cf. saber-notes/saber#179 (comment) |
Update on #78543 (comment) The issue was not with the clipper but with the |
any progress on this issue 👀 |
This issue seemed to have gotten worse on Impeller. Drawing a lot of strokes on a CustomPaint is way slower with Impeller. |
Does it make any difference if they are stroke width over size 1px vs under 1px? |
All future performance work is targeting Impeller.
It is not impossible to solve, but it is much easier to solve when Flutter can control the entire stack down to the hardware which is why we are focusing on Impeller for the future. You mentioned "ios native" above. Have you tried this with Flutter on iOS using Impeller? |
Yes, I also tried it on iOS and had this problem too. I can assist with testing, is there anything I can provide?
|
I tested Flutter SDK 3.16.9 with impeller enabled on iOS. |
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of |
Uh oh!
There was an error while loading. Please reload this page.
The video below shows a CustomPainter that draws hundreds of cubic beziers.
The CustomPaint Widget is transformed using an InteractiveViewer Widget. After a certain zoom level the performance degrades severely from 4ms to 80ms+
Bildschirmaufnahme.2021-03-18.um.15.32.03.mov
The text was updated successfully, but these errors were encountered: