-
Notifications
You must be signed in to change notification settings - Fork 6k
Add cache transform to TransformLayer #21943
Conversation
|
Question for @yjbanov - is there any way to implement this for web? Or will it provide any improvement? The values will just be ignored on web which is fine since they are a hint and don't modify the output (except for a speed/quality tradeoff). |
|
It occurs to me that perhaps this should be restricted to an affine matrix for the cacheTransform argument. Translation should not really affect anything (though sub-pixel resolution might affect the blurriness of edges), and perspective isn't useful for any real world cases that I can think of. Right now I have the prototype changes for the framework taking a Matrix4 for these properties, but perhaps they should take Matrix2 (or Matrix3, which would allow sub-pixel translations, but would also allow them to muck with the 3rd row that really should be Thoughts? |
| /// See [pop] for details about the operation stack. | ||
| TransformEngineLayer? pushTransform( | ||
| Float64List matrix4, { | ||
| Float64List? cacheMatrix4, |
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.
Perhaps name this just cacheMatrix and restrict it to 3x3 or 3x2 or 2x2 depending. The vec64 package only allows Matrix2,Matrix3,Matrix4 so 3x2 isn't really available...
1eb66b7 to
5c389b5
Compare
5c389b5 to
291883f
Compare
|
|
||
| TransformLayer::TransformLayer(const SkMatrix& transform) | ||
| TransformLayer::TransformLayer(const SkMatrix& transform, | ||
| const SkMatrix* cache_transform) |
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 wonder if it's simpler to send in just bool cache_child instead of const SkMatrix* cache_transform. It would be equivalent to always have cache_transform = identity if cache_child = true. Then users don't have to figure out what cache_transform needs to be.
In case of a more complicated scale + rotation transform, I'd imagine something like
TransformLayer(
transform: some_scale_matrix,
cache_child: false,
child: TransformLayer(
transform: some_rotate_matrix,
cache_child: true,
child: some_complex_child,
);
);
instead of
TransformLayer(
transform: some_scale_and_rotate_matrix,
cache_transform: some_scale_matrix,
child: some_complex_child,
);
);
Using the boolean flag, it's also natural to extend this to the RepaintBoundary widget which generates an identity TransformLayer. Then we can provide this caching mechanism to any parent widget. (Compared to our Opacity cache, the only downside of this cache is being unable to reduce one saveLayer.)
Moreover, without the additional matrix, we'd have a simpler RasterCacheResult::drawTransformed that takes only 1 matrix instead of 2. That seems to reduce some complexity without sacrificing much performance improvement.
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 this a suggestion for just the engine, or for the Widget API as well?
My concern is that if they have a widget at some transform and want to animate it to a new transform then the matrix decomposition math could get tricky. If they specify 2 transforms then they could do something simple like copy the "before" transform into the cache property, animate the transform matrix to the new values, then remove the cache property. No matrix math needs to be done in that case other than what they are already doing to animate from A to B.
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 a very common case, though, the cache transform would likely be identity. On the other hand, cacheTransform: Matrix4.identity() is not that hard to understand.
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.
Another thought is to use an enum that describes how to apply the transform, with values something along the lines of:
- (re)render (default)
- transform_as_bitmap
- (future?) transform_with_mipmap?
This would be similar to the clip behavior enum for Clip objects.
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 like the enum approach. Calling out the difference between "(re)render" and "transform_as_bitmap" is important as the latter creates a new offscreen layer and may actually change the semantics and the visual result in addition to the performance difference.
Animating from matrix A to B is indeed a common case that we should support well. Caching bitmap using the start matrix A may cause some problems though. For example, if A is scale_half and B is scale_two, applying A as cache_matrix would make the animation pixelated. Forcing developers to write something like
TransformLayer(
transform: some_parent_scale_transform_to_avoid_pixelation,
cache_behavior: rerender,
TransformLayer(
transform: animating_from_A_to_B_modulo_parent_scale, // some complicated and possibly perspective matrix
cache_behavior: bitmap,
child: ...
),
),
would avoid such problems without forcing them to do complicated matrix decomposition?
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 working on a PR that uses the enum, but I'm running into a problem exposing that enum from my engine repo through to the test app. The changes to the method signatures in my engine repo seem to be propagated, but the brand new enum shows up as "Type not found".
My issue with the decomposition is broader than simply "which scale do I factor out". It is likely that one would want to factor out the greater of the two during the transition and have the animating transform be relative to that. For example, if going from 2->3, you'd want to cache at 3 and animate from 2/3 to 1.0. If going from 5->4 you'd want to cache at 5 and animate from 1.0->4/5. But, a developer may actually want the pixelization, depending on their tastes.
No, the issue that I have with the decomposition is the following. You specify Matrix4.scale(2.0) as the matrix. But, the matrix that gets used is not [2,0,0][0,2,0]. It is actually a composed matrix that deals with alignment and such. This is the issue I had with the ImageTransformed work-around - you couldn't just say IF(filter.matrix(rotate(theta)) because that would rotate it around the origin. You had to do extra work to find the actual location of the widget, find its center, and compose a matrix from translate(x,y); rotate(theta); translate(-x, -y). And the x,y isn't known to the app, it has to be queried using methods that aren't available on the first call to build(Context). Similarly, do you want to scale around the center of the widget, or the origin?
By putting both on the Transform() widget, I am able to ensure that both matrices are adjusted the same way as we process them before handing them to the engine.
Now, with 2 nested Transform() widgets instead (the outer one using a bitmapTransform operation), things may be OK due to our framework code handling the alignment of both, but the adjustments of the nested transform won't be the same as the adjustments to the outer one. I won't know how easy that is until I get my new variation of the mechanism working with a test app.
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 "enum" variation of this solution is now submitted as a new PR: #22045
I will close whichever of these solutions is not used when one of them is finally merged.
|
This will be solved more simply in the Framework by flutter/flutter#76742 |
The engine changes that will eventually enable fixing flutter/flutter#24627
An additional PR for the Framework repo will be required to use these changes, but the new property I added here should be backwards compatible to existing Frameworks as it defaults to the compatibility value (null).
This is the engine work preparing for adding a (nullable) cacheTransform property to the Transform widget in the framework.
By default this property will be null and the TransformLayer will do the same thing it has always done. If a Matrix4 value is specified for the cache property, then TransformLayer will use the cacheTransform for populating the cache and then apply the transform property as a texture transform operation on the cache entry.
On an animation test (see below for gist) various techniques for animating a transform produced the following render timings (taken from the Performance Overlay average rendering value):
Note that the ImageFiltered solution to animating the transform helps a lot with complex children, but can actually be slightly worse for simple children. But, the new cacheTransform property on the Transform widget is the fastest animation solution for both complex and simple children.
In addition to the improved performance, the cacheTransform property is much easier to use as compared to the ImageFiltered widget. In most cases, the performance can be improved by simply adding
cacheTransform: Matrix4.identity(),to the Transform widget construction. An issue with needing to query the rendering location of the child and manually adjust for transformation alignment in the ImageFiltered case is also eliminated as the Transform will apply the requested alignment for both transforms consistently and transparently.Here is the gist of the test used for the numbers shown above. All tests above were run with "child RPB" enabled: https://gist.github.com/flar/49fb1c70b8cc2de16af5bd91dbcd34ac
The test also provides 2 sliders for playing with the rotation and scale of the cacheTransform property which help verify that the location, size, and orientation of the final rendered child will be the same regardless of the caching transform - only the quality of the results might vary with a poorly chosen cache transform (ideally most developers would just use an identity cache matrix, but they might include a scale or rotate if the originating or final state had one). The sliders default to Identity values, but changing them should verify that they won't have any (significant) visual changes in the output, other than pixelization quality changes from dropping the scale slider too low. (You can see an interesting artifact if you drop the scale really low and then play with the rotation slider as you start to see rounding errors as the text code tries to render rotated text with very few pixels to work with.)