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

Picture shader #8835

Closed
wants to merge 3 commits into from
Closed

Picture shader #8835

wants to merge 3 commits into from

Conversation

cmkweber
Copy link

@cmkweber cmkweber commented May 3, 2019

This PR is addressing:
#flutter/flutter#31338

It implements a new shader type: PictureShader. This is a shader that is constructed through Picture.toShader() which returns Future(PictureShader). The reason this does not follow the same construction as ImageShader (by passing in an image) is that PictureShader must be created on the GPU thread. If it was constructed through PictureShader(picture, ...) the actual shader would be blank until it is actually drawn by the GPU.

You can see in this Skia fiddle that an SkPictureShader cannot be allocated on the CPU path:
https://fiddle.skia.org/c/6090413aa2b53941221574f14f3fd383

If you comment out the beginning 'if' statement, the fiddle crashes...

Another thing to note is that the shader generation doesn't use SkPicture::makeShader. Unfortunately, the bulk of that function is copied here but without the use of SkResourceCache. If this shader is created every frame the SkResourceCache grows quickly and causes large collections by the time dart sees the shaders are no longer referenced. Even without the SkResourceCache, the collection of just these shaders is noticeable in Debug, in Release it is performant.

A better long-term solution may be to provide flutter the ability to use an SkSurface backed canvas instead of just a one time use Canvas with PictureRecorder.

cc @brianosman @fmalita

@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here (e.g. I signed it!) and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

1 similar comment
@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here (e.g. I signed it!) and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@cmkweber
Copy link
Author

cmkweber commented May 3, 2019 via email

@googlebot
Copy link

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

@googlebot googlebot added cla: yes and removed cla: no labels May 3, 2019
@googlebot
Copy link

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

@brianosman
Copy link
Contributor

Just wanted to clarify something: Picture shader does work on the CPU backend. The fiddle from the original comment has a subtle bug (it specifies that there are 6 indices, but only supplies three). Because the vertices are being used in-order, no indices are needed, so it can be simplified (and made safe) by removing them:

https://fiddle.skia.org/c/ef8fd833efa942bac5b0671c5b3cc273

@cbracken cbracken requested a review from liyuqian May 6, 2019 18:15
@cmkweber
Copy link
Author

Brian - great catch! When I changed it from drawing a square to a triangle I forgot to change the indices count along with the vertex count. I still believe the PictureShader will need to be created on the GPU thread since we are referencing a surface-backed Picture, but it is good to know the CPU path for PictureShader works...

@cbracken
Copy link
Member

@cmkweber do you plan to follow up with the simplifications @brianosman suggested?

/cc @Hixie for dart:ui API review.

@cmkweber
Copy link
Author

cmkweber commented May 20, 2019

@cbracken - The PictureShader needs to be on the GPU thread because it references a surface-backed Picture created on the GPU thread. @brianosman was correcting my earlier mistake that a bug might be preventing CPU PictureShaders, but it is my belief CPU PictureShaders aren't suitable here.

@@ -2669,6 +2670,13 @@ class ImageShader extends Shader {
super._();
}

/// A shader (as used by [Paint.shader]) that tiles a picture.
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

(notably, how to create a PictureShader should be documented here, as well as how to use it)

///
/// The shader is returned asynchronously to allow time for the gpu to
/// draw the picture and compile a shader.
Future<PictureShader> toShader(TileMode tmx, TileMode tmy, Float64List matrix4) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In general we prefer to avoid abbreviations, so tileModeX or some such.

In https://master-api.flutter.dev/flutter/painting/paintImage.html we use RepeatImage which doesn't expose mirror. Is your goal to eventually add mirror values to the RepeatImage enum?

@Hixie
Copy link
Contributor

Hixie commented May 20, 2019

Ah, I hadn't realized that this was a parallel to ImageShader.

We literally never use ImageShader. If it has a problem, it might be simpler for us just to remove it entirely than to add more API surface to try to improve it.

@cmkweber
Copy link
Author

@Hixie - I would agree ImageShader doesn't have many use cases, other than possibly tiling an image pattern. The reason in favor of PictureShader is that it is sometimes useful to draw to a canvas and use the result in further drawing, leaving everything on the GPU and avoiding the round-trip with ImageShader.

This functionality is critical for our app, to help draw a large amount of paths, we instead pre-triangulate our entire scene and draw it in 1 or 2 batches using drawTriangles. To be able to color paths individually, we then draw a simple texture atlas and use that as the paint shader in drawTriangles.

Since this is being done every frame, a more streamlined way would be to add surface backing functionality to Canvas. Essentially, a user could create a surface-backed canvas, draw to it, request a shader from it, etc. This would allow the user to reuse the same SkSurface/SkCanvas (implicitly) and just draw to it and request a shader each frame.

@Hixie
Copy link
Contributor

Hixie commented May 21, 2019

I think we should consider how to use ImageShader and PictureShader to reimplement paintImage, if it's more efficient than what paintImage does today. (Maybe just for some cases.)

At a very minimum, we should have extensive tests here to make sure that ImageShader and PictureShader actually work as we expect. Right now there's really nothing to stop us from regressing the behaviour and we'd never know because these codepaths don't even get run once in any of our automated testing as far as I can tell.

@cmkweber
Copy link
Author

@Hixie - paintImage seems to have more functionality that wouldn't be directly correlated with ImageShader, such as colorFilter and invertColors. They could conceivably be added by using a SkShaders::Blend. Also, the image slicing isn't supported with ImageShader. ImageShader would be more efficient because paintImage issues a number of draw calls to achieve tiling.

I agree that tests should be added to accommodate ImageShader and PictureShader. PictureShader is a proposal but as I've mentioned before, a better alternative is a surface-backed canvas that is long-lived which allows a shader to be requested after drawing operations, but that is a bigger API change...

@Hixie
Copy link
Contributor

Hixie commented May 21, 2019

Since we don't use ImageShader at all, I'm certainly open to larger API changes if you think they'd be better.

@cmkweber
Copy link
Author

@Hixie - What is your advice for how to approach the API changes? I have a few proposals below and would like some feedback.

I have distilled our rendering pipeline into a simple skia fiddle:
https://fiddle.skia.org/c/1e4b48c4a5729f5ad4770a8872f3cc67

The important thing to note is how a canvas uses the result of a previous canvas:

  • Canvas1 - Used to draw the texture atlas
  • Canvas2 - Uses Canvas1 as the shader for drawVertices
  • Canvas3 - Uses Canvas2 to redraw at half size to allow for supersampling

Here are a couple proposals to support this:

  1. Picture.toShader() - This is what is currently committed in this PR. A PictureShader is returned asynchronously through a call to Picture.toShader.

    Drawbacks:

    • Wasteful - Surface is recreated each frame.
    • Asynchronous - In my particular setup, the result would be two frames behind at least since the result from the previous canvas needs to be waited on.
  2. Canvas.fromSize(width, height) - A surface backed canvas created through another constructor. This would create a surface once and record commands into a SkPictureRecorder, when the user requests an Image through Canvas.toImage, it would playback the recorded commands into the surface canvas and return the image by snapshot, the user could then create an ImageShader with this image.

    Drawbacks:

    • Wasteful - Since an Image is returned, it would be retained by skia for awhile until dart releases it, causing large collections. This could be remedied by adding Canvas.toShader to Canvas though to discard the intermediate Image.
    • Subclass - Image is currently setup to work with bitmap backed SkImages, it may need to be subclassed to allow for a surface backed SkImage. There may also be issues because two threads would be referencing the surface backed SkImage.
    • Asynchronous - Same as PictureShader, Canvas.toImage and Canvas.toShader would have to create a GPU task and wait for it to complete before returning to Dart.
  3. PictureShader(picture) - Instead of this being created through Picture.toShader, this would be constructed through PictureShader(picture). When the SkPaint's are created on the GPU side, if it encounters one with a PictureShader it would compile the underlying SkShader at that time.

    Drawbacks:

    • Wasteful - Surface is recreated each frame.

My guess is Option 3 would be best because it would be synchronous (on the Dart side) and not have to create GPU tasks constantly and just plug into the current pipeline. I'm not familiar enough with the rendering code to know how to accomplish this though, somewhere on the GPU thread it would have to special case (if SkPaint.shader == PictureShader) and playback its contents before continuing. If this doesn't make sense I can try to explain it another way but I wanted to get your thoughts on which would be the best way to proceed.

@Hixie
Copy link
Contributor

Hixie commented May 30, 2019

I guess my first question would be: what problem are we trying to solve?

Maybe what we should do is start from an issue, with a description maybe like "Our app draws N paths each frame, and doing so takes Xms or device Y, which is unacceptably slow". Then we can talk more about exactly what effect you're trying to achieve, and see if there's a better way to solve that specific problem. For example, maybe we just need a "drawPaths" method that draws paths faster than we currently draw them when calling drawPath in a loop. Maybe you're using paths to draw a repeating pattern and the fact that it's paths isn't the issue, it's just that you don't have an efficient way to repeat the pattern. I don't know. It's hard to evaluate proposals without knowing what the problems are.

@cmkweber
Copy link
Author

cmkweber commented May 30, 2019

The initial issue is related to drawing many paths unacceptably slow. Our app is a coloring book app which can have anywhere from 100 to 500 paths per scene. Luckily, there is no animation to the paths, they don't change at all, just the canvas transform changes as the user pans and zooms. The only animation involves the paths changing colors.

Simple scenes were performant, but once we loaded in scenes with 100 or more shapes, with that many draw calls it struggled to achieve 30fps on the few devices available to me. I wrote a simple main.dart to test drawing 300 or more objects with various methods: drawPath with every path, drawVertices with every path, and drawVertices with all paths at once, and drawVertices in one call was the only one that was performant across devices. I can try and find this dart test and share it if necessary.

I think a drawPaths might be a good addition, but I'm not sure if it will be performant because it will still generate many draw calls (unless it unions the paths), and still need to triangulate. Let me know your thoughts, I can also provide some sample scenes if necessary.

Screenshots:
scene
draw vertices
atlas

Also - the reason for the supersampling is drawvertices doesn't support antialiasing, this isn't required but would be a nice to have...

@Hixie
Copy link
Contributor

Hixie commented May 30, 2019

Oh, yeah, you definitely don't want to have to do all the triangulation and atlassing and so on yourself. That seems like a lot of work for something that should just work well out of the box.

I recommend filing a bug (https://github.com/flutter/flutter/issues/new?labels=severe%3A+performance&template=performance.md) and we'll get the skia folks involved. If you have tests or sample code that we can look at specifically, that would be fantastic. Also, regarding drawVertices not doing antialiasing, please file a bug for that too.

@cmkweber
Copy link
Author

@Hixie - Luckily this triangulation and atlas creation is already done, we wrote an Adobe Illustrator export script to handle this. The only missing piece is being able to draw the texture atlas and use that result as a shader for drawVertices. I have spoke with some of the skia folks already and they appear to be on board, some of them are on this PR I believe.

As for drawVertices not doing anti-aliasing, there is a skia issue for that but my understanding is the solution is a long ways out for that.

I can create a new issue, but wouldn't adding drawPaths ultimately present the same problem? It will presumably take an SkPaint as an argument which would draw all the paths with the same SkPaint, where each path needs its own SkPaint.

@Hixie
Copy link
Contributor

Hixie commented Jun 3, 2019

You may have done it for your case but I'm more worried about the next ten people who hit this problem, no offense intended. :-)

@cmkweber
Copy link
Author

cmkweber commented Jun 3, 2019

@Hixie - no offense taken! What about the issue of painting individual paths, won't that issue still exist if drawPaths is added to canvas? Unless you are envisioning it also triangulates, creates an atlas, and uses ImageShader internally to accomplish this. I'm in favor of this, it would make our exporter much simpler, I'm worried about the development time for this to be added to Flutter. We are hoping to launch within the next 3-4 months and I could see something this substantial not being implemented for a long while, let alone it getting onto a stable channel so I could resume using continuous integration. The only thing really missing for us to accomplish what my fiddle is demonstrating is ImageShader using makeImageSnapshot but without rasterizing. It would be great if this could be accomplished synchronously, but we could manage without that.

@Hixie
Copy link
Contributor

Hixie commented Jun 3, 2019

What about the issue of painting individual paths, won't that issue still exist if drawPaths is added to canvas?

I don't know, that's why I think we should start from a more comprehensive description of the problem we're trying to solve, as discussed above.

We are hoping to launch within the next 3-4 months and I could see something this substantial not being implemented for a long while

What might make sense for you is to create your own engine and ship your app from that locally on the short term, and on the long term we can see what a more comprehensive solution might look like.

@reed-at-google
Copy link
Contributor

reed-at-google commented Jun 4, 2019 via email

@cmkweber
Copy link
Author

cmkweber commented Jun 4, 2019

@reed-at-google thanks for adding that - I agree there are numerous use cases here with RTT. Currently in flutter the only way to make an image snapshot is with Picture.toImage - but it goes all the way to CPU.

@Hixie - is there anything we could add in the intermediate while drawPaths is being fleshed out? Right now Image always references a CPU SkImage, but if functionality was added to allow it to reference a surface-backed GPU SkImage, it would provide a lot of versatility. I'd like to avoid (if possible) having to compile my own local engine and keep it in sync with latest flutter and be able to use continuous integration.

@cmkweber
Copy link
Author

cmkweber commented Jun 4, 2019

I have created a performance issue for the long-tail resolution to this:
flutter/flutter#33849

Would still be very interested in a short-term solution as proposed above...

@cbracken
Copy link
Member

@cmkweber @reed-at-google @Hixie it sounds like the plan is to not proceed with this, but instead to deal with this upstream in Skia. If so, should we close this PR?

@cmkweber
Copy link
Author

@cbracken - It seems like the plan is to close this, but it would be extremely helpful to my project for a short term fix for this since the upstream fix is probably a long way out. As @reed-at-google pointed out there are many uses for the ability to reference a surface backed image. If I just had a way to "snap-off" an image from the canvas and use it (with ImageShader) without going to CPU that would be immensely useful.

@Hixie
Copy link
Contributor

Hixie commented Jun 19, 2019

@cmkweber For the short term I would recommend building a custom engine with this feature. No need for us to ship the feature just for you. :-)

@cmkweber
Copy link
Author

@Hixie - It looks like that may be my only option. At the risk of being annoying at this point, I will try one more 'Hail Mary'! These are a number of open issues I think would benefit from this, so I don't think the issue would be just for me in fairness...

ImageFilter Widget
flutter/flutter#13489 (comment)

As you noted here ImageFilter can't be 60fps because the rasterizer returns images asynchronously.
ImageFilters when implemented would be more useful if it could be applied to graphics drawn by canvas. As it stands now, ImageFilter would only work on CPU loaded imagery, not widgets or canvas graphics.

Slow transform:
flutter/flutter#24627 (comment)

Your comment notes that RepaintBoundary.toImage is going from GPU to CPU and back to GPU.
While caching the first call would improve performance in this case, it essentially means
the content will remain static throughout transform animation. In many cases this may be
acceptible, but I can imagine cases where this isn't. Route transitions is one such case:

https://api.flutter.dev/flutter/widgets/ModalRoute/buildTransitions.html

Routes will stop animating and pause during screen transition if transform is used.

Saturation Blur:
flutter/flutter#29483 (comment)

The comment here about using a repaint boundary requires static content, since RepaintBoundary.toImage
is async, you would have to wait for the result, apply the saturation blur, then draw.

Draw repeat patterns:
flutter/flutter#14733 (comment)

Your suggestion here is to avoid the toImage call and use drawPicture multiple times.
This is non-performant because successive picture draws need to replay identical buffers.
This issue grows as the size of the PictureRecorder buffer grows and the amount of times the picture is drawn.

Texture Canvas:
flutter/flutter#20664

This user is essentially mirroring the entire Canvas API just to be able to use the drawn texture elsewhere.

@Hixie
Copy link
Contributor

Hixie commented Jun 20, 2019

#14733 would be addressed to some extent by this PR, but the others wouldn't.

@Hixie
Copy link
Contributor

Hixie commented Jun 20, 2019

To reiterate, the problem with this PR is that it extends an API that we're already not using, to solve a use case that we should solve at a higher level. I could see us getting behind this PR if it removed the existing unused ImageShader, reimplemented paintImage (in the framework) in a backwards-compatible fashion using this (thus actually showing that we will use this feature), and had a benchmark that showed that it was an actual improvement. (We'd also to address the comments that are already applicable to this PR, such as adding sample code and so on.) But if the goal is just to solve the stated problem, then it's not the right long-term solution, and we shouldn't take short-term solutions.

@Hixie
Copy link
Contributor

Hixie commented Jun 20, 2019

I spoke with @reed-at-google and @brianosman of the Skia team about this. They gave me more context and pointed out a few things that we could do that would be even better for you than this PR:

  • our current Scene.toImage and Picture.toImage calls involve readback from the GPU (because our original usecase was golden file testing); we should be able to improve that by providing a GPU-specific version.
  • our current Picture.toImage call is asynchronous but could be made synchronous using Skia's Picture-backed GPU Image construct.
  • Skia supports a Picture-backed shader that's even more efficient than the one used in this PR.

I am happy for us to do these, provided that as part of that work or before that work we first pay down the technical debt we have in this area:

  • we should make sure we have tests and documentation for ImageShader, drawVertices, and so on.
  • we should use these features where it makes sense in the framework (e.g. paintImage).

@Hixie
Copy link
Contributor

Hixie commented Jun 20, 2019

(We don't have an ETA for when to do this; we have a lot of higher priority work going on right now. We would accept PRs that move us in that direction if you are interested.)

@cmkweber
Copy link
Author

@Hixie - that's great news, I truly believe opening up toImage calls to return a usable Image on the current frame opens up myriad possibilities. Entire widget trees could be screenshotted and have ImageEffects applied, allowing things like animated saturation blurs, or animated convolution filters if they get added.
Need to be careful here though that some of these previous issues don't crop back up:
flutter/flutter#23621
flutter/flutter#21618

I think it is good if ImageShader is kept, otherwise the texture coordinates supplied to Vertices can't be used effectively.

I'll definitely help where I can, my trials with storing a surface backed image with toImage calls were unsuccessful since I ran into GrSingleOwner problems, so I'm not the best one to tackle that. But I can help write tests and documentation and rework paintImage to use ImageShader.

@cmkweber
Copy link
Author

Also - to add, the reason I didn't use PictureShader directly in this PR is because I was noticing SkResourceCache objects being held longer than expected. This may be alleviated now though with the merging of this issue: #9004

@cbracken
Copy link
Member

@cmkweber @Hixie It sounds like we should probably close out this PR for now and open any new PRs you'd like to contribute based on the discussion above. Thanks again for your contribution.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants