Reuse buffer allocation if canvas size does not change
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 23 Mar 2012 16:58:53 +0000 (16:58 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 23 Mar 2012 16:58:53 +0000 (16:58 +0000)
https://bugs.webkit.org/show_bug.cgi?id=80871

Patch by Sami Kyostila <skyostil@chromium.org> on 2012-03-23
Reviewed by Stephen White.

Source/WebCore:

If the user changes the width or height attributes of a canvas element,
the contents of the canvas should be cleared and the context state
should be reset. This has become a common idiom to clear the canvas
"efficiently" at the start of a frame.

Previously, this code path triggered a full reallocation of the image
buffer backing the canvas, leading to reduced performance. This patch
implements an optimization where we reuse the previous image buffer
allocation if the size of the canvas did not change. Also, instead of
clearing the canvas every time its dimensions are touched, we only clear
if anything has been drawn into the canvas since it was previously
cleared.

Note that for now this optimization only applies for 2D canvases,
since it is not entirely clear how touching the dimensions of a WebGL
canvas should work.

Test: fast/canvas/canvas-resize-after-paint-without-layout.html +
      existing layout tests for canvas resetting.

* html/HTMLCanvasElement.cpp:
(WebCore::HTMLCanvasElement::HTMLCanvasElement):
(WebCore::HTMLCanvasElement::reset):
(WebCore::HTMLCanvasElement::createImageBuffer): Save the initial
graphics context state so we can restore it without creating a new
context.
(WebCore::HTMLCanvasElement::clearImageBuffer):
(WebCore):
(WebCore::HTMLCanvasElement::clearCopiedImage):
* html/HTMLCanvasElement.h:
(HTMLCanvasElement):
* html/canvas/CanvasRenderingContext2D.cpp:
(WebCore::CanvasRenderingContext2D::reset): No need to notify the
compositor when the context is reset, because clearing the image buffer
does the same thing. We can also skip the notification if we did not
have an image buffer at the time of the reset, because the reset will
not have any visual impact in this case. Finally, if the canvas size
did change, the notification is also unnecessary because of the call
to RenderObject::repaint() from HTMLCanvasElement::reset().

LayoutTests:

Add layout test to check canvas resizing without changing its layout size.

We also update the expected image one canvas clearing test. The test
is setting the size of a canvas and expecting it to be cleared in the process.
With the optimization to retain the underlying ImageBuffer, we no longer call
RenderReplaced::repaint() as a part of this process. This function used to
repaint both the canvas itself (100x50) as well as its local selection
rectangle (100x54).

In this case the local selection rectangle is larger than the canvas because
the canvas is contained within an anonymous RenderBlock that also has two empty
text nodes. The extra 4 pixels are needed for drawing the selection rectangle
around any descenders in the the text of those nodes.

Since clearing the canvas has no effect on the selection rectangle, we only
need to repaint the area of the canvas itself.

* fast/canvas/canvas-resize-after-paint-without-layout.html: Added.
* fast/canvas/canvas-resize-after-paint-without-layout-expected.txt: Added.
* platform/chromium-linux/fast/canvas/canvas-resize-after-paint-without-layout-expected.png: Added.
* platform/chromium-linux/fast/canvas/setWidthResetAfterForcedRender-expected.png: Updated.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@111872 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/fast/canvas/canvas-resize-after-paint-without-layout-expected.txt [new file with mode: 0644]
LayoutTests/fast/canvas/canvas-resize-after-paint-without-layout.html [new file with mode: 0644]
LayoutTests/platform/chromium-linux/fast/canvas/canvas-resize-after-paint-without-layout-expected.png [new file with mode: 0644]
LayoutTests/platform/chromium-linux/fast/canvas/setWidthResetAfterForcedRender-expected.png
LayoutTests/platform/chromium-linux/platform/chromium/virtual/gpu/fast/canvas/canvas-resize-after-paint-without-layout-expected.png [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/html/HTMLCanvasElement.cpp
Source/WebCore/html/HTMLCanvasElement.h
Source/WebCore/html/canvas/CanvasRenderingContext2D.cpp

index 840c494396af903fad09dc3d305dd759021b1b5b..49cd4337bbc0c130d3575e1d1769313df8bb131f 100644 (file)
@@ -1,3 +1,32 @@
+2012-03-23  Sami Kyostila  <skyostil@chromium.org>
+
+        Reuse buffer allocation if canvas size does not change
+        https://bugs.webkit.org/show_bug.cgi?id=80871
+
+        Reviewed by Stephen White.
+
+        Add layout test to check canvas resizing without changing its layout size.
+
+        We also update the expected image one canvas clearing test. The test
+        is setting the size of a canvas and expecting it to be cleared in the process.
+        With the optimization to retain the underlying ImageBuffer, we no longer call
+        RenderReplaced::repaint() as a part of this process. This function used to
+        repaint both the canvas itself (100x50) as well as its local selection
+        rectangle (100x54).
+
+        In this case the local selection rectangle is larger than the canvas because
+        the canvas is contained within an anonymous RenderBlock that also has two empty
+        text nodes. The extra 4 pixels are needed for drawing the selection rectangle
+        around any descenders in the the text of those nodes.
+
+        Since clearing the canvas has no effect on the selection rectangle, we only
+        need to repaint the area of the canvas itself.
+
+        * fast/canvas/canvas-resize-after-paint-without-layout.html: Added.
+        * fast/canvas/canvas-resize-after-paint-without-layout-expected.txt: Added.
+        * platform/chromium-linux/fast/canvas/canvas-resize-after-paint-without-layout-expected.png: Added.
+        * platform/chromium-linux/fast/canvas/setWidthResetAfterForcedRender-expected.png: Updated.
+
 2012-03-23  Dan Bernstein  <mitz@apple.com>
 
         Added all tests that failed more than once on the Lion WebKit2 Release bot between r111814
diff --git a/LayoutTests/fast/canvas/canvas-resize-after-paint-without-layout-expected.txt b/LayoutTests/fast/canvas/canvas-resize-after-paint-without-layout-expected.txt
new file mode 100644 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/LayoutTests/fast/canvas/canvas-resize-after-paint-without-layout.html b/LayoutTests/fast/canvas/canvas-resize-after-paint-without-layout.html
new file mode 100644 (file)
index 0000000..6aa01c9
--- /dev/null
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!-- Check that resizing a (potentially accelerated) canvas properly clears its
+     contents even if the layout size of the canvas does not change. Expected
+     output is a blank canvas.
+     https://bugs.webkit.org/show_bug.cgi?id=80871 -->
+<html>
+  <head>
+    <style>
+      #canvas {
+        outline: solid 1px black;
+        width: 300px;
+        height: 300px;
+      }
+    </style>
+    <script src="resources/repaint.js"></script>
+    <script>
+      if (window.layoutTestController)
+        layoutTestController.dumpAsText(true);
+
+      function runTest() {
+        var canvas = document.getElementById('canvas');
+        var ctx = canvas.getContext('2d');
+        ctx.fillStyle = 'red';
+        ctx.fillRect(0, 0, 300, 300);
+        runRepaintTest();
+      }
+
+      function repaintTest() {
+        var canvas = document.getElementById('canvas');
+        // This changes the resolution of the canvas but keeps its layout size constant.
+        canvas.width = canvas.width / 2;
+      }
+    </script>
+  </head>
+  <body onload="runTest();">
+    <canvas id="canvas" width="300" height="300"/>
+  </body>
+</html>
diff --git a/LayoutTests/platform/chromium-linux/fast/canvas/canvas-resize-after-paint-without-layout-expected.png b/LayoutTests/platform/chromium-linux/fast/canvas/canvas-resize-after-paint-without-layout-expected.png
new file mode 100644 (file)
index 0000000..91f145c
Binary files /dev/null and b/LayoutTests/platform/chromium-linux/fast/canvas/canvas-resize-after-paint-without-layout-expected.png differ
index 092b0e8e9ab29ec7f5a1bf03f5161bb9b376f4be..62d07ef016d6f888f94eb6ba8afb8ae96e2db5bb 100644 (file)
Binary files a/LayoutTests/platform/chromium-linux/fast/canvas/setWidthResetAfterForcedRender-expected.png and b/LayoutTests/platform/chromium-linux/fast/canvas/setWidthResetAfterForcedRender-expected.png differ
diff --git a/LayoutTests/platform/chromium-linux/platform/chromium/virtual/gpu/fast/canvas/canvas-resize-after-paint-without-layout-expected.png b/LayoutTests/platform/chromium-linux/platform/chromium/virtual/gpu/fast/canvas/canvas-resize-after-paint-without-layout-expected.png
new file mode 100644 (file)
index 0000000..d901ca1
Binary files /dev/null and b/LayoutTests/platform/chromium-linux/platform/chromium/virtual/gpu/fast/canvas/canvas-resize-after-paint-without-layout-expected.png differ
index a481235dd87b31b70eed821bd54606920a29b58f..0a7b639a1dc3843c64b1d4ae754b28bf50248165 100644 (file)
@@ -1,3 +1,50 @@
+2012-03-23  Sami Kyostila  <skyostil@chromium.org>
+
+        Reuse buffer allocation if canvas size does not change
+        https://bugs.webkit.org/show_bug.cgi?id=80871
+
+        Reviewed by Stephen White.
+
+        If the user changes the width or height attributes of a canvas element,
+        the contents of the canvas should be cleared and the context state
+        should be reset. This has become a common idiom to clear the canvas
+        "efficiently" at the start of a frame.
+
+        Previously, this code path triggered a full reallocation of the image
+        buffer backing the canvas, leading to reduced performance. This patch
+        implements an optimization where we reuse the previous image buffer
+        allocation if the size of the canvas did not change. Also, instead of
+        clearing the canvas every time its dimensions are touched, we only clear
+        if anything has been drawn into the canvas since it was previously
+        cleared.
+
+        Note that for now this optimization only applies for 2D canvases,
+        since it is not entirely clear how touching the dimensions of a WebGL
+        canvas should work.
+
+        Test: fast/canvas/canvas-resize-after-paint-without-layout.html +
+              existing layout tests for canvas resetting.
+
+        * html/HTMLCanvasElement.cpp:
+        (WebCore::HTMLCanvasElement::HTMLCanvasElement):
+        (WebCore::HTMLCanvasElement::reset):
+        (WebCore::HTMLCanvasElement::createImageBuffer): Save the initial
+        graphics context state so we can restore it without creating a new
+        context.
+        (WebCore::HTMLCanvasElement::clearImageBuffer):
+        (WebCore):
+        (WebCore::HTMLCanvasElement::clearCopiedImage):
+        * html/HTMLCanvasElement.h:
+        (HTMLCanvasElement):
+        * html/canvas/CanvasRenderingContext2D.cpp:
+        (WebCore::CanvasRenderingContext2D::reset): No need to notify the
+        compositor when the context is reset, because clearing the image buffer
+        does the same thing. We can also skip the notification if we did not
+        have an image buffer at the time of the reset, because the reset will
+        not have any visual impact in this case. Finally, if the canvas size
+        did change, the notification is also unnecessary because of the call
+        to RenderObject::repaint() from HTMLCanvasElement::reset().
+
 2012-03-22  Martin Robinson  <mrobinson@igalia.com>
 
         Fix some code generation warnings on GTK+.
index 07ab3091e598384a3db309c955e11a28c9e4d2c3..dea7f5ca7be11823a501d262d5986095cbc3b8df 100644 (file)
@@ -92,6 +92,7 @@ HTMLCanvasElement::HTMLCanvasElement(const QualifiedName& tagName, Document* doc
 #endif
     , m_originClean(true)
     , m_hasCreatedImageBuffer(false)
+    , m_didClearImageBuffer(false)
 {
     ASSERT(hasTagName(canvasTag));
 }
@@ -247,13 +248,26 @@ void HTMLCanvasElement::reset()
     if (!ok || h < 0)
         h = DefaultHeight;
 
+    if (m_contextStateSaver) {
+        // Reset to the initial graphics context state.
+        m_contextStateSaver->restore();
+        m_contextStateSaver->save();
+    }
+
     if (m_context && m_context->is2d()) {
         CanvasRenderingContext2D* context2D = static_cast<CanvasRenderingContext2D*>(m_context.get());
         context2D->reset();
     }
 
     IntSize oldSize = size();
-    setSurfaceSize(IntSize(w, h)); // The image buffer gets cleared here.
+    // If the size of an existing buffer matches, we can just clear it instead of reallocating.
+    // This optimization is only done for 2D canvases for now.
+    if (m_hasCreatedImageBuffer && oldSize == IntSize(w, h) && m_context && m_context->is2d()) {
+        if (!m_didClearImageBuffer)
+            clearImageBuffer();
+        return;
+    }
+    setSurfaceSize(IntSize(w, h));
 
 #if ENABLE(WEBGL)
     if (m_context && m_context->is3d() && oldSize != size())
@@ -262,8 +276,13 @@ void HTMLCanvasElement::reset()
 
     if (RenderObject* renderer = this->renderer()) {
         if (m_rendererIsCanvas) {
-            if (oldSize != size())
+            if (oldSize != size()) {
                 toRenderHTMLCanvas(renderer)->canvasSizeChanged();
+#if USE(ACCELERATED_COMPOSITING)
+                if (renderBox() && renderBox()->hasLayer() && renderBox()->layer()->hasAcceleratedCompositing())
+                    renderBox()->layer()->contentChanged(RenderLayer::CanvasChanged);
+#endif
+            }
             if (hadImageBuffer)
                 renderer->repaint();
         }
@@ -353,6 +372,7 @@ void HTMLCanvasElement::setSurfaceSize(const IntSize& size)
 {
     m_size = size;
     m_hasCreatedImageBuffer = false;
+    m_contextStateSaver.clear();
     m_imageBuffer.clear();
     clearCopiedImage();
 }
@@ -498,6 +518,7 @@ void HTMLCanvasElement::createImageBuffer() const
     ASSERT(!m_imageBuffer);
 
     m_hasCreatedImageBuffer = true;
+    m_didClearImageBuffer = true;
 
     FloatSize logicalSize = size();
     FloatSize deviceSize = convertLogicalToDevice(logicalSize);
@@ -529,6 +550,7 @@ void HTMLCanvasElement::createImageBuffer() const
     m_imageBuffer->context()->setShadowsIgnoreTransforms(true);
     m_imageBuffer->context()->setImageInterpolationQuality(DefaultInterpolationQuality);
     m_imageBuffer->context()->setStrokeThickness(1);
+    m_contextStateSaver = adoptPtr(new GraphicsContextStateSaver(*m_imageBuffer->context()));
 
 #if USE(JSC)
     JSC::JSLock lock(JSC::SilenceAssertionsOnly);
@@ -573,9 +595,25 @@ Image* HTMLCanvasElement::copiedImage() const
     return m_copiedImage.get();
 }
 
+void HTMLCanvasElement::clearImageBuffer() const
+{
+    ASSERT(m_hasCreatedImageBuffer);
+    ASSERT(!m_didClearImageBuffer);
+    ASSERT(m_context);
+
+    m_didClearImageBuffer = true;
+
+    if (m_context->is2d()) {
+        CanvasRenderingContext2D* context2D = static_cast<CanvasRenderingContext2D*>(m_context.get());
+        // No need to undo transforms/clip/etc. because we are called right after the context is reset.
+        context2D->clearRect(0, 0, width(), height());
+    }
+}
+
 void HTMLCanvasElement::clearCopiedImage()
 {
     m_copiedImage.clear();
+    m_didClearImageBuffer = false;
 }
 
 AffineTransform HTMLCanvasElement::baseTransform() const
index e2c4888e5b4b645654fad4ae9828bdeaf4056cb2..92ebd3438528bf64f7b173a5657264855f76bf83 100644 (file)
@@ -45,6 +45,7 @@ namespace WebCore {
 class CanvasContextAttributes;
 class CanvasRenderingContext;
 class GraphicsContext;
+class GraphicsContextStateSaver;
 class HTMLCanvasElement;
 class Image;
 class ImageData;
@@ -143,6 +144,7 @@ private:
     void reset();
 
     void createImageBuffer() const;
+    void clearImageBuffer() const;
 
     void setSurfaceSize(const IntSize&);
 
@@ -166,7 +168,9 @@ private:
 
     // m_createdImageBuffer means we tried to malloc the buffer.  We didn't necessarily get it.
     mutable bool m_hasCreatedImageBuffer;
+    mutable bool m_didClearImageBuffer;
     mutable OwnPtr<ImageBuffer> m_imageBuffer;
+    mutable OwnPtr<GraphicsContextStateSaver> m_contextStateSaver;
     
     mutable RefPtr<Image> m_presentedImage;
     mutable RefPtr<Image> m_copiedImage; // FIXME: This is temporary for platforms that have to copy the image buffer to render (and for CSSCanvasValue).
index 279b2a1c309275a8499cd2dd8ac29153316b51b2..8b26574749a77571669bffbc107f60819ca69981 100644 (file)
@@ -161,11 +161,6 @@ void CanvasRenderingContext2D::reset()
     m_stateStack.resize(1);
     m_stateStack.first() = State();
     m_path.clear();
-#if USE(ACCELERATED_COMPOSITING)
-    RenderBox* renderBox = canvas()->renderBox();
-    if (renderBox && renderBox->hasLayer() && renderBox->layer()->hasAcceleratedCompositing())
-        renderBox->layer()->contentChanged(RenderLayer::CanvasChanged);
-#endif
 }
 
 CanvasRenderingContext2D::State::State()