Enable ShaderMask support RasterCache#32027
Conversation
|
This pull request has been changed to a draft. The currently pending flutter-gold status will not be able to resolve until a new commit is pushed or the change is marked ready for review again. |
…6/engine into fix_shader_mask_raster_cache
flar
left a comment
There was a problem hiding this comment.
The proposed implementation breaks the existing behavior of how a shader mask is supposed to work as detailed in the comments. Mostly a saveLayer added in the child-caching case I think will fix it.
flow/layers/shader_mask_layer.cc
Outdated
| if (context.raster_cache->Draw(GetCacheableChild(), | ||
| *context.leaf_nodes_canvas, | ||
| cache_paint.paint())) { | ||
| DrawMask(context); |
There was a problem hiding this comment.
This code path is wrong because the DrawMask affects all of the area in the mask rect.
Consider if the mask rect is 100x100 and the children only draw a tiny rectangle in the middle of that, say 40,40->60,60.
If this method draws normally then we execute a saveLayer and the saveLayer creates a 100x100 temporary texture and initializes it to transparency. The children then fill in pixels for the middle 20x20 part of that temp texture. Then we ask the shader to paint over the entire 100x100 area, most of which is transparency. If the shader/blend_mode specified do not modify transparent pixels (something that might happen with a linear gradient transparency modulation at least for some of the pixels in the mask area - a common case) then we end up with a tiny 20x20 area that has been blended over with the shader surrounded by a field of transparent pixels many of which were left transparent. When the saveLayer ends and that temp texture is blended back onto the background, it will only change the pixels modified in the temp buffer.
However, if you render the children to the backdrop with no saveLayer then they are now mixed in to the background. Then when you draw the mask after that, you will end up modifying all of those pixels in the 100x100 area, including the background pixels and end up with potentially different results because:
(shader output blend_moded with transparent pixels in the saveLayer) over the background
is not the same as (note the change of parentheses):
shader output blend_moded with (transparent pixels in the saveLayer over the background)
Additionally another problem can occur. The saveLayer will clip the rendering. If a developer takes a huge sub-scene that covers 1000x1000 pixels and applies a ShaderMask to it with a mask rect of 100x100, you have nothing here to prevent the children from rendering all over the background when they should have been rendering into (and clipped to) a 100x100 playground instead.
So, in the end, you do need a saveLayer of sorts anyway. It may still make things faster by caching the children, but it should not be able to elide the saveLayer without a lot of fancy testing on the type of shader, the blend_mode and the content of the children that will swamp just using a saveLayer.
There was a problem hiding this comment.
Actually it looks like the bounds chosen for the saveLayer are the bounds of the layer which should be identical to the bounds of the children, so we would end up clipping the shader mask to the children rather than the children to the shader bounds...
There was a problem hiding this comment.
In either case, any part of the save layer not rendered by the children would be transparent pixels for the shader to modify before they are blended back into the background.
There was a problem hiding this comment.
We should create some specific test cases that cover this situation. I can imagine something along the lines of the following should show it:
Test 1, mask cannot affect background:
- background is cleared to a solid opaque color
- shader mask is run in a small rectangle with a blend mode of Clear
- result should be a solid screen of the opaque color because the Clear due to the shadermask will have only affected the saveLayer
The same thing could also be done with something like a gradient from opaque to transparent with a mode of DstIn. It should only have effect within the bounds of the mask rectangle and anywhere it is transparent should remain the background color.
Test 2, everything clipped to child bounds:
Then another test that has the children take up less room than the shader mask rectangle and the shader mask fill should be clipped to the bounds of the children due to the actions of the clipLayer.
There was a problem hiding this comment.
Hey @flar as the test case do you mine this:
Stack(
children: [
Positioned(
top: 100,
left: 150,
child: Container(
width: 100,
height: 100,
color: Colors.green,
),
),
Positioned(
top: 100,
left:170,
child: SizedBox(
width: 100,
height: 100,
child: ShaderMask(
blendMode: BlendMode.clear,
shaderCallback: (Rect bounds) {
return const RadialGradient(
center: Alignment.topLeft,
radius: 1.0,
colors: <Color>[Colors.yellow, Colors.red],
tileMode: TileMode.mirror,
).createShader(bounds);
},
child: Flex(
direction: Axis.horizontal,
children: [
Container(width: 50, height: 50, color: Colors.red,)
],
),
),
)
)
],
);
Has a green rect as the ShaderMask's background, and it has a small rect witch size is 50 * 50; ShaderMasker's blendMode is clear
There was a problem hiding this comment.
We should create some specific test cases that cover this situation. I can imagine something along the lines of the following should show it:
Test 1, mask cannot affect background:
- background is cleared to a solid opaque color
- shader mask is run in a small rectangle with a blend mode of Clear
- result should be a solid screen of the opaque color because the Clear due to the shadermask will have only affected the saveLayer
The same thing could also be done with something like a gradient from opaque to transparent with a mode of DstIn. It should only have effect within the bounds of the mask rectangle and anywhere it is transparent should remain the background color.
Test 2, everything clipped to child bounds:
Then another test that has the children take up less room than the shader mask rectangle and the shader mask fill should be clipped to the bounds of the children due to the actions of the clipLayer.
Very good advice!Indeed, cached children should be painted using saveLayer, we have fixed it and added the test, cc @JsouLiang.
There was a problem hiding this comment.
If I read that correctly, there is a green field at 100,150->200,250.
On top of that is a shadermask which should cover 100,170->200,270 so it is 20 "pixels" down from the green rect.
The shadermask child (to which it should be clipped) is at 100,170->150,220 so it covers half of the left side of the green rect. It is red, but that will be overwritten by the shadermask.
(I'm not familiar with how the Flex will affect that layout, though.)
The shadermask has an opaque linear gradient, but that will be ignored because it has a blend mode of kClear.
If the shadermask is clipped to the child then we end up with a green rectangle with an empty space covering the left half of its width from 20 pixels from the top to 30 pixels from the bottom. Did I read that right?
There was a problem hiding this comment.
yes, right. In this case the ShaderMask is bigger than his child. I think it may be the test case which you described above.
Add transform_layer and draw child RasterCache in child size
|
flutter/flutter#100957 Hey @flar , I have add a case to check the point which you mentioned before. |
|
In other layers (OpacityLayer, Image FilterLayer, ColorFilterLayer), if children are cached, the save layer in And in this PR, it looks like we can't avoid save layer by caching children. So should we still need to cache children so aggressively? |
Well, If don't cache the child layer, it would be easier to deal with. At present, we use memory to reduce time cost, although saveLayer is still used, but will with fewer draw instructions. Therefore, whether it's worth caching depends on the scenario. If the child layer is complex enough, it's worth caching, otherwise not necessary. Then, can we know what the complexity of child layer is in most scenarios? @flar do you have more experience about that? |
If a child layer is very complex (such as a complex display list layer), then it should have its own cache logic in it. If you look closely, you will find that the image filter layer caches itself only after it is accessed 3 times, but it always caches child very aggressively, because it knows that the cost of caching child is similar to save layer, and it may be reused in the next frame, so it is so aggressive about caching the child. So I suggest that if the save layer cannot be avoided, then we better not cache the child. @flar WDYT? |
flar
left a comment
There was a problem hiding this comment.
I didn't see the new test case, but there is an error here in the way paints are applied to the cached copy of the children and a conflict with a recently landed PR that needs to be corrected for.
ColdPaleLight
left a comment
There was a problem hiding this comment.
flagging this as "request changes" to figure out if the child should be cached so aggressively since save layer cannot be avoided
|
The problem with the aggressive caching of the child if the ShaderMask is stable then I think we're good, but we don't necessarily have a good handle on whether or not it was complex enough to cache. A ShaderMask with a simple child may not save us much in the long run. The reason it was OK for some of the other layers is that we had to do a saveLayer for the children anyway so the worst case if the children were simple was the memory cost of retaining the texture that would have been their "temp save layer texture". But, that "worst case" has a drawback if the children are changing. Since we evict the cache entries from the previous frame at the end of this frame, we may have last frame's children and this frame's children both cached at the same time while we are processing this frame - meaning we are actually taking twice the memory we need. And now ShaderMask could take up to triple the necessary memory. Without a complexity metric or better predictive logic to know if the children are stable it may be that the standard caching logic doesn't really work for ShaderMask. |
The children are stable in most cases, but for general considerations, we need to consider the "worst case". Therefore, to be cautious, your suggestion also is that we don't necessarily to cache child layer anymore? Do you think it's OK to roll back to the first commit bc4bb7c of this PR? |
0ede59f to
2692448
Compare
6a6cfad to
5ab8d8f
Compare
1742748 to
0750c58
Compare
ColdPaleLight
left a comment
There was a problem hiding this comment.
This LGTM once it LGT @flar
Pre-launch Checklist
writing and running engine tests.
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.