65

I would like to have a better understanding of how the components of Android's (2D) Canvas drawing pipeline fit together.

For example, how do XferMode, Shader, MaskFilter and ColorFilter interact? The reference docs for these classes are pretty sparse and the docs for Canvas and Paint don't really add any useful explanation.

It's also not entirely clear to me how drawing operations that have intrinsic colors (eg: drawBitmap, versus the "vector" primitives like drawRect) fit into all of this -- do they always ignore the Paint's color and use use their intrinsic color instead?

I was also surprised by the fact that one can do something like this:

Paint eraser = new Paint();
eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawOval(rectF, eraser);

This erases an oval. Before I noticed this my mental-model was that drawing to a canvas (conceptually) draws to a separate "layer" and then that layer is composed with the Canvas's Bitmap using the Paint's transfer mode. If it were as simple as that then the above code would erase the entire Bitmap (within the clipping region) as CLEAR always sets the color (and alpha) to 0 regardless of the source's alpha. So this implies that there's an additional sort of masking going on to constrain the erasing to an oval.

I did find the API demos but each demo works "in a vacuum" and doesn't show how the thing it focusses on (eg: XferModes) interacts with other stuff (eg: ColorFilters).

With enough time and effort I could empirically figure out how these pieces relate or go decipher the source, but I'm hoping that someone else has already worked this out, or better yet that there's some actual documentation of the pipeline/drawing-model that I missed.

This question was inspired by seeing the code in this answer to another SO question.

Update

While looking around for some documentation it occurred to me that since much the stuff I'm interested in here seems to be a pretty thin veneer on top of skia, maybe there's some skia documentation that would be helpful. The best thing I could find is the documentation for SkPaint which says:

There are 6 types of effects that can be assigned to a paint:

  • SkPathEffect - modifications to the geometry (path) before it generates an alpha mask (e.g. dashing)
  • SkRasterizer - composing custom mask layers (e.g. shadows)
  • SkMaskFilter - modifications to the alpha mask before it is colorized and drawn (e.g. blur, emboss)
  • SkShader - e.g. gradients (linear, radial, sweep), bitmap patterns (clamp, repeat, mirror)
  • SkColorFilter - modify the source color(s) before applying the xfermode (e.g. color matrix)
  • SkXfermode - e.g. porter-duff transfermodes, blend modes

It isn't stated explicitly, but I'm guessing that the order of the effects here is the order they appear in the pipeline.

Community
  • 1
  • 1
Laurence Gonsalves
  • 137,896
  • 35
  • 246
  • 299
  • They are best understood as mathematical functions taking (alpha, color) tuples from 1 or 2 source images, and outputting one tuple. http://hi-android.info/doc/android/graphics/PorterDuff.Mode.html (Not posting as an answer because I have never used Skia; just based on web search results) – rwong Apr 30 '11 at 05:14
  • @rwong: That's a fine explanation of PorterDuff.Mode, but it doesn't really explain the overall pipeline. See the part of my question about PorterDuff.Mode.CLEAR and drawOval, for example. CLEAR is defined as always outputting 0 to both alpha and the color channels regardless of the inputs, so applying CLEAR should wipe the entire destination unless there is some further masking restricting the application of the mathematical function `clear(src, dst) = [0,0]`. – Laurence Gonsalves Apr 30 '11 at 07:30
  • why are you assuming that there's extra masking? The oval itself *is* the mask. The software renderer does not bother blending pixels outside of the shape you are trying to draw. Your explanation would only work with a bitmap. – Romain Guy May 01 '11 at 18:09

3 Answers3

51

Like Romain Guy said, "This question is difficult to answer on StackOverflow". There wasn't really any complete documentation, and complete documentation would be kind of large to include here.

I ended up reading through the source and doing a bunch of experiments. I took notes along the way, and ended up turning them into a document which you can see here:

as well as this diagram:

enter image description here

It's "unofficial", obviously, so the normal caveats apply.

Based on the above, here are answers to some of the "sub-questions":

It's also not entirely clear to me how drawing operations that have intrinsic colors (eg: drawBitmap, versus the "vector" primitives like drawRect) fit into all of this -- do they always ignore the Paint's color and use use their intrinsic color instead?

The "source colors" come from the Shader. In drawBitmap the Shader is temporarily replaced by a BitmapShader if a non-ALPHA_8 Bitmap is used. In other cases, if no Shader is specified a Shader that just generates a solid color, the Paint's color, is used.

I was also surprised by the fact that one can do something like this:

Paint eraser = new Paint();
eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawOval(rectF, eraser);

This erases an oval. Before I noticed this my mental-model was that drawing to a canvas (conceptually) draws to a separate "layer" and then that layer is composed with the Canvas's Bitmap using the Paint's transfer mode. If it were as simple as that then the above code would erase the entire Bitmap (within the clipping region) as CLEAR always sets the color (and alpha) to 0 regardless of the source's alpha. So this implies that there's an additional sort of masking going on to constrain the erasing to an oval.

The XferMode applies to the "source colors" (from the Shader) and the "destination colors" (from the Canvas's Bitmap). The result is then blended with the destination using the mask computed in Rasterization. See the Transfer phase in the above document for more details.

Laurence Gonsalves
  • 137,896
  • 35
  • 246
  • 299
  • `Error 403 - Forbidden You tried to access a document for which you don't have privileges.` on your link – Aleadam May 09 '11 at 22:03
  • @Aleadam Oops! Thanks. Should be fixed now. – Laurence Gonsalves May 09 '11 at 22:34
  • @Laurence nice work! It will take a long time for me to go through all that info. – Aleadam May 09 '11 at 23:08
  • The shading part of your diagram is a bit confusing. It seems like the Shader would generate an image that is then processed through the ColorFilter before being sent to the Xfermode. This is not what happens when when your Shader is a linear gradient for instance (no "image" is generated.) Your explanation on your website makes it much clearer (a Shader is a function f(x,y) that returns a color.) – Romain Guy May 13 '11 at 07:17
  • You mention two possible bugs on your website. The first one is not a bug, this is a guaranteed behavior. The second one is indeed intriguing and I would say it's a bug. I would have to check with the original author of Skia to make sure. – Romain Guy May 13 '11 at 07:27
  • @Romain Yeah, I know the Shader doesn't actually materialize an image in the general case. The shading function is invoked many times, which kind of breaks the pipeline metaphor unless you say that the Shading phase's result is a function. I thought first-class functions might be a bit much for the high level description (and the diagram) so I referred to the `(x,y) → (α,r,g,b)` function as an image (albeit a "virtual" one). I'm not entirely happy with the way that part is written, so I will probably go back and rewrite it and I'll definitely take your feedback into account. – Laurence Gonsalves May 13 '11 at 22:26
  • @Romain Thanks for the info on those two things. I was less sure about the Bitmap one. The way the code was structured it did seem less likely to be a mistake (while with the Rasterizer+MaskFilter code I believe the behavior was just the result of a certain function call being in the wrong place). That said, the Bitmap behavior is strangely inconsistent. An ARGB Bitmap doesn't use its alpha channel until Shading, but an ALPHA_8 Bitmap uses it in Rasterization. I would have expected that anything possible with an ALPHA_8 Bitmap would be possible with the right ARGB Bitmap, but it is not so. – Laurence Gonsalves May 13 '11 at 22:39
12

This question is difficult to answer on StackOverflow. Before I get started however, note that shapes (drawRect() for instance) do NOT have an intrinsic color. The color information always comes from the Paint object.

This erases an oval. Before I noticed this my mental-model was that drawing to a canvas (conceptually) draws to a separate "layer" and then that layer is composed with the Canvas's Bitmap using the Paint's transfer mode. If it were as simple as that then the above code would erase the entire Bitmap (within the clipping region) as CLEAR always sets the color (and alpha) to 0 regardless of the source's alpha. So this implies that there's an additional sort of masking going on to constrain the erasing to an oval.

Your model is a bit off. The oval is not drawn into a separate layer (unless you call Canvas.saveLayer()), it is drawn directly onto the Canvas' backing bitmap. The Paint's transfer mode is applied to every pixel drawn by the primitive. In this case, only the result of the rasterization of an oval affects the Bitmap. There's no special masking going on, the oval itself is the mask.

Anyhow, here is a simplified view of the pipeline:

  1. Primitive (rect, oval, path, etc.)
  2. PathEffect
  3. Rasterization
  4. MaskFilter
  5. Color/Shader/ColorFilter
  6. Xfermode

(I just saw your update and yes, what you found describes the stages of the pipeline in order.)

The pipeline becomes just a little bit more complicated when using layers (Canvas.saveLayer()), as the pipeline doubles. You first go through the pipeline to render your primitive(s) inside an offscreen bitmap (the layer), and the offscreen bitmap is then applied to the Canvas by going through the pipeline.

Romain Guy
  • 97,993
  • 18
  • 219
  • 200
  • 1
    Then there's the relationship between the 2d and 3d pipelines in Android 3. – Ed Burnette May 01 '11 at 18:41
  • Thanks. I think you missed the word "versus". I was saying drawBitmap has an intrinsic color, and was contrasting this with "vector" primitives like drawRect, that do not. Would it be correct to say that the Paint's color (or Shader, if one is set) is used for primitives that have no color, but for bitmaps the colors come from the bitmap itself? And are ColorFilters applied in all three cases (Color, Shader, and Bitmap)? – Laurence Gonsalves May 01 '11 at 22:55
  • Regarding the masking of ovals, I said "conceptually". I know there isn't a temporary bitmap (unless saveLayer is used). I'm still not entirely clear on the masking thing. You say "the oval itself is the mask". If a BlurMaskFilter is applied then areas outside of the oval are affected. MaskFilter's documentation says it "perform[s] transformations on an alpha-channel mask". So does that mean that the mask for a drawing operation is equal to the set of pixels that have a non-0 alpha, or is there a mask separate from the alpha channel which is manipulated by MaskFilters? – Laurence Gonsalves May 01 '11 at 22:59
  • In my earlier comment I meant "primitives that have no intrinsic color", where I said "primitives that have no color". – Laurence Gonsalves May 01 '11 at 23:17
  • @Laurence For bitmaps the colors come from the bitmap itself, unless it's an alpha mask (A8 and A1 configs.) So for bitmaps, only the ColorFilter applies, not the Shader, nor the Paint's color (although the alpha component of the Paint's color does apply.) – Romain Guy May 02 '11 at 00:41
  • @Ed It doesn't matter, the hw accelerated pipeline is the same as the software one, it just rasterizes/renders using OpenGL instead of software. The way shader, color filters, etc. are applied remains the same. – Romain Guy May 02 '11 at 00:43
  • @Laurence there's is no "mask", the rasterizer generates pixels and only the generated pixels are blended. If you apply a MaskFilter, it only changes the output of the rasterizer. A MaskFilter applies a transform on the alpha channel of every pixel generated by the rasterizer in the first pass. For instance, when you draw an oval, the MaskFilter applies only to the oval shape, but if you draw a Bitmap with transparent pixels, the MaskFilter applies to these transparent pixels as well. – Romain Guy May 02 '11 at 03:55
  • What you're calling "the [set of] generated pixels" is what I'm calling the "mask". They're isomorphic. I'm not sure what you mean by "applies a transform on the alpha channel of every pixel generated by the rasterizer". Does this mean the set of pixels coming out of the Rasterizer is the same set of pixels coming out of the MaskFilter, but the alpha values are different when they come out of the MaskFilter, or can the MaskFilter actually change the set of generated pixels? – Laurence Gonsalves May 02 '11 at 06:00
  • No, I'm saying that the MaskFilter takes the result of the rasterizer in input and outputs another set of pixel. A BlurMaskFilter for instance blurs the rasterized set of pixels, but only on the alpha channel. It can therefore output more pixels. – Romain Guy May 02 '11 at 08:33
  • I actually went and looked through the code, and also did a bunch of experiments. See my answer on this question. I'm confused about your comments regarding masks, since the code itself actually does talk about a mask, and even uses the type `SkMask` to represent it. I also discovered that the `MaskFilter` only takes the result of the built-in rasterization (ie: scan conversion of the `Path`) and does not seem to apply to the result of a `Rasterizer` if one is present. This seems like a bug... do you know if this behavior was intentional? – Laurence Gonsalves May 12 '11 at 18:35
  • When I talk about masks and rasterizers I am not referring to Skia's C++ classes, and never said I was. Your confusion probably comes from this assumption. – Romain Guy May 13 '11 at 07:15
0

SkPathEffect - modifications to the geometry (path) before it generates an alpha mask (e.g. dashing) SkRasterizer - composing custom mask layers (e.g. shadows) SkMaskFilter - modifications to the alpha mask before it is colorized and drawn (e.g. blur) SkShader - e.g. gradients (linear, radial, sweep), bitmap patterns (clamp, repeat, mirror) SkColorFilter - modify the source color(s) before applying the xfermode (e.g. color matrix) SkXfermode - e.g. porter-duff transfermodes, blend modes

https://i.stack.imgur.com/lM0Zl.jpg