Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@flar
Copy link
Contributor

@flar flar commented Oct 17, 2020

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):

With "complex child" and "child RPB" both enabled:
Transform without caching:       ~56   ms/frame ########################################################
ImageFiltered widget:            ~14.5 ms/frame ##############=
Transform with cacheTransform:    ~7   ms/frame #######

With only "child RPB" enabled (simpler child rendering):
Transform without caching:       ~12.5 ms/frame ############=
ImageFiltered widget:            ~14.5 ms/frame ##############=
Transform with cacheTransform:    ~7   ms/frame #######

(All measurements milliseconds per frame - shorter bars are better)

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.)

@flar flar requested review from liyuqian and yjbanov October 17, 2020 02:54
@flar
Copy link
Contributor Author

flar commented Oct 17, 2020

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).

@flar
Copy link
Contributor Author

flar commented Oct 18, 2020

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 [0,0,1]).

Thoughts?

/// See [pop] for details about the operation stack.
TransformEngineLayer? pushTransform(
Float64List matrix4, {
Float64List? cacheMatrix4,
Copy link
Contributor Author

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...

@flar flar force-pushed the transform-with-caching branch from 1eb66b7 to 5c389b5 Compare October 19, 2020 06:21
@flar flar force-pushed the transform-with-caching branch from 5c389b5 to 291883f Compare October 19, 2020 19:23

TransformLayer::TransformLayer(const SkMatrix& transform)
TransformLayer::TransformLayer(const SkMatrix& transform,
const SkMatrix* cache_transform)
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@flar
Copy link
Contributor Author

flar commented Apr 28, 2021

This will be solved more simply in the Framework by flutter/flutter#76742

@flar flar closed this Apr 28, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

cla: yes Work in progress (WIP) Not ready (yet) for review!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants