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

Enable ShaderMask support RasterCache#32027

Merged
fluttergithubbot merged 18 commits intoflutter:mainfrom
wangying3426:fix_shader_mask_raster_cache
Apr 12, 2022
Merged

Enable ShaderMask support RasterCache#32027
fluttergithubbot merged 18 commits intoflutter:mainfrom
wangying3426:fix_shader_mask_raster_cache

Conversation

@wangying3426
Copy link
Contributor

@wangying3426 wangying3426 commented Mar 15, 2022

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See testing the engine for instructions on
    writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@wangying3426 wangying3426 changed the title Enable ShaderMask to support RasterCache Enable ShaderMask support RasterCache Mar 15, 2022
@wangying3426 wangying3426 marked this pull request as draft March 15, 2022 11:45
@flutter-dashboard
Copy link

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.

@wangying3426 wangying3426 marked this pull request as ready for review March 15, 2022 15:58
@ColdPaleLight ColdPaleLight requested a review from flar March 18, 2022 07:15
Copy link
Contributor

@flar flar left a comment

Choose a reason for hiding this comment

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

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.

if (context.raster_cache->Draw(GetCacheableChild(),
*context.leaf_nodes_canvas,
cache_paint.paint())) {
DrawMask(context);
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

@wangying3426 wangying3426 Mar 29, 2022

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, right. In this case the ShaderMask is bigger than his child. I think it may be the test case which you described above.

@ColdPaleLight ColdPaleLight requested review from dnfield and gw280 and removed request for ColdPaleLight March 28, 2022 02:52
@wangying3426 wangying3426 marked this pull request as draft March 29, 2022 03:02
@wangying3426 wangying3426 marked this pull request as ready for review March 29, 2022 06:18
@wangying3426 wangying3426 requested a review from flar March 29, 2022 06:58
@JsouLiang
Copy link
Contributor

flutter/flutter#100957 Hey @flar , I have add a case to check the point which you mentioned before.

@ColdPaleLight
Copy link
Member

ColdPaleLight commented Mar 30, 2022

In other layers (OpacityLayer, Image FilterLayer, ColorFilterLayer), if children are cached, the save layer in Paint will be avoided. So we are very aggressive with cache children.

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?

@wangying3426
Copy link
Contributor Author

wangying3426 commented Mar 30, 2022

In other layers (OpacityLayer, Image FilterLayer, ColorFilterLayer), if children are cached, the save layer in Paint will be avoided. So we are very aggressive with cache children.

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?

@ColdPaleLight
Copy link
Member

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?

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?

@gw280 gw280 removed their request for review March 31, 2022 19:39
Copy link
Contributor

@flar flar left a comment

Choose a reason for hiding this comment

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

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.

Copy link
Member

@ColdPaleLight ColdPaleLight left a comment

Choose a reason for hiding this comment

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

flagging this as "request changes" to figure out if the child should be cached so aggressively since save layer cannot be avoided

@flar
Copy link
Contributor

flar commented Apr 6, 2022

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.

@wangying3426
Copy link
Contributor Author

wangying3426 commented Apr 7, 2022

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?
For most scenarios, only caching ShaderMaskLayer still can improve performance.

@wangying3426 wangying3426 force-pushed the fix_shader_mask_raster_cache branch from 0ede59f to 2692448 Compare April 11, 2022 03:51
@wangying3426 wangying3426 force-pushed the fix_shader_mask_raster_cache branch from 6a6cfad to 5ab8d8f Compare April 11, 2022 11:50
@wangying3426 wangying3426 force-pushed the fix_shader_mask_raster_cache branch from 1742748 to 0750c58 Compare April 11, 2022 12:45
Copy link
Member

@ColdPaleLight ColdPaleLight left a comment

Choose a reason for hiding this comment

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

This LGTM once it LGT @flar

Copy link
Contributor

@flar flar left a comment

Choose a reason for hiding this comment

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

LGTM

@flar flar added the waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land. label Apr 12, 2022
@fluttergithubbot fluttergithubbot merged commit f0a2da9 into flutter:main Apr 12, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Apr 12, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants