-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Teach render objects to reuse engine layers #36402
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
Conversation
liyuqian
left a comment
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.
Only reviewed 4 files... Will resume reviewing tomorrow.
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.
This is a breaking change. Some inherited classes may need to update their signatures.
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 reverted the change. I only changed it because it didn't feel right. It puts the responsibility of setting the _engineLayer field on the caller of addToScene. This means that the caller always has to do this:
childLayer._engineLayer = childLayer.addToScene(builder);
There are two issues with this. One is that _engineLayer is private and so a custom layer implementation won't be able to do this correctly. The second is that the caller needs to remember to do this. They also need to make sure they do this correctly. I found a bug in _ProxyLayer that returned another layer's engine layer, which is wrong. It only manifested after my change because my change exercises the new assertions I added in the SceneBuilder API.
However, there's no rush to fix this. We can do this as a follow-up PR.
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 remembered that we've had this conversation before. Maybe let's create a Github issue to track our discussions and decisions so we won't forget it again in a few months :-)
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.
Update the document if the return type is changed to void.
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.
Reverted the change.
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.
Do we currently have use cases for engineLayer in Leader/FollowerLayers? I remember that I explicitly set engine layer to null because Leader/FollowerLayers break assumptions of the retained rendering. Hence I don't want someone to accidentally use retained rendering for them.
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.
Yes, when a leader/follower layer is moving without changing its children, we want to reuse the engine layer DOM nodes associated with pushTransform.
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 see. Maybe add a comment somewhere like "The engine layer's resources may be reused through oldLayer, but do not reuse the engine layer's rasterized pixels through addRetained directly because there's no guarantee that the pixels would stay the same even if the whole subtree is unchanged."
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.
There's plenty of documentation on the oldLayer parameter about what it does. It might be overkill to document it again for every invocation of push* method, no?
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.
This is specific to the Leader/FolllowerLayer. Previously, it's impossible to re-use the engineLayer of a Leader/FollowerLayer for addRetained because we return null.
Now we return the engineLayer for oldLayer, but we still don't want developers to use it for addRetained. I don't think that we need to document every push* method. Just documenting Leader/FollowerLayer.addToScene would be enough. Maybe something like "Only use the returned engineLayer for oldLayer in .... Do not use it for addRetained".
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.
Shall we also invalidate the layer's engineLayer at this point?
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 think that's layer-specific. The old engineLayer may still be needed to be passed as oldLayer during compositeFrame.
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 see. So engineLayer could either be:
- in a state that's synchronized with the layer so we can directly use it in
addRetained, - in a stale state where one can only use it as
oldLayer.
Is there some mechanism that we can enforce the developer to not misuse such two states of the engineLayer?
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.
Yes, I added a bunch of assertions in the SceneBuilder in the engine code.
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.
LeaderLayer shouldn't even have an engine layer, should it?
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.
@yjbanov ^
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.
LeaderLayer sometimes creates a TransformEngineLayer:
| builder.pushTransform(Matrix4.translationValues(_lastOffset.dx, _lastOffset.dy, 0.0).storage); |
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.
Oh, right.
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.
This may be a breaking change as we're changing the return type. Maybe we can just assert(_layer is OffsetLayer) and return an OffsetLayer as this is just for backward compatibility.
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.
Hmm. Interesting idea. Since I'm deprecating it, might as well keep the same type. I'll give it a shot.
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.
Done.
|
cc @goderbauer |
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.
this should be protected. Nobody outside this object should be reading it as far as I can tell.
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.
There are 41 unprotected usages. Most are in tests (which can be fixed by making it @protected @visibleForTesting). However, there are 18 usages in package:flutter_test, widget_inspector.dart, and rendering/binding.dart, that need this to remain public.
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.
How many of these are for debug purposes (for which we could use a debugLayer) vs production?
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 was able to convert all but 5 to debugLayer. Here's the production code that uses layer:
rendering/binding.dart:
| return renderView.layer.findAll<MouseTrackerAnnotation>(offset * window.devicePixelRatio); |
multiple places in widget_inspector.dart (although I'm not sure if widget inspector is considered debug code):
| data.containerLayer.append(_ProxyLayer(repaintBoundary.layer)); |
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.
Looking the surrounding code in widget_inspector.dart it's all debug code, so I was able to convert it to debugLayer as well. So, the only remaining production usage of layer is the MouseTracker in rendering/binding.dart.
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.
Let's move that closure into a method on RenderView, and then pass that method in rather than using a closure. That'll be generally cleaner and will allow us to make this protected.
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.
why do we want to remove this?
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.
Because any render object can create and keep a layer the normal layer getter no longer requires isRepaintBoundary and !_needsPaint. So this getter no longer adds any value to tests. They can as successfully use the layer getter (in fact, I moved all our existing tests to it).
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.
It seems to me that this should still require needsPaint. All the more so now than before. Indeed the setter probably should have asserts too: we presumably should never change our layer except during paint.
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.
In the past it required !_needsPaint:
| assert(!_needsPaint); |
With this change it no longer requires it, because, as you say we're producing layers during paint. Are you suggesting that we should flip the requirement to require _needsPaint?
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.
Actually, we're clearing _needsPaint before calling paint, so requiring _needsPaint in the layer getter won't work:
| _needsPaint = false; |
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 meant that layer should require !needsPaint, sorry for the confusion above.
If we make layer protected I guess I'm not as worried about this. Though it sounds like we still want the debugLayer for those places outside of RenderObject that want to read the layer during development.
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.
here and in other places, might it be better to just create the layer using oldLayer ??= PhysicalModelLayer() and then set the attributes unconditionally? Do the constructors do anything more efficient than that?
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 wasn't worried about efficiency as much as I was about API compatibility. Most layer constructors either have @required parameters, or they assume non-null arguments. I can change the constructors to allow blank objects. WDYT?
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.
We can give them default values, or we can allow null and document they have to be non-null before adding to scene and add the relevant asserts (we have some cases like that already). I think the ergonomics will be nicer if we remove the @requireds and just have the one way of doing this, though. Having both ways is going to result in bugs when we accidentally only fix one code path.
liyuqian
left a comment
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.
Remaining files scanned :-)
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.
Can we have a null layer here?
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.
Done
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.
Same here. It doesn't seem to me that layer could be null at this point.
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.
Done.
Description
Teach
Layerand its implementations,RenderObjectand its implementations, andPaintingContextto reuse engine layers. The idea is that a concreteRenderObjectcreates aLayerand holds on to it as long as it needs it (i.e. when it is composited, and the layer type does not change). In return, eachLayerobject holds on to anEngineLayerand reports it to the engine viaaddRetainedandoldLayer. This allows the Web engine to reuse DOM elements across frames. Without it, each frame drops all previously rendered HTML and regenerates it from scratch.Because render objects can manage their own layers,
RenderObject.layermay now be called any time. This rendersRenderObject.debugLayerunnecessary, and therefore is marked as@Deprecated. This getter can stay in the API indefinitely for backwards-compatibility as it is not hard to maintain.Issues
Fixes #37256
Tests
I added the following tests:
object_test.dartthat test that layers are created and updated properly byPaintingContext.proxy_box_test.dartandcolor_filter_test.dartthat test that all render objects that useoldLayercreate and update layers correctly.repaint_boundary_test.dartthat test that the framework manages repaint boundaryOffsetLayercorrectly.rendering_tester.dartto fail test by default whenFlutterErrorcatches errors. Previously, a test would pass in the presence of these errors.Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]). This will ensure a smooth and quick review process.///).flutter analyze --flutter-repo) does not report any problems on my PR.Breaking Change
Does your PR require Flutter developers to manually update their apps to accommodate your change?
What broke?
Layer.addToScenenow returnsvoid. Previously it returnedEngineLayer. It is now the responsibility of theLayerimplementations to create and manage engine layers. For example, a container layer that pushes opacity previously would do this:After this change, it would change its signature to
voidand assign the layer using theengineLayersetter instead of returning it: