Bug 1246851 (Part 1) - Add a new SurfacePipe API for writing to image surfaces in a safe and composable manner. r=njn
authorSeth Fowler <mark.seth.fowler@gmail.com>
Thu, 25 Feb 2016 16:21:29 -0800
changeset 285652 de2f01e26094f9c4bb9a42421ee07219278dcb4d
parent 285651 d506b1c5ce8e91b0320f9d33c84a6b504b3e1d21
child 285653 e24b148198373a064a9544099882accbf2f8ca46
push id30033
push userkwierso@gmail.com
push dateFri, 26 Feb 2016 19:21:05 +0000
treeherdermozilla-central@07438c9bfc83 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnjn
bugs1246851
milestone47.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1246851 (Part 1) - Add a new SurfacePipe API for writing to image surfaces in a safe and composable manner. r=njn
image/Decoder.h
image/SurfacePipe.cpp
image/SurfacePipe.h
image/build/nsImageModule.cpp
image/moz.build
--- a/image/Decoder.h
+++ b/image/Decoder.h
@@ -272,16 +272,18 @@ public:
    *
    * Any errors are reported by setting the appropriate state on the decoder.
    */
   void Write(const char* aBuffer, uint32_t aCount);
 
 
 protected:
   friend class nsICODecoder;
+  friend class PalettedSurfaceSink;
+  friend class SurfaceSink;
 
   virtual ~Decoder();
 
   /*
    * Internal hooks. Decoder implementations may override these and
    * only these methods.
    *
    * BeforeFinishInternal() can be used to detect if decoding is in an
new file mode 100644
--- /dev/null
+++ b/image/SurfacePipe.cpp
@@ -0,0 +1,199 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "SurfacePipe.h"
+
+#include <utility>
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/DebugOnly.h"
+#include "Decoder.h"
+
+namespace mozilla {
+namespace image {
+
+using namespace gfx;
+
+using std::min;
+
+/* static */ UniquePtr<NullSurfaceSink> NullSurfaceSink::sSingleton;
+
+/* static */ NullSurfaceSink*
+NullSurfaceSink::Singleton()
+{
+  if (!sSingleton) {
+    MOZ_ASSERT(NS_IsMainThread());
+    sSingleton = MakeUnique<NullSurfaceSink>();
+    ClearOnShutdown(&sSingleton);
+
+    DebugOnly<nsresult> rv = sSingleton->Configure(NullSurfaceConfig { });
+    MOZ_ASSERT(NS_SUCCEEDED(rv), "Couldn't configure a NullSurfaceSink?");
+  }
+
+  return sSingleton.get();
+}
+
+nsresult
+NullSurfaceSink::Configure(const NullSurfaceConfig& aConfig)
+{
+  // Note that the choice of uint32_t as the pixel size here is more or less
+  // arbitrary, since you cannot write to a NullSurfaceSink anyway, but uint32_t
+  // is a natural choice since most SurfacePipes will be for BGRA/BGRX surfaces.
+  ConfigureFilter(IntSize(), sizeof(uint32_t));
+  return NS_OK;
+}
+
+Maybe<SurfaceInvalidRect>
+AbstractSurfaceSink::TakeInvalidRect()
+{
+  if (mInvalidRect.IsEmpty()) {
+    return Nothing();
+  }
+
+  SurfaceInvalidRect invalidRect;
+  invalidRect.mInputSpaceRect = invalidRect.mOutputSpaceRect = mInvalidRect;
+
+  // Forget about the invalid rect we're returning.
+  mInvalidRect = IntRect();
+
+  return Some(invalidRect);
+}
+
+uint8_t*
+AbstractSurfaceSink::DoResetToFirstRow()
+{
+  mRow = 0;
+  return GetRowPointer();
+}
+
+uint8_t*
+AbstractSurfaceSink::AdvanceRow()
+{
+  if (mRow >= uint32_t(InputSize().height)) {
+    return nullptr;
+  }
+
+  // If we're vertically flipping the output, we need to flip the invalid rect. Since we're
+  // dealing with an axis-aligned rect, only the y coordinate needs to change.
+  int32_t invalidY = mFlipVertically
+                   ? InputSize().height - (mRow + 1)
+                   : mRow;
+  mInvalidRect.UnionRect(mInvalidRect,
+                         IntRect(0, invalidY, InputSize().width, 1));
+
+  mRow = min(uint32_t(InputSize().height), mRow + 1);
+
+  return mRow < uint32_t(InputSize().height) ? GetRowPointer()
+                                             : nullptr;
+}
+
+nsresult
+SurfaceSink::Configure(const SurfaceConfig& aConfig)
+{
+  // For non-paletted surfaces, the surface size is just the output size.
+  IntSize surfaceSize = aConfig.mOutputSize;
+
+  // Non-paletted surfaces should not have frame rects, so we just pass
+  // AllocateFrame() a frame rect which covers the entire surface.
+  IntRect frameRect(0, 0, surfaceSize.width, surfaceSize.height);
+
+  // Allocate the frame.
+  // XXX(seth): Once every Decoder subclass uses SurfacePipe, we probably want
+  // to allocate the frame directly here and get rid of Decoder::AllocateFrame
+  // altogether.
+  nsresult rv = aConfig.mDecoder->AllocateFrame(aConfig.mFrameNum,
+                                                surfaceSize,
+                                                frameRect,
+                                                aConfig.mFormat);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  mImageData = aConfig.mDecoder->mImageData;
+  mImageDataLength = aConfig.mDecoder->mImageDataLength;
+  mFlipVertically = aConfig.mFlipVertically;
+
+  MOZ_ASSERT(mImageData);
+  MOZ_ASSERT(mImageDataLength ==
+               uint32_t(surfaceSize.width * surfaceSize.height * sizeof(uint32_t)));
+
+  ConfigureFilter(surfaceSize, sizeof(uint32_t));
+  return NS_OK;
+}
+
+uint8_t*
+SurfaceSink::GetRowPointer() const
+{
+  // If we're flipping vertically, reverse the order in which we traverse the
+  // rows.
+  uint32_t row = mFlipVertically
+               ? InputSize().height - (mRow + 1)
+               : mRow;
+
+  uint8_t* rowPtr = mImageData + row * InputSize().width * sizeof(uint32_t);
+
+  MOZ_ASSERT(rowPtr >= mImageData);
+  MOZ_ASSERT(rowPtr < mImageData + mImageDataLength);
+  MOZ_ASSERT(rowPtr + InputSize().width * sizeof(uint32_t) <=
+               mImageData + mImageDataLength);
+
+  return rowPtr;
+}
+
+
+nsresult
+PalettedSurfaceSink::Configure(const PalettedSurfaceConfig& aConfig)
+{
+  // For paletted surfaces, the surface size is the size of the frame rect.
+  IntSize surfaceSize = aConfig.mFrameRect.Size();
+
+  // Allocate the frame.
+  // XXX(seth): Once every Decoder subclass uses SurfacePipe, we probably want
+  // to allocate the frame directly here and get rid of Decoder::AllocateFrame
+  // altogether.
+  nsresult rv = aConfig.mDecoder->AllocateFrame(aConfig.mFrameNum,
+                                                aConfig.mOutputSize,
+                                                aConfig.mFrameRect,
+                                                aConfig.mFormat,
+                                                aConfig.mPaletteDepth);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  mImageData = aConfig.mDecoder->mImageData;
+  mImageDataLength = aConfig.mDecoder->mImageDataLength;
+  mFlipVertically = aConfig.mFlipVertically;
+  mFrameRect = aConfig.mFrameRect;
+
+  MOZ_ASSERT(mImageData);
+  MOZ_ASSERT(mImageDataLength ==
+               uint32_t(mFrameRect.width * mFrameRect.height * sizeof(uint8_t)));
+
+  ConfigureFilter(surfaceSize, sizeof(uint8_t));
+  return NS_OK;
+}
+
+uint8_t*
+PalettedSurfaceSink::GetRowPointer() const
+{
+  // If we're flipping vertically, reverse the order in which we traverse the
+  // rows.
+  uint32_t row = mFlipVertically
+               ? InputSize().height - (mRow + 1)
+               : mRow;
+
+  uint8_t* rowPtr = mImageData + row * InputSize().width * sizeof(uint8_t);
+
+  MOZ_ASSERT(rowPtr >= mImageData);
+  MOZ_ASSERT(rowPtr < mImageData + mImageDataLength);
+  MOZ_ASSERT(rowPtr + InputSize().width * sizeof(uint8_t) <=
+               mImageData + mImageDataLength);
+
+  return rowPtr;
+}
+
+} // namespace image
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/image/SurfacePipe.h
@@ -0,0 +1,570 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+/**
+ * A SurfacePipe is a pipeline that consists of a series of SurfaceFilters
+ * terminating in a SurfaceSink. Each SurfaceFilter transforms the image data in
+ * some way before the SurfaceSink ultimately writes it to the surface. This
+ * design allows for each transformation to be tested independently, for the
+ * transformations to be combined as needed to meet the needs of different
+ * situations, and for all image decoders to share the same code for these
+ * transformations.
+ *
+ * Writing to the SurfacePipe is done using lambdas that act as generator
+ * functions. Because the SurfacePipe machinery controls where the writes take
+ * place, a bug in an image decoder cannot cause a buffer overflow of the
+ * underlying surface. In particular, when using WritePixels() a buffer overflow
+ * is impossible as long as the SurfacePipe code is correct.
+ */
+
+#ifndef mozilla_image_SurfacePipe_h
+#define mozilla_image_SurfacePipe_h
+
+#include <stdint.h>
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Move.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/unused.h"
+#include "mozilla/Variant.h"
+#include "mozilla/gfx/2D.h"
+
+namespace mozilla {
+namespace image {
+
+class Decoder;
+
+/**
+ * An invalid rect for a surface. Results are given both in the space of the
+ * input image (i.e., before any SurfaceFilters are applied) and in the space
+ * of the output surface (after all SurfaceFilters).
+ */
+struct SurfaceInvalidRect
+{
+  gfx::IntRect mInputSpaceRect;   /// The invalid rect in pre-SurfacePipe space.
+  gfx::IntRect mOutputSpaceRect;  /// The invalid rect in post-SurfacePipe space.
+};
+
+/**
+ * An enum used to allow the lambdas passed to WritePixels() and WriteRows() to
+ * communicate their state to the caller.
+ */
+enum class WriteState : uint8_t
+{
+  NEED_MORE_DATA,  /// The lambda ran out of data.
+
+  FINISHED,        /// The lambda is done writing to the surface; future writes
+                   /// will fail.
+
+  ERROR            /// The lambda encountered an error. The caller may recover
+                   /// if possible and continue to write. (This never indicates
+                   /// an error in the SurfacePipe machinery itself; it's only
+                   /// generated by the lambdas.)
+};
+
+/**
+ * A template alias used to make the return value of WritePixels() lambdas
+ * (which may return either a pixel value or a WriteState) easier to specify.
+ * WriteRows() doesn't need such a template alias since WriteRows() lambdas
+ * don't return a pixel value.
+ */
+template <typename PixelType>
+using NextPixel = Variant<PixelType, WriteState>;
+
+/**
+ * SurfaceFilter is the abstract superclass of SurfacePipe pipeline stages.
+ * It implements the the code that actually writes to the surface -
+ * WritePixels() and WriteRows() - which are non-virtual for efficiency.
+ *
+ * SurfaceFilter's API is nonpublic; only SurfacePipe and other SurfaceFilters
+ * should use it. Non-SurfacePipe code should use the methods on SurfacePipe.
+ *
+ * To implement a SurfaceFilter, it's necessary to subclass SurfaceFilter and
+ * implement, at a minimum, the pure virtual methods. It's also necessary to
+ * define a Config struct with a Filter typedef member that identifies the
+ * matching SurfaceFilter class, and a Configure() template method. See an
+ * existing SurfaceFilter subclass, such as RemoveFrameRectFilter, for an
+ * example of how the Configure() method must be implemented. It takes a list of
+ * Config structs, passes the tail of the list to the next filter in the chain's
+ * Configure() method, and then uses the head of the list to configure itself. A
+ * SurfaceFilter's Configure() method must also call
+ * SurfaceFilter::ConfigureFilter() to provide WritePixels() and WriteRows()
+ * with the information they need to do their jobs.
+ */
+class SurfaceFilter
+{
+public:
+  SurfaceFilter()
+    : mRowPointer(nullptr)
+    , mCol(0)
+    , mPixelSize(0)
+  { }
+
+  virtual ~SurfaceFilter() { }
+
+  /**
+   * Reset this surface to the first row. It's legal for this filter to throw
+   * away any previously written data at this point, as all rows must be written
+   * to on every pass.
+   *
+   * @return a pointer to the buffer for the first row.
+   */
+  uint8_t* ResetToFirstRow()
+  {
+    mCol = 0;
+    mRowPointer = DoResetToFirstRow();
+    return mRowPointer;
+  }
+
+  /// @return a pointer to the buffer for the current row.
+  uint8_t* CurrentRowPointer() const { return mRowPointer; }
+
+  /// @return true if we've finished writing to the surface.
+  bool IsSurfaceFinished() const { return mRowPointer == nullptr; }
+
+  /// @return the input size this filter expects.
+  gfx::IntSize InputSize() const { return mInputSize; }
+
+  /**
+   * Write pixels to the surface one at a time by repeatedly calling a lambda
+   * that yields pixels. WritePixels() should be preferred over WriteRows()
+   * whenever using it will not introduce additional copies or other performance
+   * penalties, because it is completely memory safe.
+   *
+   * Writing continues until every pixel in the surface has been written to
+   * (i.e., IsSurfaceFinished() returns true) or the lambda returns a WriteState
+   * which WritePixels() will return to the caller.
+   *
+   * The template parameter PixelType must be uint8_t (for paletted surfaces) or
+   * uint32_t (for BGRA/BGRX surfaces) and must be in agreement with the pixel
+   * size passed to ConfigureFilter().
+   *
+   * XXX(seth): We'll remove all support for paletted surfaces in bug 1247520,
+   * which means we can remove the PixelType template parameter from this
+   * method.
+   *
+   * @param aFunc A lambda that functions as a generator, yielding the next
+   *              pixel in the surface each time it's called. The lambda must
+   *              return a NextPixel<PixelType> value.
+   *
+   * @return A WriteState value indicating the lambda generator's state.
+   *         WritePixels() itself will return WriteState::FINISHED if writing
+   *         has finished, regardless of the lambda's internal state.
+   */
+  template <typename PixelType, typename Func>
+  WriteState WritePixels(Func aFunc)
+  {
+    MOZ_ASSERT(mPixelSize == 1 || mPixelSize == 4);
+    MOZ_ASSERT_IF(mPixelSize == 1, sizeof(PixelType) == sizeof(uint8_t));
+    MOZ_ASSERT_IF(mPixelSize == 4, sizeof(PixelType) == sizeof(uint32_t));
+
+    while (!IsSurfaceFinished()) {
+      PixelType* rowPtr = reinterpret_cast<PixelType*>(mRowPointer);
+
+      for (; mCol < mInputSize.width; ++mCol) {
+        NextPixel<PixelType> result = aFunc();
+        if (result.template is<PixelType>()) {
+          rowPtr[mCol] = result.template as<PixelType>();
+          continue;
+        }
+
+        switch (result.template as<WriteState>()) {
+          case WriteState::NEED_MORE_DATA:
+            return WriteState::NEED_MORE_DATA;
+
+          case WriteState::FINISHED:
+            // Make sure that IsSurfaceFinished() returns true so the caller
+            // can't write anything else to the pipeline.
+            mRowPointer = nullptr;
+            mCol = 0;
+            return WriteState::FINISHED;
+
+          case WriteState::ERROR:
+            // Note that we don't need to record this anywhere, because this
+            // indicates an error in aFunc, and there's nothing wrong with our
+            // machinery. The caller can recover as needed and continue writing to
+            // the row.
+            return WriteState::ERROR;
+        }
+      }
+
+      // We've finished the row.
+      mRowPointer = AdvanceRow();
+      mCol = 0;
+    }
+
+    // We've finished the entire surface.
+    return WriteState::FINISHED;
+  }
+
+  /**
+   * Write rows to the surface one at a time by repeatedly calling a lambda
+   * that yields rows. Because WriteRows() is not completely memory safe,
+   * WritePixels() should be preferred whenever it can be used without
+   * introducing additional copies or other performance penalties.
+   *
+   * Writing continues until every row in the surface has been written to (i.e.,
+   * IsSurfaceFinished() returns true) or the lambda returns a WriteState which
+   * WriteRows() will return to the caller.
+   *
+   * The template parameter PixelType must be uint8_t (for paletted surfaces) or
+   * uint32_t (for BGRA/BGRX surfaces) and must be in agreement with the pixel
+   * size passed to ConfigureFilter().
+   *
+   * XXX(seth): We'll remove all support for paletted surfaces in bug 1247520,
+   * which means we can remove the PixelType template parameter from this
+   * method.
+   *
+   * @param aFunc A lambda that functions as a generator, yielding the next
+   *              row in the surface each time it's called. The lambda must
+   *              return a Maybe<WriteState> value; if Some(), the return value
+   *              indicates a WriteState to return to the caller, while
+   *              Nothing() indicates that the lambda can generate more rows.
+   *
+   * @return A WriteState value indicating the lambda generator's state.
+   *         WriteRows() itself will return WriteState::FINISHED if writing
+   *         has finished, regardless of the lambda's internal state.
+   */
+  template <typename PixelType, typename Func>
+  WriteState WriteRows(Func aFunc)
+  {
+    MOZ_ASSERT(mPixelSize == 1 || mPixelSize == 4);
+    MOZ_ASSERT_IF(mPixelSize == 1, sizeof(PixelType) == sizeof(uint8_t));
+    MOZ_ASSERT_IF(mPixelSize == 4, sizeof(PixelType) == sizeof(uint32_t));
+
+    if (IsSurfaceFinished()) {
+      return WriteState::FINISHED;  // Already done.
+    }
+
+    while (true) {
+      PixelType* rowPtr = reinterpret_cast<PixelType*>(mRowPointer);
+
+      Maybe<WriteState> result = aFunc(rowPtr, mInputSize.width);
+      if (result != Some(WriteState::ERROR)) {
+        mCol = 0;
+        mRowPointer = AdvanceRow();  // We've finished the row.
+      }
+
+      if (IsSurfaceFinished()) {
+        break;
+      }
+
+      if (result == Some(WriteState::FINISHED)) {
+        // Make sure that IsSurfaceFinished() returns true so the caller can't
+        // write anything else to the pipeline.
+        mRowPointer = nullptr;
+        mCol = 0;
+      }
+
+      if (result) {
+        return *result;
+      }
+    }
+
+    // We've finished the entire surface.
+    return WriteState::FINISHED;
+  }
+
+  //////////////////////////////////////////////////////////////////////////////
+  // Methods Subclasses Should Override
+  //////////////////////////////////////////////////////////////////////////////
+
+  /// @return true if this SurfaceFilter can be used with paletted surfaces.
+  virtual bool IsValidPalettedPipe() const { return false; }
+
+  /**
+   * Called by WritePixels() and WriteRows() to advance this filter to the next
+   * row.
+   *
+   * @return a pointer to the buffer for the next row, or nullptr to indicate
+   *         that we've finished the entire surface.
+   */
+  virtual uint8_t* AdvanceRow() = 0;
+
+  /**
+   * @return a SurfaceInvalidRect representing the region of the surface that
+   *         has been written to since the last time TakeInvalidRect() was
+   *         called, or Nothing() if the region is empty (i.e. nothing has been
+   *         written).
+   */
+  virtual Maybe<SurfaceInvalidRect> TakeInvalidRect() = 0;
+
+protected:
+
+  /**
+   * Called by ResetToFirstRow() to actually perform the reset. It's legal to
+   * throw away any previously written data at this point, as all rows must be
+   * written to on every pass.
+   */
+  virtual uint8_t* DoResetToFirstRow() = 0;
+
+
+  //////////////////////////////////////////////////////////////////////////////
+  // Methods For Internal Use By Subclasses
+  //////////////////////////////////////////////////////////////////////////////
+
+  /**
+   * Called by subclasses' Configure() methods to initialize the configuration
+   * of this filter. After the filter is configured, calls ResetToFirstRow().
+   *
+   * @param aInputSize The input size of this filter, in pixels. The previous
+   *                   filter in the chain will expect to write into rows
+   *                   |aInputSize.width| pixels wide.
+   * @param aPixelSize How large, in bytes, each pixel in the surface is. This
+   *                   should be either 1 for paletted images or 4 for BGRA/BGRX
+   *                   images.
+   */
+  void ConfigureFilter(gfx::IntSize aInputSize, uint8_t aPixelSize)
+  {
+    mInputSize = aInputSize;
+    mPixelSize = aPixelSize;
+
+    ResetToFirstRow();
+  }
+
+private:
+  gfx::IntSize mInputSize;  /// The size of the input this filter expects.
+  uint8_t* mRowPointer;     /// Pointer to the current row or null if finished.
+  int32_t mCol;             /// The current column we're writing to. (0-indexed)
+  uint8_t  mPixelSize;      /// How large each pixel in the surface is, in bytes.
+};
+
+class NullSurfaceSink;
+
+/// A trivial configuration struct for NullSurfaceSink.
+struct NullSurfaceConfig
+{
+  using Filter = NullSurfaceSink;
+};
+
+/**
+ * NullSurfaceSink is a trivial SurfaceFilter implementation that behaves as if
+ * it were a zero-size SurfaceSink. It's used as the default filter chain for an
+ * uninitialized SurfacePipe.
+ *
+ * To avoid unnecessary allocations when creating SurfacePipe objects,
+ * NullSurfaceSink is a singleton. (This implies that the implementation must be
+ * stateless.)
+ */
+class NullSurfaceSink final : public SurfaceFilter
+{
+public:
+  /// Returns the singleton instance of NullSurfaceSink.
+  static NullSurfaceSink* Singleton();
+
+  virtual ~NullSurfaceSink() { }
+
+  nsresult Configure(const NullSurfaceConfig& aConfig);
+
+  Maybe<SurfaceInvalidRect> TakeInvalidRect() override { return Nothing(); }
+  uint8_t* AdvanceRow() override { return nullptr; }
+
+protected:
+  uint8_t* DoResetToFirstRow() override { return nullptr; }
+
+private:
+  static UniquePtr<NullSurfaceSink> sSingleton;  /// The singleton instance.
+};
+
+
+/**
+ * SurfacePipe is the public API that decoders should use to interact with a
+ * SurfaceFilter pipeline.
+ */
+class SurfacePipe
+{
+public:
+  /// Initialize global state used by all SurfacePipes.
+  static void Initialize() { NullSurfaceSink::Singleton(); }
+
+  SurfacePipe()
+    : mHead(NullSurfaceSink::Singleton())
+  { }
+
+  SurfacePipe(SurfacePipe&& aOther)
+    : mHead(Move(aOther.mHead))
+  { }
+
+  ~SurfacePipe()
+  {
+    // Ensure that we don't free the NullSurfaceSink singleton.
+    if (mHead.get() == NullSurfaceSink::Singleton()) {
+      Unused << mHead.release();
+    }
+  }
+
+  SurfacePipe& operator=(SurfacePipe&& aOther)
+  {
+    MOZ_ASSERT(this != &aOther);
+
+    // Ensure that we don't free the NullSurfaceSink singleton.
+    if (mHead.get() == NullSurfaceSink::Singleton()) {
+      Unused << mHead.release();
+    }
+
+    mHead = Move(aOther.mHead);
+    return *this;
+  }
+
+  /// Begins a new pass, seeking to the first row of the surface.
+  void ResetToFirstRow() { mHead->ResetToFirstRow(); }
+
+  /**
+   * Write pixels to the surface one at a time by repeatedly calling a lambda
+   * that yields pixels. WritePixels() should be preferred over WriteRows()
+   * whenever using it will not introduce additional copies or other performance
+   * penalties, because it is completely memory safe.
+   *
+   * @see SurfaceFilter::WritePixels() for the canonical documentation.
+   */
+  template <typename PixelType, typename Func>
+  WriteState WritePixels(Func aFunc)
+  {
+    return mHead->WritePixels<PixelType>(Forward<Func>(aFunc));
+  }
+
+  /**
+   * Write rows to the surface one at a time by repeatedly calling a lambda
+   * that yields rows. Because WriteRows() is not completely memory safe,
+   * WritePixels() should be preferred whenever it can be used without
+   * introducing additional copies or other performance penalties.
+   *
+   * @see SurfaceFilter::WriteRows() for the canonical documentation.
+   */
+  template <typename PixelType, typename Func>
+  WriteState WriteRows(Func aFunc)
+  {
+    return mHead->WriteRows<PixelType>(Forward<Func>(aFunc));
+  }
+
+  /// @return true if we've finished writing to the surface.
+  bool IsSurfaceFinished() const { return mHead->IsSurfaceFinished(); }
+
+  /// @see SurfaceFilter::TakeInvalidRect() for the canonical documentation.
+  Maybe<SurfaceInvalidRect> TakeInvalidRect() const
+  {
+    return mHead->TakeInvalidRect();
+  }
+
+private:
+  friend class SurfacePipeFactory;
+  friend class TestSurfacePipeFactory;
+
+  explicit SurfacePipe(UniquePtr<SurfaceFilter>&& aHead)
+    : mHead(Move(aHead))
+  { }
+
+  SurfacePipe(const SurfacePipe&) = delete;
+  SurfacePipe& operator=(const SurfacePipe&) = delete;
+
+  UniquePtr<SurfaceFilter> mHead;  /// The first filter in the chain.
+};
+
+/**
+ * AbstractSurfaceSink contains shared implementation for both SurfaceSink and
+ * PalettedSurfaceSink.
+ */
+class AbstractSurfaceSink : public SurfaceFilter
+{
+public:
+  AbstractSurfaceSink()
+    : mImageData(nullptr)
+    , mImageDataLength(0)
+    , mRow(0)
+    , mFlipVertically(false)
+  { }
+
+  Maybe<SurfaceInvalidRect> TakeInvalidRect() override final;
+  uint8_t* AdvanceRow() override final;
+
+protected:
+  uint8_t* DoResetToFirstRow() override final;
+  virtual uint8_t* GetRowPointer() const = 0;
+
+  gfx::IntRect mInvalidRect;  /// The region of the surface that has been written
+                              /// to since the last call to TakeInvalidRect().
+  uint8_t*  mImageData;       /// A pointer to the beginning of the surface data.
+  uint32_t  mImageDataLength; /// The length of the surface data.
+  uint32_t  mRow;             /// The row to which we're writing. (0-indexed)
+  bool      mFlipVertically;  /// If true, write the rows from top to bottom.
+};
+
+class SurfaceSink;
+
+/// A configuration struct for SurfaceSink.
+struct SurfaceConfig
+{
+  using Filter = SurfaceSink;
+  Decoder* mDecoder;           /// Which Decoder to use to allocate the surface.
+  uint32_t mFrameNum;          /// Which frame of animation this surface is for.
+  gfx::IntSize mOutputSize;    /// The size of the surface.
+  gfx::SurfaceFormat mFormat;  /// The surface format (BGRA or BGRX).
+  bool mFlipVertically;        /// If true, write the rows from bottom to top.
+};
+
+/**
+ * A sink for normal (i.e., non-paletted) surfaces. It handles the allocation of
+ * the surface and protects against buffer overflow. This sink should be used
+ * for all non-animated images and for the first frame of animated images.
+ *
+ * Sinks must always be at the end of the SurfaceFilter chain.
+ */
+class SurfaceSink final : public AbstractSurfaceSink
+{
+public:
+  nsresult Configure(const SurfaceConfig& aConfig);
+
+protected:
+  uint8_t* GetRowPointer() const override;
+};
+
+class PalettedSurfaceSink;
+
+struct PalettedSurfaceConfig
+{
+  using Filter = PalettedSurfaceSink;
+  Decoder* mDecoder;           /// Which Decoder to use to allocate the surface.
+  uint32_t mFrameNum;          /// Which frame of animation this surface is for.
+  gfx::IntSize mOutputSize;    /// The logical size of the surface.
+  gfx::IntRect mFrameRect;     /// The surface subrect which contains data.
+  gfx::SurfaceFormat mFormat;  /// The surface format (BGRA or BGRX).
+  uint8_t mPaletteDepth;       /// The palette depth of this surface.
+  bool mFlipVertically;        /// If true, write the rows from bottom to top.
+};
+
+/**
+ * A sink for paletted surfaces. It handles the allocation of the surface and
+ * protects against buffer overflow. This sink can be used for frames of
+ * animated images except the first.
+ *
+ * Sinks must always be at the end of the SurfaceFilter chain.
+ *
+ * XXX(seth): We'll remove all support for paletted surfaces in bug 1247520,
+ * which means we can remove PalettedSurfaceSink entirely.
+ */
+class PalettedSurfaceSink final : public AbstractSurfaceSink
+{
+public:
+  bool IsValidPalettedPipe() const override { return true; }
+
+  nsresult Configure(const PalettedSurfaceConfig& aConfig);
+
+protected:
+  uint8_t* GetRowPointer() const override;
+
+private:
+  /**
+   * The surface subrect which contains data. Note that the surface size we
+   * actually allocate is the size of the frame rect, not the logical size of
+   * the surface.
+   */
+  gfx::IntRect mFrameRect;
+};
+
+} // namespace image
+} // namespace mozilla
+
+#endif // mozilla_image_SurfacePipe_h
--- a/image/build/nsImageModule.cpp
+++ b/image/build/nsImageModule.cpp
@@ -8,16 +8,17 @@
 
 #include "mozilla/ModuleUtils.h"
 #include "nsMimeTypes.h"
 
 #include "DecodePool.h"
 #include "ImageFactory.h"
 #include "ShutdownTracker.h"
 #include "SurfaceCache.h"
+#include "SurfacePipe.h"
 
 #include "gfxPrefs.h"
 #include "imgLoader.h"
 #include "imgRequest.h"
 #include "imgRequestProxy.h"
 #include "imgTools.h"
 
 #include "nsICOEncoder.h"
@@ -89,16 +90,17 @@ mozilla::image::InitModule()
   MOZ_ASSERT(NS_IsMainThread());
   // Make sure the preferences are initialized
   gfxPrefs::GetSingleton();
 
   mozilla::image::ShutdownTracker::Initialize();
   mozilla::image::ImageFactory::Initialize();
   mozilla::image::DecodePool::Initialize();
   mozilla::image::SurfaceCache::Initialize();
+  mozilla::image::SurfacePipe::Initialize();
   imgLoader::GlobalInit();
   sInitialized = true;
   return NS_OK;
 }
 
 void
 mozilla::image::ShutdownModule()
 {
--- a/image/moz.build
+++ b/image/moz.build
@@ -67,16 +67,17 @@ UNIFIED_SOURCES += [
     'imgFrame.cpp',
     'imgTools.cpp',
     'MultipartImage.cpp',
     'OrientedImage.cpp',
     'ScriptedNotificationObserver.cpp',
     'ShutdownTracker.cpp',
     'SourceBuffer.cpp',
     'SurfaceCache.cpp',
+    'SurfacePipe.cpp',
     'SVGDocumentWrapper.cpp',
     'VectorImage.cpp',
 ]
 if CONFIG['MOZ_ENABLE_SKIA']:
     UNIFIED_SOURCES += [ 'Downscaler.cpp']
 
 # These files can't be unified because of ImageLogging.h #include order issues.
 SOURCES += [