Bug 71733

Summary: Repaint broken when children of filtered SVG elements are updated
Product: WebKit Reporter: Joel Webber <jgw>
Component: SVGAssignee: Nobody <webkit-unassigned>
Status: RESOLVED FIXED    
Severity: Normal CC: jchaffraix, rhodovan.u-szeged, rniwa, simon.fraser, thorton, webkit.review.bot, zimmermann
Priority: P2    
Version: 528+ (Nightly build)   
Hardware: All   
OS: All   
Attachments:
Description Flags
Missing example
none
Patch
none
Patch
none
Patch
none
Added the requested '.' none

Description Joel Webber 2011-11-07 14:20:23 PST
When a parent (e.g., <g>) SVG element has a filter applied, and one of its children is updated (in such a way that only a sub-rectangle of the outer element would normally need to be repainted), webkit re-renders the element incorrectly. You can see this in the attached HTML file -- both squares should be green, but the second usually turns up red, because the element doesn't repaint correctly when the child's fill color is updated (on a 1ms timer).

This bug manifests in several different ways -- the attached example is the simplest reproduction I could get. Generally speaking, what's happening is as follows:
- On the first paint, the SVG filter code allocates an ImageBuffer, paints the results of the filter into it, then caches it.
  - Subsequent repaints just use the cached ImageBuffer.
- When you update the child, it kicks off a repaint for the bounds of the child's RenderObject.
- When that paint comes back down the pipeline, the <g> with the filter applied allocates a new ImageBuffer.
- The PaintInfo specifies that only the child's bounds needs to be repainted.
- The filter's *new* ImageBuffer is only partially painted.
- This mostly works, because the part of the filter's ImageBuffer that overlaps the paint rectangle is copied to the screen.
- But the *next* time we repaint the <g> from this ImageBuffer cache, it only contains a subset of the pixels it needs.
- This causes seemingly-random parts of the <g> to get blown away visually.
Comment 1 Julien Chaffraix 2011-11-08 10:27:32 PST
(In reply to comment #0)
> You can see this in the attached HTML file -- both squares should be green, but the second usually turns up red, because the element doesn't repaint correctly when the child's fill color is updated (on a 1ms timer).

Joel, it looks like the attachment did not make it. Could you upload it (again)?
Comment 2 Joel Webber 2011-11-09 07:28:42 PST
Created attachment 114273 [details]
Missing example
Comment 3 Joel Webber 2011-11-09 07:33:41 PST
Created attachment 114275 [details]
Patch
Comment 4 Joel Webber 2011-11-09 07:37:50 PST
My proposed fix for this (in the attached patch) is to modify RenderSVG[Container Root] such that they needsLayout() whenever any of their children needsLayout(). This fixes the immediate problem because the filter's ImageBuffer cache always gets fully repainted whenever a child is modified.

I also believe this will fix other probably-as-yet-unreported bugs with SVG filters, because repainting only the child's bounds is insufficient for filters with kernel sizes > 1px.

I'm not attached to the approach, but I think it's pretty straightforward -- any suggestions on the details are very much welcome.
Comment 5 Joel Webber 2011-11-09 07:43:51 PST
A couple of notes on the test case:
- This test only fails intermittently on trunk. If you load the attached HTML file, you'll see that it occasionally renders correctly (both squares green), but most of the time you'll see that the bottom-right square is red. It always works correctly with the patch applied.
- I have another test that more directly models the bug, by changing a child of the <g> element, which causes the rest of the cached ImageBuffer to get blown away (as in the bug description). But for the life of me I can't get it to fail when running in test_shell or as a layout test. I believe this has something to do with differences between the painting logic in test_shell and chrome/safari.
Comment 6 Nikolas Zimmermann 2011-11-10 08:45:37 PST
Comment on attachment 114275 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=114275&action=review

First round of review, thanks for tackling this!

> Source/WebCore/rendering/svg/RenderSVGContainer.cpp:57
> +    SVGRenderSupport::setNeedsLayoutForFilteredContainer(this);

I'd rename this to "setContainerNeedsLayoutIfNeeded".

> Source/WebCore/rendering/svg/SVGRenderSupport.cpp:310
> +    if (object->normalChildNeedsLayout()) {

Use early return here.

> Source/WebCore/rendering/svg/SVGRenderSupport.cpp:312
> +        if (resources && resources->filter())

And here, for any of the cases.

> Source/WebCore/rendering/svg/SVGRenderSupport.cpp:313
> +            object->setNeedsLayout(true);

This is dangerous, you're mutating the setNeedsLayout state of all containing blocks as well, are you sure you want that? Doing that from within layout() that looks suspicious.
I'd propose to amend RenderSVGContainers way of laying out the children, where we currently do:
    SVGRenderSupport::layoutChildren(this, selfNeedsLayout());

This could just read layoutChildren(this, selfNeedsLayout() || filtersForceContainerLayout())
where filtersForceContainerLayout() contains your object->normalChildNeedsLayout() && resources->filter() check.
Sounds better, eh?

> LayoutTests/svg/repaint/filter-child-repaint.svg:13
> +      <feOffset in="SourceGraphic" dx="0" dy="0" result="topCopy"/>
> +      <feGaussianBlur in="SourceAlpha" stdDeviation="2" result="shadow"/>
> +      <feOffset in="shadow" dx="3" dy="3" result="movedShadow"/>
> +      <feMerge>
> +        <feMergeNode in="movedShadow"/>
> +        <feMergeNode in="topCopy"/>
> +      </feMerge>
> +    </filter>

You could use the new feDropShadow primitive, would simplify the test.

> LayoutTests/svg/repaint/filter-child-repaint.svg:15
> +

Can you add a <title> section, explaining when this tests passes?
Currently its hard to tell, without tracing in mind :-)

> LayoutTests/svg/repaint/filter-child-repaint.svg:16
> +  <g filter='url(#dropShadow)' width='128px' height='128px'>

width/height are useless on <g>, you can remove it.

> LayoutTests/svg/repaint/filter-child-repaint.svg:18
> +    <rect width="64" height="64" style="fill:rgb(0,128,0)"/>
> +    <rect id='poke' x='32' y='32' width="64" height="64" style="fill:rgb(128,0,0)"/>

Also you could just use fill="green" here.
Comment 7 Joel Webber 2011-11-10 10:18:46 PST
Comment on attachment 114275 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=114275&action=review

>> Source/WebCore/rendering/svg/RenderSVGContainer.cpp:57

> 
> I'd rename this to "setContainerNeedsLayoutIfNeeded".

Done.

>> Source/WebCore/rendering/svg/SVGRenderSupport.cpp:310
>> +    if (object->normalChildNeedsLayout()) {
> 
> Use early return here.

Done.

>> Source/WebCore/rendering/svg/SVGRenderSupport.cpp:312
>> +        if (resources && resources->filter())
> 
> And here, for any of the cases.

Done.

>> Source/WebCore/rendering/svg/SVGRenderSupport.cpp:313
>> +            object->setNeedsLayout(true);
> 
> This is dangerous, you're mutating the setNeedsLayout state of all containing blocks as well, are you sure you want that? Doing that from within layout() that looks suspicious.
> I'd propose to amend RenderSVGContainers way of laying out the children, where we currently do:
>     SVGRenderSupport::layoutChildren(this, selfNeedsLayout());
> 
> This could just read layoutChildren(this, selfNeedsLayout() || filtersForceContainerLayout())
> where filtersForceContainerLayout() contains your object->normalChildNeedsLayout() && resources->filter() check.
> Sounds better, eh?

Thanks for clarifying that -- it felt a bit odd to be flipping that bit here, and I failed to notice the hierarchical behavior of calling setNeedsLayout(). I've refactored this code as you suggest, and it does indeed feel better.

>> LayoutTests/svg/repaint/filter-child-repaint.svg:13
>> +    </filter>
> 
> You could use the new feDropShadow primitive, would simplify the test.

Done, thanks. That one wasn't in the SVG 1.1 docs, but I see it in WebKit now.
(This awkward implementation, btw, came from a chromium bug I was investigating)

>> LayoutTests/svg/repaint/filter-child-repaint.svg:15
>> +
> 
> Can you add a <title> section, explaining when this tests passes?
> Currently its hard to tell, without tracing in mind :-)

Done. It's a bit hard to tell what the norms are from existing tests :)

>> LayoutTests/svg/repaint/filter-child-repaint.svg:16
>> +  <g filter='url(#dropShadow)' width='128px' height='128px'>
> 
> width/height are useless on <g>, you can remove it.

Done.

>> LayoutTests/svg/repaint/filter-child-repaint.svg:18
>> +    <rect id='poke' x='32' y='32' width="64" height="64" style="fill:rgb(128,0,0)"/>
> 
> Also you could just use fill="green" here.

Done (old habits die hard...).
Comment 8 Joel Webber 2011-11-10 10:31:56 PST
Created attachment 114526 [details]
Patch
Comment 9 Nikolas Zimmermann 2011-11-10 13:55:25 PST
Comment on attachment 114526 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=114526&action=review

Looks good, r=me!

> Source/WebCore/rendering/svg/SVGRenderSupport.h:58
> +    // Determines whether a container needs to be laid out because it's filtered and a child is being laid out

Missed period.
Comment 10 Joel Webber 2011-11-10 14:08:59 PST
Created attachment 114566 [details]
Patch
Comment 11 Joel Webber 2011-11-10 14:10:05 PST
Please ignore that last patch. There's commit-log diff garbage in there.
Comment 12 Joel Webber 2011-11-10 14:14:19 PST
Created attachment 114569 [details]
Added the requested '.'
Comment 13 Joel Webber 2011-11-10 14:19:12 PST
Many thanks for the prompt review, Nikolas. Looks like I've managed to reset the review state by adding that trivial period to the comment and re-uploading. Whenever you have a free moment to R+ it, it would be greatly appreciated.
Comment 14 Nikolas Zimmermann 2011-11-11 07:18:10 PST
Comment on attachment 114569 [details]
Added the requested '.'

Thanks!
Comment 15 WebKit Review Bot 2011-11-11 15:41:13 PST
Comment on attachment 114569 [details]
Added the requested '.'

Clearing flags on attachment: 114569

Committed r100036: <http://trac.webkit.org/changeset/100036>
Comment 16 WebKit Review Bot 2011-11-11 15:41:19 PST
All reviewed patches have been landed.  Closing bug.
Comment 17 Ryosuke Niwa 2011-11-25 14:36:03 PST
The test added by this patch is failing on Mac (the second box is red).