Skip to content

Conversation

@flar
Copy link
Contributor

@flar flar commented Feb 24, 2021

This new API is basically the culmination of the API discussions that ensued from this document with one potential modification described below. It introduces a new property on the Transform (and related) Widgets that allows a developer to request that a coordinate transform be substituted with a bitmap transform while at the same time controlling the quality/speed tradeoff of the bitmap operation.

Recently, there has been some discussion about the value of our current bitmap quality hint enum FilterQuality which is ongoing at #76737. As a result I think the final form for the API for this particular mechanism should have one more level of abstraction as we may move away from the FilterQuality enum and so tying the choice of transform approach to that one enum might not be the best approach.

A slightly more abstract API would introduce a wrapper class, along the lines of:

class TransformMethod {
  const TransformMethod.matrix();
  const TransformMethod.bitmapWithQuality(FilterQuality filterQuality);
}

and then the filterQuality property introduced by this PR would morph into a transformMethod property with a nearly identical implementation.

If we later introduce new "sampling" controls, we could simply add another constructor TransformMethod.bitmapWithSampling(<SamplingType[s]>);

After spending some time looking to add a more finely tunable set of Sampling controls to the Flutter API, I decided that the existing enum and the 4 values that it exposes (though poorly named) represented as much control over bitmap sampling as it would make sense to provide. As a result, the API as is already proposed by this PR of adding a (nullable) FilterQuality property to various transform widgets would be enough to provide developers the control they need over the quality and performance of the transform operations.

@flar flar requested review from Hixie and yjbanov February 24, 2021 23:43
@flutter-dashboard
Copy link

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat.

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@flutter-dashboard flutter-dashboard bot added framework flutter/packages/flutter repository. See also f: labels. c: contributor-productivity Team-specific productivity, code health, technical debt. labels Feb 24, 2021
@google-cla google-cla bot added the cla: yes label Feb 24, 2021
@Hixie
Copy link
Contributor

Hixie commented Feb 25, 2021

What was your conclusion regarding the idea of specializing the various animation widgets around transformation (like ScaleTransition), following a model similar to the opacity-animating widgets, rather than doing it on Transform itself?

@flar
Copy link
Contributor Author

flar commented Feb 25, 2021

What was your conclusion regarding the idea of specializing the various animation widgets around transformation (like ScaleTransition), following a model similar to the opacity-animating widgets, rather than doing it on Transform itself?

That is an interesting idea that is independent of getting this API right and should be investigated in a separate effort. It did open my eyes to a new implementation for this API, though. Previously I had been working to make the engine transform layers do double duty until I realized that I could simply do the translation in the RenderObject with no changes to the engine required.

@Hixie
Copy link
Contributor

Hixie commented Feb 26, 2021

I don't think it actually is independent. If we were able to make the transitions use the efficient model, we may be able to avoid making any API changes at all, since we wouldn't necessarily need to expose this (at least, not on Transform, which is generally not directly animated).

@flar
Copy link
Contributor Author

flar commented Feb 26, 2021

I thought you were referring to the implementation detail where animation values were handed down to the RenderObject level so that the widgets didn't need to rebuild as the animation is driven.

You seem to be asking about an idea where the bitmap path is chosen automatically by the animation and transition widgets and I don't think that's a good idea for 2 reasons:

  • there would be a quality difference that the developer has to buy into. To buy in, they need a property to indicate that the lower quality of a bitmap transform is acceptable, otherwise it is just another behavior that can backfire on them
  • some animations are faster if done by a matrix operation instead of a bitmap operation. It is hard for the animation widgets to know if that is the case. It would be better to let the developer decide if the performance matters for their case

@flar
Copy link
Contributor Author

flar commented Feb 26, 2021

With respect to whether the base Transform widget needs to have this property, it will enable a custom animation that isn't driven by an AnimationController to use this mechanism. It would also allow a developer to create a "blocky zoom effect" if that is what they want their output to look like.

Developers have also indicated that they have uses for bitmap transformation that have nothing to do with animation as can be seen by this comment that appeared after the ImageFiltered issue was closed: #13489 (comment)

@Hixie
Copy link
Contributor

Hixie commented Feb 26, 2021

Ah, that makes sense.

@yjbanov
Copy link
Contributor

yjbanov commented Feb 27, 2021

Sorry, I must have missed the discussion that connects the Dirty Region Management design with this PR. How are the two connected?

It seems to me that the concern "I want the engine to rasterize my subtree and use a certain sampling quality when compositing" is orthogonal to the concen "I want to apply a transform matrix to my subtree". Have you considered keeping pushTransform as is and introducing a new layer, say RasterLayer, that offers the sampling options? Then any layer compositing the RasterLayer, including TransformLayer, would have to respect the quality setting.

@flar
Copy link
Contributor Author

flar commented Mar 1, 2021

Sorry, I had the wrong doc link in the description - fixed now. Some of the questions you asked are answered in the corrected document link.

In particular, sampling is associated with operations so it would normally go on a widget that performed an action.

Also, what coordinate transform would a RasterLayer rasterize at? It is inside the Transform widget and has no idea what the environment was like outside the "action" layer that it is supposed to be turning into a bitmap operation. As we descend the tree we get a Transform that will change the matrix stored in the context, then it will ask the RasterLayer to do its thing and the RasterLayer will - rasterize itself at the indicated matrix? Next frame there is a new matrix and the RasterLayer doesn't really have the information to do much more than just render again under the new matrix.

There are ways we could work around this, but it would require a lot more communication between layers to coordinate when most of the system is designed around single-pass depth-first tree traversals. It is easier for the RenderTransform to make the decision as to which type of layer to use depending on whether the Transform widget was given permission to switch to a bitmap operation - almost no mechanism involved in such a case.

@Hixie
Copy link
Contributor

Hixie commented Mar 2, 2021

This seems fine to me but I'll let @goderbauer or @yjbanov do the actual detailed review.

@flar is there a bug filed on making the more efficient transformation animation widgets? Like AnimatedRotation, etc?

@flar
Copy link
Contributor Author

flar commented Mar 2, 2021

@flar is there a bug filed on making the more efficient transformation animation widgets? Like AnimatedRotation, etc?

I can check again, but after running through them as I was creating this PR I wasn't sure that there is much missing.

ScaleTransition and RotatingTransition both exist to animate scales and rotations. The AnimatedFoo widgets seem to be designed around responding to changes in the child so we have AnimatedSize that responds to a child's size, but not something like AnimatedScale since children don't usually have an observable "scale" (or "rotation") to observe and control through animation (the changes in size can be detected by watching the results of the layout pass and comparing to the former size).

When I wrote the doc and when we had a discussion about the designs in it I seemed to feel that there were missing holes, but I think we're pretty much covered for the common cases.

@Hixie
Copy link
Contributor

Hixie commented Mar 2, 2021

The AnimatedFoo widgets seem to be designed around responding to changes in the child

They're about automatically responding to changes in the widget's properties. See e.g. AnimatedOpacity vs FadeTransition.

@flar
Copy link
Contributor Author

flar commented Mar 2, 2021

The AnimatedFoo widgets seem to be designed around responding to changes in the child

They're about automatically responding to changes in the widget's properties. See e.g. AnimatedOpacity vs FadeTransition.

Ah, I see the distinction. I was looking primarily at AnimatedSize which is reacting to a child's size, but others actually maintain a parameter themselves that can be changed atomically by a builder, but result in an animated change at the RenderObject level.

So we are missing AnimatedScale which would maintain a scale property of its own and apply it slowly over time, and potentially AnimatedRotate as well. Both animated versions of Transform.scale and Transform.rotate. Potentially AnimatedTransform.rotate/scale. I'll file an issue.

@flar
Copy link
Contributor Author

flar commented Mar 2, 2021

Filed #77081 to address the issue and noted that there is also an existing issue already filed by a developer looking for the same feature in #65183

@yjbanov
Copy link
Contributor

yjbanov commented Mar 5, 2021

Heads up. Unfortunately, I won't be able to get back to this until next week.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to null out the layer if child is null, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to. It won't be used on this pass, but it might be used if we have a new frame that does have a child. It wasn't originally cleared in the null child case, so that hasn't changed here.

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've still left this in the latest push because it was present in existing logic so I didn't want to change code I wasn't specifically introducing to fix the issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this layer it going to unconditionally push a layer, then we need to make sure that we set needsCompositing to true, or that alwaysNeedsCompositing is set to true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding/removing a child (via adoptChild) will request an update to the compositing bits. It looks like the update method here will look for a child that requires compositing.

What I think may be missing (and it was missing before my modifications) is that if the transform evaluates to a translation then we avoid a layer and that won't be reflected in the value of needsCompositing for this RO or its parents. That issue is not changed here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that's the case. Previously we had:

if (childOffset == null) {
  layer = context.pushTransform(needsCompositing, ..., oldLayer: layer);
} else {
  super.paint(context, offset + childOffset);
  layer = null;
}

This says that if it's not a simple translation (the if branch), then context.pushTransform will either push a layer or inject a canvas transform, depending on the value of needsCompositing. If it's a simple transform (the else branch), we don't push layers. So there are 3 possible outcomes:

  1. Not simple translation, needs compositing: push layer
  2. Not simple translation, needs not compositing: no layer
  3. Simple translation: delegate to children

There's no situation when needsCompositing == false and we push a layer.

After this PR, we have the following situation: filterQuality != null, needsCompositing == false => push a layer.

It's OK to push a layer when none of the descendants need compositing, but we still need to set the render object's own needsCompositing to true. Otherwise, its ancestors will do wrong things.

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 see. Basically it would only push a transform layer if a child was already planning to composite (push a layer?). So, it was impartial wrt whether it was going to push a layer itself - if no children were planning to, then it didn't. Thus, this layer used to not have any additional requirements as to the compositing bits other than the "standard" pass through from the children.

Now that FQ means we will be pushing a layer, that needs to factor in...

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 have now updated the alwaysNeedsCompositing handling to match what I think you were saying here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the latest iteration looks correct.

@flar
Copy link
Contributor Author

flar commented Apr 9, 2021

@yjbanov do my clarifications above answer your questions?

@yjbanov
Copy link
Contributor

yjbanov commented Apr 28, 2021

The lib/ code LGTM. My last request is to add tests. Let's make sure we have a test case that switches between null and non-null filterQuality and child to make sure the layer gymnastics that takes place in the render object doesn't trigger something bad. It would also be good to have some screenshot tests.

@skia-gold
Copy link

Gold has detected about 1 untriaged digest(s) on patchset 4.
View them at https://flutter-gold.skia.org/cl/github/76742

@flar
Copy link
Contributor Author

flar commented May 5, 2021

The windows tests appear to be a flake. I'll try rebasing and see if they go away.

But the linux failure appears to be due to #45213. Since the HTML renderer does not support ImageFilter.matrix currently I'll either have to solve that issue or find a way to work around it as the FilterQuality property is just a hint and shouldn't cause a web app to fail with that exception. The exception occurs in the constructor so we can't have the html renderer detect the type and work around it - that constructor would have to not throw the exception first. Alternately we could catch the exception when RenderTransform tries to instantiate the filter and back off to the regular transform paths.

@flar flar force-pushed the add-filter-quality-to-transform-widgets branch from b9b06a7 to f54dc5b Compare May 19, 2021 18:22
@skia-gold
Copy link

Gold has detected about 1 untriaged digest(s) on patchset 4.
View them at https://flutter-gold.skia.org/cl/github/76742

@flutter-dashboard
Copy link

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.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #76742 at sha f54dc5bf959c637b726c094ab3e4e0d21e283d80

@flutter-dashboard flutter-dashboard bot added the will affect goldens Changes to golden files label May 19, 2021
@flar flar force-pushed the add-filter-quality-to-transform-widgets branch from f54dc5b to ebe822e Compare May 20, 2021 19:58
@flar
Copy link
Contributor Author

flar commented May 20, 2021

Tests added. The golden tests are no longer asking for triage. It should be good to go pending an approval...

Copy link
Contributor

@yjbanov yjbanov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: hasLength would produce a better failure message, i.e.:

expect(tester.layers.whereType<ImageFilterLayer>(), hasLength(1));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: for testing zero length consider using the isEmpty matcher, also for better error messages (e.g. "expected empty, but found 5 elements")

@skia-gold
Copy link

Gold has detected about 1 untriaged digest(s) on patchset 5.
View them at https://flutter-gold.skia.org/cl/github/76742

@flar flar force-pushed the add-filter-quality-to-transform-widgets branch from c749f4f to 09aeb23 Compare May 25, 2021 18:43
@flar flar merged commit ae12bf6 into flutter:master May 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: contributor-productivity Team-specific productivity, code health, technical debt. framework flutter/packages/flutter repository. See also f: labels. will affect goldens Changes to golden files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants