Bug 1012752 - Snap scrolled area to layer pixels. r?tnikkel draft
authorMarkus Stange <mstange@themasta.com>
Wed, 03 Aug 2016 19:27:15 -0400
changeset 396812 786a1bc7436e30f9ed9c54873b1ba1c7808972e2
parent 395624 6608e5864780589b25d5421c3d3673ab30c4c318
child 527299 b8d0ba0de83ef63922f78f575956a9c2e5e340dc
push id25117
push usermstange@themasta.com
push dateThu, 04 Aug 2016 15:53:33 +0000
reviewerstnikkel
bugs1012752
milestone51.0a1
Bug 1012752 - Snap scrolled area to layer pixels. r?tnikkel We want the maximum scroll position to be aligned with layer pixels. That way we don't have to re-rasterize the scrolled contents once scrolling hits the edge of the scrollable area. Here's how we determine the maximum scroll position: We get the scroll port rect, snapped to layer pixels. Then we get the scrolled rect and also snap that to layer pixels. The maximum scroll position is set to the difference between right/bottom edges of these rectangles. Now the scrollable area is computed by adding this maximum scroll position to the unsnapped scroll port size. The underlying idea here is: Pretend we have overflow:visible so that the scrolled contents start at (0, 0) relative to the scroll port and spill over the scroll port edges. When these contents are rendered, their rendering is snapped to layer pixels. We want those exact pixels to be accessible by scrolling. This way of computing the snapped scrollable area ensures that, if you scroll to the maximum scroll position, the right/bottom edges of the rendered scrolled contents line up exactly with the right/bottom edges of the scroll port. The scrolled contents are neither cut off nor are they moved too far. (This is something that no other browser engine gets completely right, see the testcase in bug 1012752.) There are also a few disadvantages to this solution. We snap to layer pixels, and the size of a layer pixel can depend on the zoom level, the document resolution, the current screen's scale factor, and CSS transforms. The snap origin is the position of the reference frame. So a change to any of these things can influence the scrollable area and the maximum scroll position. This patch does not make us adjust the current scroll position in the event that the maximum scroll position changes such that the current scroll position would be out of range, unless there's a reflow of the scrolled contents. This means that we can sometimes render a slightly inconsistent state where the current scroll position exceeds the maximum scroll position. We can fix this once it turns out to be a problem; I doubt that it will be a problem because none of the other browsers seems to prevent this problem either. The size of the scrollable area is exposed through the DOM properties scrollWidth and scrollHeight. At the moment, these are integer properties, so their value is rounded to the nearest CSS pixel. Before this patch, the returned value would always be within 0.5 CSS pixels of the value that layout computed for the content's scrollable overflow based on the CSS styles of the contents. Now that scrollWidth and scrollHeight also depend on pixel snapping, their values can deviate by up to one layer pixel from what the page might expect based on the styles of the contents. This change requires a few changes to existing tests. The fact that scrollWidth and scrollHeight can change based on the position of the scrollable element and the zoom level / resolution may surprise some web pages. However, this also seems to happen in Edge. Edge seems to always round scrollWidth and scrollHeight upwards, possibly to their equivalent of layout device pixels. MozReview-Commit-ID: 3LFV7Lio4tG
dom/html/test/test_bug332246.html
dom/tests/mochitest/general/test_offsets.html
dom/tests/mochitest/general/test_offsets.js
gfx/src/nsRect.cpp
gfx/src/nsRect.h
layout/base/FrameLayerBuilder.cpp
layout/generic/nsGfxScrollFrame.cpp
layout/generic/nsGfxScrollFrame.h
layout/generic/test/test_bug784410.html
layout/generic/test/test_bug791616.html
layout/reftests/scrolling/fractional-scroll-area-invalidation.html
layout/reftests/scrolling/fractional-scroll-area.html
layout/reftests/scrolling/reftest.list
layout/reftests/text-overflow/reftest.list
--- a/dom/html/test/test_bug332246.html
+++ b/dom/html/test/test_bug332246.html
@@ -32,40 +32,44 @@ https://bugzilla.mozilla.org/show_bug.cg
 </div>
 
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Bug 332246 **/
 
+function isWithFuzz(itIs, itShouldBe, fuzz, description) {
+  ok(Math.abs(itIs - itShouldBe) <= fuzz, `${description} - expected a value between ${itShouldBe - fuzz} and ${itShouldBe + fuzz}, got ${itIs}`);
+}
+
 var a1 = document.getElementById('a1');
 var a2 = document.getElementById('a2');
-is(a1.scrollHeight, 400, "Wrong a1.scrollHeight");
+isWithFuzz(a1.scrollHeight, 400, 1, "Wrong a1.scrollHeight");
 is(a1.offsetHeight, 100, "Wrong a1.offsetHeight");
 a2.scrollIntoView(true);
 is(a1.scrollTop, 100, "Wrong scrollTop value after a2.scrollIntoView(true)");
 a2.scrollIntoView(false);
 is(a1.scrollTop, 200, "Wrong scrollTop value after a2.scrollIntoView(false)");
 
 var b1 = document.getElementById('b1');
 var b2 = document.getElementById('b2');
-is(b1.scrollHeight, 420, "Wrong b1.scrollHeight");
+isWithFuzz(b1.scrollHeight, 420, 1, "Wrong b1.scrollHeight");
 is(b1.offsetHeight, 100, "Wrong b1.offsetHeight");
 b2.scrollIntoView(true);
 is(b1.scrollTop, 100, "Wrong scrollTop value after b2.scrollIntoView(true)");
 b2.scrollIntoView(false);
 is(b1.scrollTop, 220, "Wrong scrollTop value after b2.scrollIntoView(false)");
 
 var c1 = document.getElementById('c1');
 var c2 = document.getElementById('c2');
-is(c1.scrollHeight, 320, "Wrong c1.scrollHeight");
+isWithFuzz(c1.scrollHeight, 320, 1, "Wrong c1.scrollHeight");
 is(c1.offsetHeight, 100, "Wrong c1.offsetHeight");
 c2.scrollIntoView(true);
 is(c1.scrollTop, 100, "Wrong scrollTop value after c2.scrollIntoView(true)");
 c2.scrollIntoView(false);
-is(c1.scrollTop, 220, "Wrong scrollTop value after c2.scrollIntoView(false)");
+isWithFuzz(c1.scrollTop, 220, 1, "Wrong scrollTop value after c2.scrollIntoView(false)");
 
 </script>
 </pre>
 </body>
 </html>
 
--- a/dom/tests/mochitest/general/test_offsets.html
+++ b/dom/tests/mochitest/general/test_offsets.html
@@ -7,18 +7,22 @@
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
 
 <style>
   input {
     box-sizing: content-box;
   }
 </style>
 </head>
+
+<!-- We set a transform on the body element so that it creates a reference frame.
+     This makes sure that snapping of scrolled areas for the contained elements
+     is not influenced by offsets outside of this document. -->
 <body id="body" onload="setTimeout(testElements, 0, 'testelements', SimpleTest.finish);"
-      style="margin: 1px; border: 2px solid black; padding: 4px;">
+      style="margin: 1px; border: 2px solid black; padding: 4px; transform: translateY(1px);">
 
 <div id="testelements" style="margin: 0; border: 0; padding: 0;">
   <div id="div1" style="margin: 0; margin-left: 6px; margin-top: 2px; border: 1px solid green; padding: 6px; width: 50px; height: 20px"
          _offsetLeft="13" _offsetTop="9" _offsetWidth="64" _offsetHeight="34"
          _scrollWidth="62" _scrollHeight="32"
          _clientLeft="1" _clientTop="1" _clientWidth="62" _clientHeight="32"></div>
   <div id="noscroll" style="margin: 2px; border: 1px solid blue; padding: 3px;"
        _offsetLeft="10" _offsetTop="12" _offsetWidth="64" _offsetHeight="34"
@@ -50,17 +54,17 @@
   <div id="fixed" style="position: fixed; margin: 2px; border: 1px solid orange; padding: 7px; left: 87px; top: 12px;">
     This is some fixed positioned text.
     <div id="fixed-block" _offsetParent="fixed">
       <div id="fixed-replaced" _offsetParent="fixed" style="margin: 1px; border: 0; padding: 3px;"></div>
     </div>
   </div>
 
   <div id="scrollbox"
-       style="overflow: scroll; padding-left: 0px; margin: 3px; border: 4px solid green; max-width: 80px; max-height: 70px;"
+       style="overflow: scroll; padding-left: 0px; margin: 3px; border: 4px solid green; max-width: 80px; max-height: 70px"
        _scrollWidth="62" _scrollHeight="32"
        _clientLeft="1" _clientTop="1" _clientWidth="62" _clientHeight="32"><p id="p1" style="margin: 0; padding: 0;">One</p>
     <p id="p2">Two</p>
     <p id="scrollchild">Three</p>
     <p id="lastlinebox" style="margin: 0; padding: 0;"><input id="lastline" type="button"
                                style="margin: 0px; border: 2px solid red;"
                                value="This button is much longer than the others">
   </p></div>
--- a/dom/tests/mochitest/general/test_offsets.js
+++ b/dom/tests/mochitest/general/test_offsets.js
@@ -176,22 +176,35 @@ function checkClientState(element, left,
 }
 
 function checkCoord(element, type, val, testname)
 {
   if (val != -10000)
     is(element[type], Math.round(val), testname + " " + type);
 }
 
+function checkCoordFuzzy(element, type, val, fuzz, testname)
+{
+  if (val != -10000)
+    ok(Math.abs(element[type] - Math.round(val)) <= fuzz, testname + " " + type);
+}
+
 function checkCoords(element, type, left, top, width, height, testname)
 {
   checkCoord(element, type + "Left", left, testname);
   checkCoord(element, type + "Top", top, testname);
-  checkCoord(element, type + "Width", width, testname);
-  checkCoord(element, type + "Height", height, testname);
+
+  if (type == "scroll") {
+    // scrollWidth and scrollHeight can deviate by 1 pixel due to snapping.
+    checkCoordFuzzy(element, type + "Width", width, 1, testname);
+    checkCoordFuzzy(element, type + "Height", height, 1, testname);
+  } else {
+    checkCoord(element, type + "Width", width, testname);
+    checkCoord(element, type + "Height", height, testname);
+  }
 
   if (element instanceof SVGElement)
     return;
 
   if (element.id == "outerpopup" && !element.parentNode.open) // closed popup
     return;
 
   if (element.id == "div-displaynone" || element.id == "nonappended") // hidden elements
--- a/gfx/src/nsRect.cpp
+++ b/gfx/src/nsRect.cpp
@@ -1,30 +1,44 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsRect.h"
 #include "mozilla/gfx/Types.h"          // for NS_SIDE_BOTTOM, etc
+#include "mozilla/CheckedInt.h"         // for CheckedInt
 #include "nsDeviceContext.h"            // for nsDeviceContext
 #include "nsString.h"               // for nsAutoString, etc
 #include "nsMargin.h"                   // for nsMargin
 
 static_assert((int(NS_SIDE_TOP) == 0) &&
               (int(NS_SIDE_RIGHT) == 1) &&
               (int(NS_SIDE_BOTTOM) == 2) &&
               (int(NS_SIDE_LEFT) == 3),
               "The mozilla::css::Side sequence must match the nsMargin nscoord sequence");
 
 const mozilla::gfx::IntRect& GetMaxSizedIntRect() {
   static const mozilla::gfx::IntRect r(0, 0, INT32_MAX, INT32_MAX);
   return r;
 }
 
+
+bool nsRect::Overflows() const {
+#ifdef NS_COORD_IS_FLOAT
+  return false;
+#else
+  mozilla::CheckedInt<int32_t> xMost = this->x;
+  xMost += this->width;
+  mozilla::CheckedInt<int32_t> yMost = this->y;
+  yMost += this->height;
+  return !xMost.isValid() || !yMost.isValid();
+#endif
+}
+
 #ifdef DEBUG
 // Diagnostics
 
 FILE* operator<<(FILE* out, const nsRect& rect)
 {
   nsAutoString tmp;
 
   // Output the coordinates in fractional pixels so they're easier to read
--- a/gfx/src/nsRect.h
+++ b/gfx/src/nsRect.h
@@ -124,16 +124,19 @@ struct nsRect :
   {
     *this = aRect1.SaturatingUnion(aRect2);
   }
   void SaturatingUnionRectEdges(const nsRect& aRect1, const nsRect& aRect2)
   {
     *this = aRect1.SaturatingUnionEdges(aRect2);
   }
 
+  // Return whether this rect's right or bottom edge overflow int32.
+  bool Overflows() const;
+
   /**
    * Return this rect scaled to a different appunits per pixel (APP) ratio.
    * In the RoundOut version we make the rect the smallest rect containing the
    * unrounded result. In the RoundIn version we make the rect the largest rect
    * contained in the unrounded result.
    * @param aFromAPP the APP to scale from
    * @param aToAPP the APP to scale to
    * @note this can turn an empty rectangle into a non-empty rectangle
--- a/layout/base/FrameLayerBuilder.cpp
+++ b/layout/base/FrameLayerBuilder.cpp
@@ -3696,17 +3696,17 @@ ContainerState::ComputeOpaqueRect(nsDisp
         RelativeTo::ScrollFrame);
     if (!usingDisplayport) {
       // No async scrolling, so all that matters is that the layer contents
       // cover the scrollport.
       displayport = sf->GetScrollPortRect();
     }
     nsIFrame* scrollFrame = do_QueryFrame(sf);
     displayport += scrollFrame->GetOffsetToCrossDoc(mContainerReferenceFrame);
-    if (opaque.Contains(displayport)) {
+    if (opaquePixels.Contains(ScaleRegionToNearestPixels(displayport))) {
       *aOpaqueForAnimatedGeometryRootParent = true;
     }
   }
   return opaquePixels;
 }
 
 static const DisplayItemScrollClip*
 InnermostScrollClipApplicableToAGR(const DisplayItemScrollClip* aItemScrollClip,
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -3365,17 +3365,23 @@ ScrollFrameHelper::BuildDisplayList(nsDi
         if (mClipAllDescendants) {
           inactiveScrollClip = clipStateForScrollClip.InsertInactiveScrollClipForContentDescendants(aBuilder, sf);
         } else {
           inactiveScrollClip = clipStateForScrollClip.InsertInactiveScrollClipForContainingBlockDescendants(aBuilder, sf);
         }
         MOZ_ASSERT(!inactiveScrollClip->mIsAsyncScrollable);
       }
 
-      DisplayListClipState::AutoSaveRestore displayPortClipState(aBuilder);
+      // Clip our contents to the unsnapped scrolled rect. This makes sure that
+      // we don't have display items over the subpixel seam at the edge of the
+      // scrolled area.
+      DisplayListClipState::AutoSaveRestore scrolledRectClipState(aBuilder);
+      nsRect scrolledRectClip =
+        GetScrolledRectInternal(mScrolledFrame->GetScrollableOverflowRect(),
+                                mScrollPort.Size()) + mScrolledFrame->GetPosition();
       if (usingDisplayPort) {
         // Clip the contents to the display port.
         // The dirty rect already acts kind of like a clip, in that
         // FrameLayerBuilder intersects item bounds and opaque regions with
         // it, but it doesn't have the consistent snapping behavior of a
         // true clip.
         // For a case where this makes a difference, imagine the following
         // scenario: The display port has an edge that falls on a fractional
@@ -3383,18 +3389,20 @@ ScrollFrameHelper::BuildDisplayList(nsDi
         // whole display port up until that fractional edge, and there is a
         // transparent display item that overlaps the edge. We want to prevent
         // this transparent item from enlarging the scrolled layer's visible
         // region beyond its opaque region. The dirty rect doesn't do that -
         // it gets rounded out, whereas a true clip gets rounded to nearest
         // pixels.
         // If there is no display port, we don't need this because the clip
         // from the scroll port is still applied.
-        displayPortClipState.ClipContainingBlockDescendants(dirtyRect + aBuilder->ToReferenceFrame(mOuter));
+        scrolledRectClip = scrolledRectClip.Intersect(dirtyRect);
       }
+      scrolledRectClipState.ClipContainingBlockDescendants(
+        scrolledRectClip + aBuilder->ToReferenceFrame(mOuter));
 
       mOuter->BuildDisplayListForChild(aBuilder, mScrolledFrame, dirtyRect, scrolledContent);
     }
 
     if (contentBoxClipForNonCaretContent) {
       DisplayListClipState::AutoSaveRestore contentBoxClipState(aBuilder);
       if (mClipAllDescendants) {
         contentBoxClipState.ClipContentDescendants(*contentBoxClipForNonCaretContent);
@@ -5625,53 +5633,112 @@ ScrollFrameHelper::GetBorderRadii(const 
     ReduceRadii(border.left, border.bottom,
                 aRadii[NS_CORNER_BOTTOM_LEFT_X],
                 aRadii[NS_CORNER_BOTTOM_LEFT_Y]);
   }
 
   return true;
 }
 
+static nscoord
+SnapCoord(nscoord aCoord, double aRes, nscoord aAppUnitsPerPixel)
+{
+  double snappedToLayerPixels = NS_round((aRes*aCoord)/aAppUnitsPerPixel);
+  return NSToCoordRoundWithClamp(snappedToLayerPixels*aAppUnitsPerPixel/aRes);
+}
+
 nsRect
 ScrollFrameHelper::GetScrolledRect() const
 {
   nsRect result =
     GetScrolledRectInternal(mScrolledFrame->GetScrollableOverflowRect(),
                             mScrollPort.Size());
 
   if (result.width < mScrollPort.width) {
     NS_WARNING("Scrolled rect smaller than scrollport?");
   }
   if (result.height < mScrollPort.height) {
     NS_WARNING("Scrolled rect smaller than scrollport?");
   }
+
+  // Expand / contract the result by up to half a layer pixel so that scrolling
+  // to the right / bottom edge does not change the layer pixel alignment of
+  // the scrolled contents.
+  // For that, we first convert the scroll port and the scrolled rect to rects
+  // relative to the reference frame, since that's the space where painting does
+  // snapping.
+  nsSize scrollPortSize = GetScrollPositionClampingScrollPortSize();
+  nsIFrame* referenceFrame = nsLayoutUtils::GetReferenceFrame(mOuter);
+  nsPoint toReferenceFrame = mOuter->GetOffsetToCrossDoc(referenceFrame);
+  nsRect scrollPort(mScrollPort.TopLeft() + toReferenceFrame, scrollPortSize);
+  nsRect scrolledRect = result + scrollPort.TopLeft();
+
+  if (scrollPort.Overflows() || scrolledRect.Overflows()) {
+    return result;
+  }
+
+  // Now, snap the bottom right corner of both of these rects.
+  // We snap to layer pixels, so we need to respect the layer's scale.
+  nscoord appUnitsPerDevPixel = mScrolledFrame->PresContext()->AppUnitsPerDevPixel();
+  gfxSize scale = FrameLayerBuilder::GetPaintedLayerScaleForFrame(mScrolledFrame);
+  if (scale.IsEmpty()) {
+    scale = gfxSize(1.0f, 1.0f);
+  }
+
+  // Compute bounds for the scroll position, and computed the snapped scrolled
+  // rect from the scroll position bounds.
+  nscoord snappedScrolledAreaBottom = SnapCoord(scrolledRect.YMost(), scale.height, appUnitsPerDevPixel);
+  nscoord snappedScrollPortBottom = SnapCoord(scrollPort.YMost(), scale.height, appUnitsPerDevPixel);
+  nscoord maximumScrollOffsetY = snappedScrolledAreaBottom - snappedScrollPortBottom;
+  result.SetBottomEdge(scrollPort.height + maximumScrollOffsetY);
+
+  if (GetScrolledFrameDir() == NS_STYLE_DIRECTION_LTR) {
+    nscoord snappedScrolledAreaRight = SnapCoord(scrolledRect.XMost(), scale.width, appUnitsPerDevPixel);
+    nscoord snappedScrollPortRight = SnapCoord(scrollPort.XMost(), scale.width, appUnitsPerDevPixel);
+    nscoord maximumScrollOffsetX = snappedScrolledAreaRight - snappedScrollPortRight;
+    result.SetRightEdge(scrollPort.width + maximumScrollOffsetX);
+  } else {
+    // In RTL, the scrolled area's right edge is at scrollPort.XMost(),
+    // and the scrolled area's x position is zero or negative. We want
+    // the right edge to stay flush with the scroll port, so we snap the
+    // left edge.
+    nscoord snappedScrolledAreaLeft = SnapCoord(scrolledRect.x, scale.width, appUnitsPerDevPixel);
+    nscoord snappedScrollPortLeft = SnapCoord(scrollPort.x, scale.width, appUnitsPerDevPixel);
+    nscoord minimumScrollOffsetX = snappedScrolledAreaLeft - snappedScrollPortLeft;
+    result.SetLeftEdge(minimumScrollOffsetX);
+  }
+
   return result;
 }
 
-nsRect
-ScrollFrameHelper::GetScrolledRectInternal(const nsRect& aScrolledFrameOverflowArea,
-                                               const nsSize& aScrollPortSize) const
-{
-  uint8_t frameDir = IsLTR() ? NS_STYLE_DIRECTION_LTR : NS_STYLE_DIRECTION_RTL;
-
+
+uint8_t
+ScrollFrameHelper::GetScrolledFrameDir() const
+{
   // If the scrolled frame has unicode-bidi: plaintext, the paragraph
   // direction set by the text content overrides the direction of the frame
   if (mScrolledFrame->StyleTextReset()->mUnicodeBidi &
       NS_STYLE_UNICODE_BIDI_PLAINTEXT) {
     nsIFrame* childFrame = mScrolledFrame->PrincipalChildList().FirstChild();
     if (childFrame) {
-      frameDir =
-        (nsBidiPresUtils::ParagraphDirection(childFrame) == NSBIDI_LTR)
+      return (nsBidiPresUtils::ParagraphDirection(childFrame) == NSBIDI_LTR)
           ? NS_STYLE_DIRECTION_LTR : NS_STYLE_DIRECTION_RTL;
     }
   }
 
+  return IsLTR() ? NS_STYLE_DIRECTION_LTR : NS_STYLE_DIRECTION_RTL;
+}
+
+nsRect
+ScrollFrameHelper::GetScrolledRectInternal(const nsRect& aScrolledFrameOverflowArea,
+                                               const nsSize& aScrollPortSize) const
+{
   return nsLayoutUtils::GetScrolledRect(mScrolledFrame,
                                         aScrolledFrameOverflowArea,
-                                        aScrollPortSize, frameDir);
+                                        aScrollPortSize, GetScrolledFrameDir());
 }
 
 nsMargin
 ScrollFrameHelper::GetActualScrollbarSizes() const
 {
   nsRect r = mOuter->GetPaddingRect() - mOuter->GetPosition();
 
   return nsMargin(mScrollPort.y - r.y,
--- a/layout/generic/nsGfxScrollFrame.h
+++ b/layout/generic/nsGfxScrollFrame.h
@@ -617,16 +617,17 @@ protected:
                           nsIScrollbarMediator::ScrollSnapMode aSnap
                             = nsIScrollbarMediator::DISABLE_SNAP);
 
   void CompleteAsyncScroll(const nsRect &aRange, nsIAtom* aOrigin = nullptr);
 
   bool HasPluginFrames();
   bool HasPerspective() const;
   bool HasBgAttachmentLocal() const;
+  uint8_t GetScrolledFrameDir() const;
 
   static void EnsureFrameVisPrefsCached();
   static bool sFrameVisPrefsCached;
   // The number of scrollports wide/high to expand when tracking frame visibility.
   static uint32_t sHorzExpandScrollPort;
   static uint32_t sVertExpandScrollPort;
   // The fraction of the scrollport we allow to scroll by before we schedule
   // an update of frame visibility.
--- a/layout/generic/test/test_bug784410.html
+++ b/layout/generic/test/test_bug784410.html
@@ -4,17 +4,17 @@
   <title>Test bug 784410</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <script src="/tests/SimpleTest/paint_listener.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <p id="display"></p>
-<div id="outer" style="overflow:auto; height:200px; border:2px dotted black;" onscroll="doneScroll()">
+<div id="outer" style="overflow:auto; height:200px; border:2px dotted black; transform: translateY(1px)" onscroll="doneScroll()">
   <div id="d" style="overflow:auto; height:102px;" onscroll="doneScroll()">
     <div id="inner" style="height:100.1px; border:1px solid black; background:yellow;">Hello</div>
   </div>
   <div style="height:500px;"></div>
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript;version=1.7">
 var sel = window.getSelection();
--- a/layout/generic/test/test_bug791616.html
+++ b/layout/generic/test/test_bug791616.html
@@ -6,17 +6,17 @@
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
   <style>
 #t {
     overflow: auto;
     position: absolute;
     left: 200px;
     top: 100px;
-    font: 14px/1.1em "Consolas","Bitstream Vera Sans Mono","Courier New",Courier,monospace;
+    font: 14px/1.3em "Consolas","Bitstream Vera Sans Mono","Courier New",Courier,monospace;
 }
   </style>
 </head>
 <body>
 <p id="display"></p>
 <div id="t" contenteditable>
   <div>66666666666666</div>
   <div id="target">777777777777777777777777777777777777777</div>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/scrolling/fractional-scroll-area-invalidation.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html lang="en" reftest-async-scroll>
+<meta charset="utf-8">
+<title>Make sure the scrolled layer is not invalidated when you scroll all the way to the bottom</title>
+
+<style>
+
+body {
+  margin: 0;
+}
+
+.scrollbox {
+  margin: 50px;
+  width: 200px;
+  height: 200px;
+  overflow: auto;
+}
+
+.scrolled-contents {
+  height: 150.2px;
+  padding-top: 150px;
+}
+
+.reftest-no-paint {
+  margin: 0 20px;
+  border: 1px solid blue;
+  height: 25px;
+}
+
+</style>
+
+<body>
+
+<div class="scrollbox"
+     reftest-displayport-x="0" reftest-displayport-y="0"
+     reftest-displayport-w="200" reftest-displayport-h="200"
+     reftest-async-scroll-x="0" reftest-async-scroll-y="0">
+  <div class="scrolled-contents">
+    <div class="reftest-no-paint">
+      <!-- This element has the magic "reftest-no-paint" class which
+           constitutes the actual test here. -->
+    </div>
+  </div>
+</div>
+
+<script>
+
+var scrollbox = document.querySelector(".scrollbox");
+scrollbox.scrollTop = 2;
+scrollbox.scrollTop = 1;
+scrollbox.scrollTop = 0;
+
+function doTest() {
+  scrollbox.scrollTop = 999;
+  document.documentElement.removeAttribute("class");
+}
+document.addEventListener("MozReftestInvalidate", doTest);
+
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/scrolling/fractional-scroll-area.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="utf-8">
+<title>Fractional scroll area position / size</title>
+
+<style>
+
+body {
+  margin: 0;
+}
+
+#scrollbox {
+  width: 200px;
+  overflow: hidden;
+  background: red;
+}
+
+#scrolled-content {
+  background: lime;
+  box-sizing: border-box;
+  border: solid black;
+  border-width: 1px 0;
+}
+
+</style>
+
+<div id="scrollbox">
+  <div id="scrolled-content"></div>
+</div>
+
+<script>
+
+function getFloatQueryParams(defaultValues) {
+  let result = Object.assign({}, defaultValues);
+  for (let chunk of location.search.substr(1).split("&")) {
+    let parts = chunk.split("=");
+    result[parts[0]] = parseFloat(parts[1]);
+  }
+  return result;
+}
+
+let params = getFloatQueryParams({
+  top: 0,
+  outerBottom: 100,
+  innerBottom: 100,
+  borderTop: 0,
+  borderBottom: 0,
+  scrollBefore: 0,
+  scrollAfter: undefined,
+  offsetAfter: undefined,
+});
+
+let scrollArea = document.getElementById("scrollbox");
+let scrolledContent = document.getElementById("scrolled-content");
+
+scrollArea.style.marginTop = params.top + "px";
+scrollArea.style.height = (params.outerBottom - params.top) + "px";
+scrolledContent.style.height =  (params.innerBottom - params.top) + "px";
+
+scrollArea.scrollTop = 1;
+scrollArea.scrollTop = 2;
+scrollArea.scrollTop = params.scrollBefore;
+
+window.addEventListener("MozReftestInvalidate", function () {
+  if (params.scrollAfter !== undefined) {
+    scrollArea.scrollTop = params.scrollAfter;
+  }
+  if (params.offsetAfter !== undefined) {
+    document.body.style.marginTop = params.offsetAfter + "px";
+  }
+  document.documentElement.className = "";
+});
+
+</script>
--- a/layout/reftests/scrolling/reftest.list
+++ b/layout/reftests/scrolling/reftest.list
@@ -31,8 +31,57 @@ fuzzy-if(Android,5,20000) == uncovering-
 skip-if(B2G||Mulet) fuzzy-if(asyncPan&&!layersGPUAccelerated,121,3721) == less-than-scrollbar-height.html less-than-scrollbar-height-ref.html # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(B2G||Mulet) == huge-horizontal-overflow.html huge-horizontal-overflow-ref.html # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(B2G||Mulet) == huge-vertical-overflow.html huge-vertical-overflow-ref.html # Initial mulet triage: parity with B2G/B2G Desktop
 fuzzy-if(asyncPan&&!layersGPUAccelerated,102,6818) == iframe-scrolling-attr-1.html iframe-scrolling-attr-ref.html
 skip-if((B2G&&browserIsRemote)||Mulet) fuzzy-if(asyncPan&&!layersGPUAccelerated,102,6818) == iframe-scrolling-attr-2.html iframe-scrolling-attr-ref.html # Initial mulet triage: parity with B2G/B2G Desktop
 == frame-scrolling-attr-1.html frame-scrolling-attr-ref.html
 fuzzy-if(asyncPan&&!layersGPUAccelerated,102,2420) == frame-scrolling-attr-2.html frame-scrolling-attr-ref.html
 == move-item.html move-item-ref.html # bug 1125750
+== fractional-scroll-area.html?top=-0.4&outerBottom=100&innerBottom=200 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0.4&outerBottom=100&innerBottom=200 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0&outerBottom=99.6&innerBottom=200 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0&outerBottom=100.4&innerBottom=200 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=-0.4&outerBottom=99.6&innerBottom=200 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=-0.4&outerBottom=100.4&innerBottom=200 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0.4&outerBottom=99.6&innerBottom=200 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0.4&outerBottom=100.4&innerBottom=200 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=-0.4&outerBottom=100&innerBottom=199.6 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0.4&outerBottom=100&innerBottom=199.6 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0&outerBottom=99.6&innerBottom=199.6 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0&outerBottom=100.4&innerBottom=199.6 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=-0.4&outerBottom=99.6&innerBottom=199.6 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=-0.4&outerBottom=100.4&innerBottom=199.6 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0.4&outerBottom=99.6&innerBottom=199.6 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0.4&outerBottom=100.4&innerBottom=199.6 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=-0.4&outerBottom=100&innerBottom=200.4 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0.4&outerBottom=100&innerBottom=200.4 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0&outerBottom=99.6&innerBottom=200.4 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0&outerBottom=100.4&innerBottom=200.4 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=-0.4&outerBottom=99.6&innerBottom=200.4 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=-0.4&outerBottom=100.4&innerBottom=200.4 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0.4&outerBottom=99.6&innerBottom=200.4 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=0.4&outerBottom=100.4&innerBottom=200.4 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200
+== fractional-scroll-area.html?top=-0.4&outerBottom=100&innerBottom=200&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0.4&outerBottom=100&innerBottom=200&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0&outerBottom=99.6&innerBottom=200&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0&outerBottom=100.4&innerBottom=200&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=-0.4&outerBottom=99.6&innerBottom=200&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=-0.4&outerBottom=100.4&innerBottom=200&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0.4&outerBottom=99.6&innerBottom=200&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0.4&outerBottom=100.4&innerBottom=200&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=-0.4&outerBottom=100&innerBottom=199.6&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0.4&outerBottom=100&innerBottom=199.6&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0&outerBottom=99.6&innerBottom=199.6&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0&outerBottom=100.4&innerBottom=199.6&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=-0.4&outerBottom=99.6&innerBottom=199.6&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=-0.4&outerBottom=100.4&innerBottom=199.6&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0.4&outerBottom=99.6&innerBottom=199.6&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0.4&outerBottom=100.4&innerBottom=199.6&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=-0.4&outerBottom=100&innerBottom=200.4&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0.4&outerBottom=100&innerBottom=200.4&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0&outerBottom=99.6&innerBottom=200.4&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0&outerBottom=100.4&innerBottom=200.4&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=-0.4&outerBottom=99.6&innerBottom=200.4&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=-0.4&outerBottom=100.4&innerBottom=200.4&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0.4&outerBottom=99.6&innerBottom=200.4&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+== fractional-scroll-area.html?top=0.4&outerBottom=100.4&innerBottom=200.4&scrollBefore=999 fractional-scroll-area.html?top=0&outerBottom=100&innerBottom=200&scrollBefore=999
+!= fractional-scroll-area-invalidation.html about:blank
--- a/layout/reftests/text-overflow/reftest.list
+++ b/layout/reftests/text-overflow/reftest.list
@@ -17,17 +17,17 @@ skip-if(B2G||Mulet) random-if(/^Windows\
 HTTP(..) == marker-shadow.html marker-shadow-ref.html
 == aligned-baseline.html aligned-baseline-ref.html
 skip-if(Android||B2G) fuzzy-if(skiaContent,1,5) == clipped-elements.html clipped-elements-ref.html
 HTTP(..) == theme-overflow.html theme-overflow-ref.html
 skip-if(B2G||Mulet) HTTP(..) == table-cell.html table-cell-ref.html # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(Mulet) fuzzy-if(gtkWidget,10,32) HTTP(..) == two-value-syntax.html two-value-syntax-ref.html # MULET: Bug 1144079: Re-enable Mulet mochitests and reftests taskcluster-specific disables
 skip-if(B2G||Mulet) HTTP(..) == single-value.html single-value-ref.html  # Initial mulet triage: parity with B2G/B2G Desktop
 skip-if(B2G||Mulet) fuzzy-if(gtkWidget,10,2) HTTP(..) == atomic-under-marker.html atomic-under-marker-ref.html # Initial mulet triage: parity with B2G/B2G Desktop
-fuzzy(1,702) skip-if(Android||B2G||Mulet) fuzzy-if(asyncPan&&!layersGPUAccelerated,102,12352) HTTP(..) == xulscroll.html xulscroll-ref.html # Initial mulet triage: parity with B2G/B2G Desktop
+fuzzy(1,2616) skip-if(Android||B2G||Mulet) fuzzy-if(asyncPan&&!layersGPUAccelerated,102,12352) HTTP(..) == xulscroll.html xulscroll-ref.html # Initial mulet triage: parity with B2G/B2G Desktop
 HTTP(..) == combobox-zoom.html combobox-zoom-ref.html
 
 # The vertical-text pref setting can be removed after bug 1138384 lands
 pref(layout.css.vertical-text.enabled,true) == vertical-decorations-1.html vertical-decorations-1-ref.html
 pref(layout.css.vertical-text.enabled,true) == vertical-decorations-2.html vertical-decorations-2-ref.html
 pref(layout.css.vertical-text.enabled,true) != vertical-decorations-1.html vertical-decorations-1-2-notref.html
 pref(layout.css.vertical-text.enabled,true) != vertical-decorations-2.html vertical-decorations-1-2-notref.html
 pref(layout.css.vertical-text.enabled,true) == vertical-decorations-3.html vertical-decorations-3-ref.html