-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Description
(With some recently learnings from the breakage I caused in #111145)
Certain operations on the canvas can look much better if they are aligned to physical pixels, for example drawing an image could be pixel aligned. This is especially noticeable on low DPR devices, though it can be noticed on higher DPR devices with a careful eye.
Flutter uses logical pixels for all drawing operations. Logical pixels convert to physical pixels at a ratio specified by the specific device the Flutter application is running on. For example, my home desktop monitor has a configured DPR of 1.25, meaning for every 4 logical pixels, there are 5 physical pixels. Many mobile devices have device pixel ratios greater than 2.
In framework code, it is possible to convert logical pixels to physical pixels and vice-versa using the windows devicePixelRatio. Additionally, due to the current unconditional presence of the raster cache pixel snapping, we can assume that every picture has an origin which is always aligned to a physical pixel. That is, 0,0 in each picture is guaranteed to fall on a pixel boundary. It seems like it may be possible for user code to manually pixel align, but there are some problems today:
Logical to Physical Conversion
All canvas APIs accept logical pixels (except those that refer to samples/dimensions from an Image). In order to align an operation, you would need to convert the requested logical pixels to physical pixels, round, and then convert back to logical pixels:
var dxx = (dx * devicePixelRatio).round() / devicePixelRatio;
var dxy = (dy * devicePixelRatio).round() / devicePixelRatioHowever, the rounded out physical pixel does not necessarily correspond to an exact logical pixel. This may leave the ultimately chosen destination at the mercy of floating point math. Consider, we want to draw an image at the logical pixels (23, 42.5) with a device pixel ratio of 1.25. We convert this to the physical pixels (28.75, 53.1250). This can be rounded to (29, 53) which we then convert back to logical pixels of (23.2, 42.4). Everything is good!
But what happens if we have a DPR like 3.33? For the same X coordinate, we convert, round , and convert back ending up with 23.1231231231. Hopefully that rounds to a physical pixel, but it might not.
Knowledge of Accumulated Transformations
The above conversions only work if the only transform above the current picture is the DPR transform. It is possible for a given widget or render object to compute a "localToGlobal" or "globalToLocal" transform, but there is no guarantee the render object will repaint if this changes. Consider the following widget tree:
Transform.scale(x, child: RepaintBoundary(child: Widget()))The first time that Widget paints, it can compute the total transform and incorporate that to ensure it is still pixel aligned. However if the scaling value changes, then because of the RepaintBounary wrapping Widget, it will not be repainted. Thus the pixel alignment will fail.
For other sorts of transformations, animations are common, and pixel alignment may not be important. For example, if you are apply a perspective transform or doing a rotation, then there may be no benefits in pixel alignment.
Other workarounds
The only 100% guaranteed way to pixel align the origin of a drawing operation is to place it in its own Picture using a RepaintBoundary. However, adding repaint boundaries can also regress performance by adding more layer management.
Ideas
Could we delegate pixel alignment to the engine? For example, in the same way we round out the bounds and origin for raster caching. The canvas/display list code could also encode some intent that some operation should be pixel aligned, and use its knowledge of the CTM to compute that exactly. Would this have an impact on raster caching?