Stroke Fidelity Puzzle

292 views
Skip to first unread message

corey....@gmail.com

unread,
Feb 11, 2022, 12:01:43 PM2/11/22
to skia-discuss
Can someone shed light on why the stroke of this rounded rect path appears "wobbly" ?

The path itself is composed of simple cubics on the corners.

Make sure you are testing/viewing on non-retina.

corey....@gmail.com

unread,
Feb 11, 2022, 12:05:45 PM2/11/22
to skia-discuss
Here is an example using latest Skia fiddle...  the "wobble" occurs on the GPU path (which is what we use).

corey....@gmail.com

unread,
Feb 11, 2022, 12:46:26 PM2/11/22
to skia-discuss
Lastly here is a more clear example - where our app is really suffering on GPU...  The path itself is a stroke path we've always used, we render it "filled".  Contrast cpu and gpu.  https://fiddle.skia.org/c/8b1ba877e0409522eff6b8f484dc2ba5

On Friday, February 11, 2022 at 12:01:43 PM UTC-5 corey....@gmail.com wrote:

Michael Ludwig

unread,
Feb 12, 2022, 3:25:15 PM2/12/22
to skia-discuss
Hi Corey,

There are a few different sources of the wobble that you're seeing in the fiddles on this post and your other message.

1. For https://fiddle.skia.org/c/7e42210ddb6675fe8729c0ba5b07daf5 - I see subtle linear artifacts on the corners, where the overall curve isn't smooth enough
2. For https://fiddle.skia.org/c/8b1ba877e0409522eff6b8f484dc2ba5 - I see a complete lack of anti-aliasing on the GPU backend
Good news is for #1, I found a bug in the path renderer that was chosen for that stroked rounded rectangle and the fix is here: https://skia-review.googlesource.com/c/skia/+/507926
Bad news for #2 is that we have so far been unable to reproduce it, but it is likely trying to use the same path renderer that's being selected in example #3. In this, Skia has a new path renderer that automatically allocates an offscreen MSAA surface to do batched path rendering into. It's sample count is controlled by GrContextOptions::fInternalMultisampleCount and defaults to 4.  MSAA 4 is generally suitable for mobile devices or retina screens.  Increasing it will improve visual quality but may reduce performance; that said, the new msaa path renderers often have improved batching and reduced CPU overhead so net performance can often be much higher than the non-MSAA anti-aliasing path renderers.

Currently, my best guess for #2 is that fiddle.skia.org or SwiftShader (which fiddle uses for server-side rendering) is trying to create an offscreen surface and instead of failing, it's getting a surface claiming to have 4 samples but actually only has 1, so we draw thinking it'll be anti-aliased and it appears fully aliased. Unfortunately, we haven't seen this on our own devices.  For very thin strokes and filled paths, msaa4 can sometimes look aliased if the device's pixels are pretty large, but if you zoom into the rasterized image you'll still see a few shades of gray around the edges.

Hopefully this helps and we will continue investigating as well. Let me know if the internal multisample count option is able to improve your visual quality. Otherwise, there are options to disable the newer path renderers if you so choose.

-Michael

corey....@gmail.com

unread,
Feb 14, 2022, 8:09:58 AM2/14/22
to skia-discuss
Thank you Michael.

Ok so to clarify:

- (3) is the closest example approximating the rendering quality issues we are seeing. The filled paths you see for the stroke are what our legacy tools have always generated).
- Our target platform is WebGL (so we build Skia WASM and leverage GPU rasterization).
- The issue is most easiest to note on *non-retina* displays. At least on Mac, you can use the simple RDM tool  (https://github.com/avibrazil) to switch resolutions if all you have is a retina display. As you point out this retina allows the result to appear much better.
-We use GrBackendRenderTarget for our offscreen surface and typically have used 0 for the sampleCount option to keep performance at an optimal,
(our content is highly dynamic). But I did try bumping to 4 and it doesn't seem to help anything. https://github.com/google/skia/blob/1f193df9b393d50da39570dab77a0bb5d28ec8ef/include/gpu/GrBackendSurface.h#L428

So are you saying that the quality of (3) for you on non-retina display looks acceptable?
(Unfortunately we have a high bar here as our tools are meant for vector designers and animators who have an eye for perfect AA).

Seems our only option is to return to the legacy path renderer? If so, is this a build option?
It would be great if it were a runtime option as we could choose our path renderer stack based on screen density.


corey....@gmail.com

unread,
Feb 14, 2022, 8:41:57 AM2/14/22
to skia-discuss
And I also tried 8 on the sampleCount to no avail.

Greg Daniel

unread,
Feb 14, 2022, 9:49:27 AM2/14/22
to skia-discuss
Hi Corey. How did you try 8 on the sampleCount? Could you also try bumping up this value to https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/include/gpu/GrContextOptions.h;l=236 to see if that has any effect?

--
You received this message because you are subscribed to the Google Groups "skia-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to skia-discuss...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/skia-discuss/de8e8ae4-2cef-4bb7-99b2-44834b710ee7n%40googlegroups.com.

corey....@gmail.com

unread,
Feb 14, 2022, 10:34:54 AM2/14/22
to skia-discuss
We don't use GrContextOptions, just GrBackendRenderTarget ... and bumping the sample count to 8 didn't seem to help.

corey....@gmail.com

unread,
Feb 14, 2022, 2:04:18 PM2/14/22
to skia-discuss
Michael, do you know when these latest path renderers that lean on MSAA landed?  Milestone wise?  

Thanks.

On Saturday, February 12, 2022 at 3:25:15 PM UTC-5 Michael Ludwig wrote:

Michael Ludwig

unread,
Feb 14, 2022, 4:07:54 PM2/14/22
to skia-discuss
The techniques have been in the works for a while, with early iterations landing in m80ish. That said, they were not enabled by default (so should never even be considered) until m89 (https://skia-review.googlesource.com/c/skia/+/345759).  This was at the lowest priority of our path renderers and was only applicable if the render target was created with MSAA.  Following the history of changes here, the key change landed in m94: https://skia-review.googlesource.com/c/skia/+/436860.  This provided a path renderer that automatically draws to an offscreen surface using an msaa sample count from GrContextOptions::fInternalMultisampleCount.  This seems to coincide with the regression range you're seeing where it looks fine on m93 and moving to m97 is causing the trouble.

A few things to note: if you are creating your webgl surfaces and then wrapping them in GrBackendRenderTarget, changing the sampleCount to 8 in the GrBERT constructor without also changing the sample count of the actual FBO you made will not do anything. It may actually hurt appearance because Skia will then choose algorithms targeting msaa and draw to a surface that does not in fact have msaa, making everything appear non-anti-aliased.

If you are creating an FBO with multiple samples before wrapping it in the GrBackendRenderTarget, Skia's path renderer selection will skip the atlasing path renderer that landed in m94 but will likely choose another path renderer relying on msaa (either one that uses the same technology as the atlas but draws directly to the screen, or one that creates a triangulation of the path). In my experience, a sample count of 8 is usually sufficient on a regular screen to be on par with the non-msaa path renderers Skia provides (16 definitely is, but that may not be as commonly available).

However, you likely don't want to actually create your main surface with MSAA, in which case you should be able to provide GrContextOptions when you call GrDirectContext::MakeGL(). This lets you update the internalMultisampleCount to something higher (to see if that's acceptable quality) or disable different techniques. For instance, if you set GrContextOptions::fInternalMultisampleCount = 0, we should not use the atlasing path renderer at all. This is assuming that you are using WebGL to get an OpenGL context and compiling Skia with WASM yourself.  If you're using CanvasKit, it looks like we haven't exposed an equivalent GrContextOptions yet for the webgl factories, so CanvasKit doesn't let you tweak the internal sample count from the default 4 samples.

corey....@gmail.com

unread,
Feb 15, 2022, 10:12:22 AM2/15/22
to skia-discuss
Michael,

So I injected a GrContextOptions with various values of fInternalMultisampleCount and '0' takes the prize for highest quality rendition of our 1px (thinnish) filled paths.  4 as you noted was the default and was super jaggy on the curves on non-retina, 8 was an improvement but had some bad artifacts in places (I can file if need be but probably just a fact of life with MSAA) and wasn't nearly as nice as 0.

So if we run with 0 and opt out of the MSAA based path renderers, are we going to be safe for the long term? I don't want to be inadvertently falling back to cpu or be using some code path that will have support dropped.  The higher quality/fidelity vector renditions and AA is  just super important for our users.  With performance in mind if we run with fInternalMultisampleCount = 0 are we taking a huge hit?

By the way our GrBackendRenderTarget's sampleCount was set to 0 this whole time and did not seem to affect the final rendition at all, beyond 
the GrContextOptions/fInternalMultisampleCount.

I think the whole AA and path renderer strategy could be abstracted and exposed as a higher level set of well-documented runtime dials/flags so
it will be easier for folks to fine tune for their needs... with Flutter for example leaning on CanvasKit at a minimum CanvasKit should have a fidelity dial.

Thank you very much for helping sort out the rough edges (so to speak).

-C

Michael Ludwig

unread,
Feb 16, 2022, 9:26:42 AM2/16/22
to skia-discuss
I am glad that setting it to 0 was able to address your quality concerns. If you can take a screenshot of the bad artifacts with non-retina 8 I would appreciate it. A lot of Skia's newer path renderers have focused on MSAA-oriented techniques for improved performance and because their visual quality is acceptable on mobile / retina displays, which are becoming increasingly common.  That being said, we will always need to have non-MSAA approaches available, and if we aren't using MSAA, it will generally have higher visual quality because coverage is calculated analytically.  While it's pretty safe to say non-MSAA will always be supported, the specific techniques used for coverage AA for specific types of paths and styles may be subject to change.  

At the moment we have four non-MSAA path renderers that can be used in various situations:
1. A software fallback that uses the CPU backend's conventional rasterization techniques to make an alpha mask and upload it to the GPU. This is robust and a great fallback, but definitely hurts performance; if everything you were to draw would use the software path renderer, you're probably better off using the CPU backend of Skia, but when you have mixed content like primitive shapes, images, and filter effects, the software path renderer won't be too bad used here and there.
2. An analytic renderer for convex filled paths and strokes that tessellates a path into vertices and then draws those as triangles on the GPU. This has some CPU overhead preparing the vertices but because we know the base curve was convex, it's still reasonably efficient.
3. An analytic renderer for arbitrary filled paths and strokes that fully triangulates the path and calculates all the winding fill rules per triangle, and then computes additional inset/outset vertices to provide the anti-aliasing along all the final edges.  This has high visual quality but a lot of CPU overhead and can suffer from numerical stability issues and is currently a source of technical debt that we'd like to alleviate.
4. A signed-distance field renderer where small paths are converted to an SDF and then can be re-rendered quickly at new transformations with anti-aliasing derived from the gradient of the SDF. This can look good and have high performance, although care has to be taken around the memory budget of the cached SDF textures, and not every path converts to an SDF losslessly so we prefer it for small paths.

The specific capabilities of these renderers, in terms of path type, style, and transformation matrix can be pretty complex so the easiest knob is the InternalMultisampleCount option to turn on and off the MSAA renderers that have priority.  We definitely should have a way to expose this in CanvasKit that is currently lacking. Internally we have some testing utility code that lets us enable/disable various renderers but it would not be as simple as a "fidelity" dial. I think something like that would give us a good way to accept a quality hint from the user while still giving us flexibility to adjust implementations internally over time. For instance, we may pursue compute-shader based non-MSAA path renderers, post-processing, or multi-pass algorithms to increase the quality of the MSAA renderers.

-M

PS - The sampleCount parameter to GrBackendRenderTarget is purely informative for Skia, it tells us what sample count the GPU surface was created with externally. If you change that value without changing how the GPU surface was allocated, Skia might think it's rendering with MSAA but the rendering will be non-MSAA and we won't know. If you were changing both the sampleCount passed to GrBRT and how created the WebGL surface and were not seeing any behavioral differences then that sounds like an issue with MSAA and WebGL that would be good to dig into further.

corey....@gmail.com

unread,
Feb 16, 2022, 11:08:17 AM2/16/22
to skia-discuss
Per your request, here are 3 captures on non-retina for the same path geometry. 
sample count 0, 4, 8:
NoMSAA.pngDefaultMSAA4x.png8xMSAA.png


Reply all
Reply to author
Forward
0 new messages