-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
Today platform views are composited based on two pieces of information available to the engine:
- The platform view's place in the layer tree.
- The value of
isVisiblein the RegisterViewFactory callback signature.
The engine then makes the rest of the decisions. These decisions can be always predictable, have unpredictable performance features, or lack correctness.
Always predictable decisions 👍
The isVisible is a clear unambiguous flag that allows the engine perform guaranteed optimizations with predictable visual outcomes. For example, if the developer set the flag to false and their platform view does not indeed render anything visible (e.g. PointerInterceptor and Link widgets), the behavior is always correct and the platform view incurs minimal performance overhead.
Unpredictable performance 👎
The minimum overlay optimization makes correct visual choices, but its performance is not predictable because the overlay count is sensitive to overlaps between pictures and platform views. Just 1px of overlap can cause a new overlay created. If the overlap exists for every platform view (e.g. they are added in an loop and share the same structure), overlays multiply. Overlaps can come and go during animations, which can create subtle performance cliffs in the middle of transitions.
Lack of correctness 👎
Flutter Web limits the number of overlays to avoid running out of memory. At some point it stops creating new overlays, and instead attempts to make the least wrong compositing decision.
Proposal 1: compositing mode
Both the minimum overlay optimization and overlay limit are effective at keeping performance reasonable, and these kinds of heuristics are the only option without any developer hints. However, if the developer can provide more information, the engine can do a better job.
Provide a set of compositing modes that would provide both predictable performance and correctness guarantees. Based on the observation of a few real-world apps, the following modes are good candidates:
- Proposed by @jezell in Move pictures from deleted canvases to second-to-last canvas instead of last. engine#51397 (comment): an "always on top" mode that would always put the platform view on top of all Flutter canvases. It guarantees to never incur an overlay, and it guarantees a specific compositing effect. It may or may not be the right choice for a particular use-case, but there's nothing to guess w.r.t. what it does.
- "always behind": mutually exclusive with "always on top"; places the platform view behind all Flutter canvases. Performance and correctness guarantees are also guaranteed.
- "mask": a
(shape, color filter)tuple (perhaps based onPathandMaskFilter) applied to layers composited behind the platform view. The key use-case is to "burn a hole" of a given shape through the canvas revealing the platform view that's composited behind it. The mask would be used in conjunction with the "always behind" mode so the engine places the platform view behind the "hole".
(NOTE: on the web, the implementation of alwaysBehind is likely to be the same as isInvisible, but each engine implementation is free to deviate as the two modes are semantically different)
Proposal 2: platform view group
Compositing mode is predictable and it eliminates the need for overlays entirely, but a bit all-or-nothing. Given that Firefox and Safari are struggling to gives an efficient way to composite HTML content amongst WebGL content, all-or-nothing may be the right choice for the time being. However, it may be too limiting in some situations, in particular in cases when the HTML content is partially transparent (e.g. SVG, PNGs and GIFs with alpha, or simply HTML without an opaque background).
One way to allow transparency in platform views but still keep the number of overlays under control is to allow grouping multiple platform views such that the group induces only one overlay. Consider an image gallery where every image is a CORS image, needing an <img> element, hence requiring a platform view. Each image may be wrapped inside a widget that also supplies a background, a shadow, and a label under the image. The structure of such UI may look like this:
<grid>
<card>
<background-color />
<shadow />
<img />
<label>
</card>
<card>
<background-color />
<shadow />
<img />
<label>
</card>
... many more cards ...
</grid>
Looking at this structure linearly, every image is surrounded by non-platform view content. Naively, we'd have to sandwich every image between overlays to paint background and foreground content. You'd need numberOfCards + 1 overlays to composite the UI. However, despite how pictures appear in the render tree to the engine, the developer may know in advance that the images will always show up on top of everything that's inside the <grid>. Knowing this would allow the engine eliminate the unnecessary overlays predictably without looking into the pictures.
One way to achieve this is to introduce a new kind of ContainerLayer. Let's call it PlatformViewGroup. The layer has only one property, compositionMode, which takes two values onTop and behind. Applying this layer to the UI above, it would roughly look like this:
<platform-view-group compositionMode="onTop">
<grid>
<card>
<background-color />
<shadow />
<img />
<label>
</card>
<card>
<background-color />
<shadow />
<img />
<label>
</card>
... many more cards ...
</grid>
</platform-view-group>
The engine would "pluck" the platform views from inside the platform view group, and composite it on top of everything else that's in the group, preserving any layer effects that must be preserved, a la:
<platform-view-group compositionMode="onTop">
<grid>
<card>
<background-color />
<shadow />
<label>
</card>
<card>
<background-color />
<shadow />
<label>
</card>
... many more cards ...
</grid>
<img />
<img />
... many more imgs ...
</platform-view-group>
This operation is identical to what the minimum overlay optimization does today, but instead of relying on looking at picture contents, it uses an explicit signal from the developer. This way the optimization would still work even when half a pixel of an nearly transparent pixel of a shadow overlaps with the platform view. The fast path is guaranteed.