Bug 1475139 part 11 - Add CrossProcessPaint implementation. r=mattwoodrow
authorRyan Hunt <rhunt@eqrion.net>
Mon, 24 Sep 2018 21:48:02 -0500
changeset 495737 d393cf125c13a93eb22e353da552377bb57eeb9e
parent 495736 3942a3b983a65155b928456d9cf4ed4aca447857
child 495738 4de4f6aaef1056ec2d7f3555f7afb1595fb0e4ed
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmattwoodrow
bugs1475139
milestone64.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 1475139 part 11 - Add CrossProcessPaint implementation. r=mattwoodrow This commit adds a CrossProcessPaint class which can be used to paint a cross process document tree. This API is async, as we cannot block on child processes, and initially geared towards servicing a JS API and not internal consumers. The API can only be used in the chrome process for security reasons. The class is implemented as a recursive resolver, requesting a root paint, gathering dependent frames to be painted, then requesting paints from those tabs. Once all paints have been completed, the dependency tree is rasterized in a bottom up fashion. Future improvements can be made here. Currently, the rasterization is performed on the main thread which could cause jank. We also transmit recordings directly over IPDl, and no effort is made to minimize the recordings from child layer trees. Differential Revision: https://phabricator.services.mozilla.com/D6790
dom/ipc/PBrowser.ipdl
dom/ipc/TabChild.cpp
dom/ipc/TabChild.h
dom/ipc/TabParent.cpp
dom/ipc/TabParent.h
gfx/ipc/CrossProcessPaint.cpp
gfx/ipc/CrossProcessPaint.h
gfx/ipc/GfxMessageUtils.h
gfx/ipc/moz.build
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -27,16 +27,17 @@ include JavaScriptTypes;
 include URIParams;
 include PPrintingTypes;
 include PTabContext;
 
 include "mozilla/GfxMessageUtils.h";
 include "mozilla/layers/LayersMessageUtils.h";
 
 using class IPC::Principal from "mozilla/dom/PermissionMessageUtils.h";
+using moveonly mozilla::gfx::PaintFragment from "mozilla/gfx/CrossProcessPaint.h";
 using mozilla::gfx::Matrix from "mozilla/gfx/Matrix.h";
 using mozilla::gfx::SurfaceFormat from "mozilla/gfx/Types.h";
 using mozilla::LayoutDeviceIntPoint from "Units.h";
 using mozilla::LayoutDevicePoint from "Units.h";
 using mozilla::ScreenIntPoint from "Units.h";
 using ScreenIntSize from "Units.h";
 using struct mozilla::layers::FrameMetrics from "FrameMetrics.h";
 using struct mozilla::layers::ScrollableLayerGuid from "FrameMetrics.h";
@@ -761,16 +762,19 @@ child:
      *        visible.
      * @param aEpoch
      *        The layer observer epoch for this activation. This message should be
      *        ignored if this epoch has already been observed (via
      *        PaintWhileInterruptingJS).
      */
     async RenderLayers(bool aEnabled, bool aForceRepaint, LayersObserverEpoch aEpoch);
 
+    async RequestRootPaint(IntRect aRect, float aScale, nscolor aBackgroundColor) returns (PaintFragment retval);
+    async RequestSubPaint(float aScale, nscolor aBackgroundColor) returns (PaintFragment retval);
+child:
     /**
      * Notify the child that it shouldn't paint the offscreen displayport.
      * This is useful to speed up interactive operations over async
      * scrolling performance like resize, tabswitch, pageload.
      *
      * Each enable call must be matched with a disable call. The child
      * will remain in the suppress mode as long as there's
      * a single unmatched call.
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -21,16 +21,17 @@
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/EventListenerManager.h"
 #include "mozilla/dom/DataTransfer.h"
 #include "mozilla/dom/Event.h"
 #include "mozilla/dom/indexedDB/PIndexedDBPermissionRequestChild.h"
 #include "mozilla/dom/MessageManagerBinding.h"
 #include "mozilla/dom/MouseEventBinding.h"
 #include "mozilla/dom/PaymentRequestChild.h"
+#include "mozilla/gfx/CrossProcessPaint.h"
 #include "mozilla/IMEStateManager.h"
 #include "mozilla/ipc/URIUtils.h"
 #include "mozilla/layers/APZChild.h"
 #include "mozilla/layers/APZCCallbackHelper.h"
 #include "mozilla/layers/APZCTreeManagerChild.h"
 #include "mozilla/layers/APZEventState.h"
 #include "mozilla/layers/ContentProcessController.h"
 #include "mozilla/layers/CompositorBridgeChild.h"
@@ -2663,16 +2664,41 @@ TabChild::RecvRenderLayers(const bool& a
 
     MakeHidden();
   }
 
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
+TabChild::RecvRequestRootPaint(const IntRect& aRect, const float& aScale, const nscolor& aBackgroundColor, RequestRootPaintResolver&& aResolve)
+{
+  nsCOMPtr<nsIDocShell> docShell = do_GetInterface(WebNavigation());
+  if (!docShell) {
+    return IPC_OK();
+  }
+
+  aResolve(gfx::PaintFragment::Record(docShell, aRect, aScale, aBackgroundColor));
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+TabChild::RecvRequestSubPaint(const float& aScale, const nscolor& aBackgroundColor, RequestSubPaintResolver&& aResolve)
+{
+  nsCOMPtr<nsIDocShell> docShell = do_GetInterface(WebNavigation());
+  if (!docShell) {
+    return IPC_OK();
+  }
+
+  gfx::IntRect rect = gfx::RoundedIn(gfx::Rect(0.0f, 0.0f, mUnscaledInnerSize.width, mUnscaledInnerSize.height));
+  aResolve(gfx::PaintFragment::Record(docShell, rect, aScale, aBackgroundColor));
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
 TabChild::RecvNavigateByKey(const bool& aForward, const bool& aForDocumentNavigation)
 {
   nsIFocusManager* fm = nsFocusManager::GetFocusManager();
   if (fm) {
     RefPtr<Element> result;
     nsCOMPtr<nsPIDOMWindowOuter> window = do_GetInterface(WebNavigation());
 
     // Move to the first or last document.
--- a/dom/ipc/TabChild.h
+++ b/dom/ipc/TabChild.h
@@ -719,16 +719,20 @@ protected:
   virtual bool DeallocPRenderFrameChild(PRenderFrameChild* aFrame) override;
 
   virtual mozilla::ipc::IPCResult RecvDestroy() override;
 
   virtual mozilla::ipc::IPCResult RecvSetDocShellIsActive(const bool& aIsActive) override;
 
   virtual mozilla::ipc::IPCResult RecvRenderLayers(const bool& aEnabled, const bool& aForce, const layers::LayersObserverEpoch& aEpoch) override;
 
+  virtual mozilla::ipc::IPCResult RecvRequestRootPaint(const IntRect& aRect, const float& aScale, const nscolor& aBackgroundColor, RequestRootPaintResolver&& aResolve) override;
+
+  virtual mozilla::ipc::IPCResult RecvRequestSubPaint(const float& aScale, const nscolor& aBackgroundColor, RequestSubPaintResolver&& aResolve) override;
+
   virtual mozilla::ipc::IPCResult RecvNavigateByKey(const bool& aForward,
                                                     const bool& aForDocumentNavigation) override;
 
   virtual mozilla::ipc::IPCResult RecvRequestNotifyAfterRemotePaint() override;
 
   virtual mozilla::ipc::IPCResult RecvSuppressDisplayport(const bool& aEnabled) override;
 
   virtual mozilla::ipc::IPCResult RecvParentActivated(const bool& aActivated) override;
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -3093,16 +3093,48 @@ TabParent::LayerTreeUpdate(const LayersO
   } else {
     event->InitEvent(NS_LITERAL_STRING("MozLayerTreeCleared"), true, false);
   }
   event->SetTrusted(true);
   event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
   mFrameElement->DispatchEvent(*event);
 }
 
+void
+TabParent::RequestRootPaint(gfx::CrossProcessPaint* aPaint, IntRect aRect, float aScale, nscolor aBackgroundColor)
+{
+  auto promise = SendRequestRootPaint(aRect, aScale, aBackgroundColor);
+
+  RefPtr<gfx::CrossProcessPaint> paint(aPaint);
+  TabId tabId(GetTabId());
+  promise->Then(GetMainThreadSerialEventTarget(), __func__,
+                [paint, tabId] (PaintFragment&& aFragment) {
+                  paint->ReceiveFragment(tabId, std::move(aFragment));
+                },
+                [paint, tabId] (ResponseRejectReason aReason) {
+                  paint->LostFragment(tabId);
+                });
+}
+
+void
+TabParent::RequestSubPaint(gfx::CrossProcessPaint* aPaint, float aScale, nscolor aBackgroundColor)
+{
+  auto promise = SendRequestSubPaint(aScale, aBackgroundColor);
+
+  RefPtr<gfx::CrossProcessPaint> paint(aPaint);
+  TabId tabId(GetTabId());
+  promise->Then(GetMainThreadSerialEventTarget(), __func__,
+                [paint, tabId] (PaintFragment&& aFragment) {
+                  paint->ReceiveFragment(tabId, std::move(aFragment));
+                },
+                [paint, tabId] (ResponseRejectReason aReason) {
+                  paint->LostFragment(tabId);
+                });
+}
+
 mozilla::ipc::IPCResult
 TabParent::RecvPaintWhileInterruptingJSNoOp(const LayersObserverEpoch& aEpoch)
 {
   // We sent a PaintWhileInterruptingJS message when layers were already visible. In this
   // case, we should act as if an update occurred even though we already have
   // the layers.
   LayerTreeUpdate(aEpoch, true);
   return IPC_OK();
--- a/dom/ipc/TabParent.h
+++ b/dom/ipc/TabParent.h
@@ -12,16 +12,17 @@
 #include "mozilla/ContentCache.h"
 #include "mozilla/dom/ipc/IdType.h"
 #include "mozilla/dom/PBrowserParent.h"
 #include "mozilla/dom/PContent.h"
 #include "mozilla/dom/PFilePickerParent.h"
 #include "mozilla/dom/TabContext.h"
 #include "mozilla/EventForwards.h"
 #include "mozilla/dom/File.h"
+#include "mozilla/gfx/CrossProcessPaint.h"
 #include "mozilla/layers/CompositorBridgeParent.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/Move.h"
 #include "nsCOMPtr.h"
 #include "nsIAuthPromptProvider.h"
 #include "nsIBrowserDOMWindow.h"
 #include "nsIDOMEventListener.h"
 #include "nsIKeyEventInPluginCallback.h"
@@ -557,16 +558,19 @@ public:
 
   bool IsInitedByParent() const { return mInitedByParent; }
 
   bool SendLoadRemoteScript(const nsString& aURL,
                             const bool& aRunInGlobalScope);
 
   void LayerTreeUpdate(const LayersObserverEpoch& aEpoch, bool aActive);
 
+  void RequestRootPaint(gfx::CrossProcessPaint* aPaint, IntRect aRect, float aScale, nscolor aBackgroundColor);
+  void RequestSubPaint(gfx::CrossProcessPaint* aPaint, float aScale, nscolor aBackgroundColor);
+
   virtual mozilla::ipc::IPCResult
   RecvInvokeDragSession(nsTArray<IPCDataTransfer>&& aTransfers,
                         const uint32_t& aAction,
                         const OptionalShmem& aVisualDnDData,
                         const uint32_t& aStride, const gfx::SurfaceFormat& aFormat,
                         const LayoutDeviceIntRect& aDragRect,
                         const nsCString& aPrincipalURISpec) override;
 
new file mode 100644
--- /dev/null
+++ b/gfx/ipc/CrossProcessPaint.cpp
@@ -0,0 +1,427 @@
+/* -*- 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 "CrossProcessPaint.h"
+
+#include "mozilla/dom/ContentProcessManager.h"
+#include "mozilla/dom/ImageBitmap.h"
+#include "mozilla/dom/TabParent.h"
+#include "mozilla/gfx/DrawEventRecorder.h"
+#include "mozilla/gfx/InlineTranslator.h"
+#include "mozilla/PresShell.h"
+
+#include "gfxPlatform.h"
+
+#include "nsContentUtils.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIDocShell.h"
+#include "nsPresContext.h"
+
+#define ENABLE_PAINT_LOG 0
+// #define ENABLE_PAINT_LOG 1
+
+#if ENABLE_PAINT_LOG
+#  define PF_LOG(...) printf_stderr("PaintFragment: " __VA_ARGS__)
+#  define CPP_LOG(...) printf_stderr("CrossProcessPaint: " __VA_ARGS__)
+#else
+#  define PF_LOG(...)
+#  define CPP_LOG(...)
+#endif
+
+namespace mozilla {
+namespace gfx {
+
+using namespace mozilla::ipc;
+
+/// The minimum scale we allow tabs to be rasterized at.
+static const float kMinPaintScale = 0.05;
+
+/* static */ PaintFragment
+PaintFragment::Record(nsIDocShell* aDocShell,
+                      const IntRect& aRect,
+                      float aScale,
+                      nscolor aBackgroundColor)
+{
+  IntSize surfaceSize = aRect.Size();
+  surfaceSize.width *= aScale;
+  surfaceSize.height *= aScale;
+
+  CPP_LOG("Recording "
+          "[docshell=%p, "
+          "rect=(%d, %d) x (%d, %d), "
+          "scale=%f, "
+          "color=(%u, %u, %u, %u)]\n",
+    aDocShell,
+    aRect.x, aRect.y, aRect.width, aRect.height,
+    aScale,
+    NS_GET_R(aBackgroundColor),
+    NS_GET_G(aBackgroundColor),
+    NS_GET_B(aBackgroundColor),
+    NS_GET_A(aBackgroundColor));
+
+  // Check for invalid sizes
+  if (surfaceSize.width <= 0 || surfaceSize.height <= 0 ||
+      !Factory::CheckSurfaceSize(surfaceSize)) {
+    PF_LOG("Invalid surface size of (%d x %d).\n",
+      surfaceSize.width,
+      surfaceSize.height);
+    return PaintFragment{};
+  }
+
+  // Flush any pending notifications
+  nsContentUtils::FlushLayoutForTree(aDocShell->GetWindow());
+
+  // Grab the presentation shell to render
+  RefPtr<nsPresContext> presContext;
+  if (aDocShell) {
+    aDocShell->GetPresContext(getter_AddRefs(presContext));
+  }
+  if (!presContext) {
+    PF_LOG("Couldn't find PresContext.\n");
+    return PaintFragment{};
+  }
+
+  // Initialize the recorder
+  SurfaceFormat format = SurfaceFormat::B8G8R8A8;
+  RefPtr<DrawTarget> referenceDt =
+    Factory::CreateDrawTarget(gfxPlatform::GetPlatform()->GetSoftwareBackend(),
+                              IntSize(1, 1),
+                              format);
+
+  // TODO: This may OOM crash if the content is complex enough
+  RefPtr<DrawEventRecorderMemory> recorder =
+    MakeAndAddRef<DrawEventRecorderMemory>(nullptr);
+  RefPtr<DrawTarget> dt =
+    Factory::CreateRecordingDrawTarget(recorder, referenceDt, surfaceSize);
+
+  // Perform the actual rendering
+  {
+    nsRect r(nsPresContext::CSSPixelsToAppUnits(aRect.x),
+             nsPresContext::CSSPixelsToAppUnits(aRect.y),
+             nsPresContext::CSSPixelsToAppUnits(aRect.width),
+             nsPresContext::CSSPixelsToAppUnits(aRect.height));
+
+    RefPtr<gfxContext> thebes = gfxContext::CreateOrNull(dt);
+    thebes->SetMatrix(Matrix::Scaling(aScale, aScale));
+    nsCOMPtr<nsIPresShell> shell = presContext->PresShell();
+    Unused << shell->RenderDocument(r, 0, aBackgroundColor, thebes);
+  }
+
+  ByteBuf recording = ByteBuf((uint8_t*)recorder->mOutputStream.mData,
+                              recorder->mOutputStream.mLength,
+                              recorder->mOutputStream.mCapacity);
+  recorder->mOutputStream.mData = nullptr;
+  recorder->mOutputStream.mLength = 0;
+  recorder->mOutputStream.mCapacity = 0;
+
+  return PaintFragment{
+    surfaceSize,
+    std::move(recording),
+    std::move(recorder->TakeDependentSurfaces()),
+  };
+}
+
+bool
+PaintFragment::IsEmpty() const
+{
+  return !mRecording.mData || mRecording.mLen == 0 || mSize == IntSize(0, 0);
+}
+
+PaintFragment::PaintFragment(IntSize aSize,
+                             ByteBuf&& aRecording,
+                             nsTHashtable<nsUint64HashKey>&& aDependencies)
+  : mSize(aSize)
+  , mRecording(std::move(aRecording))
+  , mDependencies(std::move(aDependencies))
+{
+}
+
+/* static */ void
+CrossProcessPaint::StartLocal(nsIDocShell* aRoot,
+                              const IntRect& aRect,
+                              float aScale,
+                              nscolor aBackgroundColor,
+                              dom::Promise* aPromise)
+{
+  MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+  aScale = std::max(aScale, kMinPaintScale);
+
+  CPP_LOG("Starting local paint. "
+          "[docshell=%p, "
+          "rect=(%d, %d) x (%d, %d), "
+          "scale=%f, "
+          "color=(%u, %u, %u, %u)]\n",
+    aRoot,
+    aRect.x, aRect.y, aRect.width, aRect.height,
+    aScale,
+    NS_GET_R(aBackgroundColor),
+    NS_GET_G(aBackgroundColor),
+    NS_GET_B(aBackgroundColor),
+    NS_GET_A(aBackgroundColor));
+
+  RefPtr<CrossProcessPaint> resolver = new CrossProcessPaint(aPromise,
+                                                             aScale,
+                                                             aBackgroundColor,
+                                                             dom::TabId(0));
+  resolver->ReceiveFragment(dom::TabId(0),
+                            PaintFragment::Record(aRoot,
+                                                  aRect,
+                                                  aScale,
+                                                  aBackgroundColor));
+}
+
+/* static */ void
+CrossProcessPaint::StartRemote(dom::TabId aRoot,
+                               const IntRect& aRect,
+                               float aScale,
+                               nscolor aBackgroundColor,
+                               dom::Promise* aPromise)
+{
+  MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+  aScale = std::max(aScale, kMinPaintScale);
+
+  CPP_LOG("Starting remote paint. "
+          "[tab=%llu, "
+          "rect=(%d, %d) x (%d, %d), "
+          "scale=%f, "
+          "color=(%u, %u, %u, %u)]\n",
+    (uint64_t)aRoot,
+    aRect.x, aRect.y, aRect.width, aRect.height,
+    aScale,
+    NS_GET_R(aBackgroundColor),
+    NS_GET_G(aBackgroundColor),
+    NS_GET_B(aBackgroundColor),
+    NS_GET_A(aBackgroundColor));
+
+  RefPtr<CrossProcessPaint> resolver = new CrossProcessPaint(aPromise,
+                                                             aScale,
+                                                             aBackgroundColor,
+                                                             aRoot);
+  resolver->QueueRootPaint(aRoot, aRect, aScale, aBackgroundColor);
+}
+
+CrossProcessPaint::CrossProcessPaint(dom::Promise* aPromise,
+                                     float aScale,
+                                     nscolor aBackgroundColor,
+                                     dom::TabId aRootId)
+    : mPromise{aPromise}
+    , mRootId{aRootId}
+    , mScale{aScale}
+    , mBackgroundColor{aBackgroundColor}
+    , mPendingFragments{1}
+{
+}
+
+CrossProcessPaint::~CrossProcessPaint()
+{
+}
+
+void
+CrossProcessPaint::ReceiveFragment(dom::TabId aId, PaintFragment&& aFragment)
+{
+  if (IsCleared()) {
+    CPP_LOG("Ignoring fragment from %llu.\n", (uint64_t)aId);
+    return;
+  }
+
+  MOZ_ASSERT(mPendingFragments > 0);
+  MOZ_ASSERT(!mReceivedFragments.GetValue(aId));
+  MOZ_ASSERT(!aFragment.IsEmpty());
+
+  // Double check our invariants to protect against a compromised content
+  // process
+  if (mPendingFragments == 0 ||
+      mReceivedFragments.GetValue(aId) ||
+      aFragment.IsEmpty()) {
+    CPP_LOG("Dropping invalid fragment from %llu.\n", (uint64_t)aId);
+    LostFragment(aId);
+    return;
+  }
+
+  CPP_LOG("Receiving fragment from %llu.\n", (uint64_t)aId);
+
+  // Queue paints for child tabs
+  for (auto iter = aFragment.mDependencies.Iter(); !iter.Done(); iter.Next()) {
+    auto dependency = iter.Get()->GetKey();
+    QueueSubPaint(dom::TabId(dependency));
+  }
+
+  mReceivedFragments.Put(aId, std::move(aFragment));
+  mPendingFragments -= 1;
+
+  // Resolve this paint if we have received all pending fragments
+  MaybeResolve();
+}
+
+void
+CrossProcessPaint::LostFragment(dom::TabId aId)
+{
+  if (IsCleared()) {
+    CPP_LOG("Ignoring lost fragment from %llu.\n", (uint64_t)aId);
+    return;
+  }
+
+  mPromise->MaybeReject(NS_ERROR_FAILURE);
+  Clear();
+}
+
+void
+CrossProcessPaint::QueueRootPaint(dom::TabId aId,
+                                  const IntRect& aRect,
+                                  float aScale,
+                                  nscolor aBackgroundColor)
+{
+  MOZ_ASSERT(!mReceivedFragments.GetValue(aId));
+  MOZ_ASSERT(mPendingFragments == 1);
+
+  CPP_LOG("Queueing root paint for %llu.\n", (uint64_t)aId);
+
+  dom::ContentProcessManager* cpm = dom::ContentProcessManager::GetSingleton();
+
+  dom::ContentParentId cpId = cpm->GetTabProcessId(aId);
+  RefPtr<dom::TabParent> tab = cpm->GetTabParentByProcessAndTabId(cpId, aId);
+  tab->RequestRootPaint(this, aRect, aScale, aBackgroundColor);
+
+  // This will always be the first paint, so the constructor will already have
+  // incremented one pending fragment
+}
+
+void
+CrossProcessPaint::QueueSubPaint(dom::TabId aId)
+{
+  MOZ_ASSERT(!mReceivedFragments.GetValue((uint64_t)aId));
+
+  CPP_LOG("Queueing sub paint for %llu.\n", (uint64_t)aId);
+
+  dom::ContentProcessManager* cpm = dom::ContentProcessManager::GetSingleton();
+
+  dom::ContentParentId cpId = cpm->GetTabProcessId(aId);
+  RefPtr<dom::TabParent> tab = cpm->GetTabParentByProcessAndTabId(cpId, aId);
+  tab->RequestSubPaint(this, mScale, mBackgroundColor);
+
+  mPendingFragments += 1;
+}
+
+void
+CrossProcessPaint::Clear()
+{
+  mPromise = nullptr;
+  mPendingFragments = 0;
+  mReceivedFragments.Clear();
+}
+
+bool
+CrossProcessPaint::IsCleared() const
+{
+  return !mPromise;
+}
+
+void
+CrossProcessPaint::MaybeResolve()
+{
+  // Don't do anything if we aren't ready, experienced an error, or already
+  // resolved this paint
+  if (IsCleared() || mPendingFragments > 0) {
+    CPP_LOG("Not ready to resolve yet, have %u fragments left.\n",
+            mPendingFragments);
+    return;
+  }
+
+  CPP_LOG("Starting to resolve fragments.\n");
+
+  // Resolve the paint fragments from the bottom up
+  ResolvedSurfaceMap resolved;
+  if (!ResolveInternal(mRootId, &resolved)) {
+    CPP_LOG("Couldn't resolve.\n");
+
+    mPromise->MaybeReject(NS_ERROR_FAILURE);
+    Clear();
+    return;
+  }
+
+  // Grab the result from the resolved table
+  RefPtr<SourceSurface> root = resolved.Get(mRootId);
+  CPP_LOG("Resolved all fragments.\n");
+
+  ErrorResult rv;
+  RefPtr<dom::ImageBitmap> bitmap =
+    dom::ImageBitmap::CreateFromSourceSurface(mPromise->GetParentObject(),
+                                              root,
+                                              rv);
+
+  if (!rv.Failed()) {
+    CPP_LOG("Success, fulfilling promise.\n");
+    mPromise->MaybeResolve(bitmap);
+  } else {
+    CPP_LOG("Couldn't create ImageBitmap for SourceSurface.\n");
+    mPromise->MaybeReject(rv);
+  }
+  Clear();
+}
+
+bool
+CrossProcessPaint::ResolveInternal(dom::TabId aId,
+                                   ResolvedSurfaceMap* aResolved)
+{
+  // We should not have resolved this paint already
+  MOZ_ASSERT(!aResolved->GetWeak(aId));
+
+  CPP_LOG("Resolving fragment %llu.\n", (uint64_t)aId);
+
+  Maybe<PaintFragment> fragment = mReceivedFragments.GetAndRemove(aId);
+
+  // Rasterize all the dependencies first so that we can resolve this fragment
+  for (auto iter = fragment->mDependencies.Iter(); !iter.Done(); iter.Next()) {
+    auto dependency = iter.Get()->GetKey();
+    if (!ResolveInternal(dom::TabId(dependency), aResolved)) {
+      return false;
+    }
+  }
+
+  // Create the destination draw target
+  RefPtr<DrawTarget> drawTarget =
+    gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(fragment->mSize,
+                                                                 SurfaceFormat::B8G8R8A8);
+  if (!drawTarget || !drawTarget->IsValid()) {
+    CPP_LOG("Couldn't create (%d x %d) surface for fragment %llu.\n",
+      fragment->mSize.width,
+      fragment->mSize.height,
+      (uint64_t)aId);
+    return false;
+  }
+
+  // Translate the recording using our child tabs
+  {
+    InlineTranslator translator(drawTarget, nullptr);
+    translator.SetExternalSurfaces(aResolved);
+    if (!translator.TranslateRecording((char*)fragment->mRecording.mData,
+                                       fragment->mRecording.mLen)) {
+      CPP_LOG("Couldn't translate recording for fragment %llu.\n",
+        (uint64_t)aId);
+      return false;
+    }
+  }
+
+  RefPtr<SourceSurface> snapshot = drawTarget->Snapshot();
+  if (!snapshot) {
+    CPP_LOG("Couldn't get snapshot for fragment %llu.\n",
+      (uint64_t)aId);
+    return false;
+  }
+
+  // We are done with the resolved images of our dependencies, let's remove
+  // them
+  for (auto iter = fragment->mDependencies.Iter(); !iter.Done(); iter.Next()) {
+    auto dependency = iter.Get()->GetKey();
+    aResolved->Remove(dependency);
+  }
+
+  aResolved->Put(aId, snapshot);
+  return true;
+}
+
+} // namespace gfx
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/gfx/ipc/CrossProcessPaint.h
@@ -0,0 +1,170 @@
+/* -*- 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/. */
+#ifndef _include_mozilla_gfx_ipc_CrossProcessPaint_h_
+#define _include_mozilla_gfx_ipc_CrossProcessPaint_h_
+
+#include "nsISupportsImpl.h"
+
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/ipc/ByteBuf.h"
+#include "nsDataHashtable.h"
+#include "nsHashKeys.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTHashtable.h"
+
+namespace IPC {
+template<typename T> struct ParamTraits;
+} // namespace IPC
+
+namespace mozilla {
+namespace gfx {
+
+class CrossProcessPaint;
+
+/**
+ * A fragment of a paint of a cross process document tree.
+ */
+class PaintFragment
+{
+public:
+  /// Initializes an empty PaintFragment
+  PaintFragment() = default;
+
+  /**
+   * Creates a paint fragment by recording the draw commands and dependent tabs
+   * for an nsIDocShell.
+   *
+   * @param aDocShell The document shell to record.
+   * @param aRect The rectangle relative to the viewport to use.
+   * @param aScale The coordinate scale to use. The size of the resolved
+   *   surface will be `aRect.Size() * aScale`, with aScale clamped to
+   *   at least kMinPaintScale.
+   * @param aBackgroundColor The background color to use.
+   *
+   * @return A paint fragment. The paint fragment may be `empty` if rendering
+   *         was unable to be accomplished for some reason.
+   */
+  static PaintFragment Record(nsIDocShell* aDocShell,
+                              const IntRect& aRect,
+                              float aScale,
+                              nscolor aBackgroundColor);
+
+  /// Returns whether this paint fragment contains a valid recording.
+  bool IsEmpty() const;
+
+  PaintFragment(PaintFragment&&) = default;
+  PaintFragment& operator=(PaintFragment&&) = default;
+
+protected:
+  friend struct IPC::ParamTraits<PaintFragment>;
+  friend CrossProcessPaint;
+
+  typedef mozilla::ipc::ByteBuf ByteBuf;
+
+  PaintFragment(IntSize, ByteBuf&&, nsTHashtable<nsUint64HashKey>&&);
+
+  IntSize mSize;
+  ByteBuf mRecording;
+  nsTHashtable<nsUint64HashKey> mDependencies;
+};
+
+/**
+ * An object for painting a cross process document tree.
+ */
+class CrossProcessPaint
+{
+  NS_INLINE_DECL_REFCOUNTING(CrossProcessPaint);
+
+public:
+  /**
+   * Begin an asynchronous paint of a cross process document tree starting at
+   * a local document shell. The local document will be painted, then async
+   * paints will be queued for remote subframes. Once all subframes have been
+   * recorded, the final image will be resolved, and the promise will be
+   * resolved with a dom::ImageBitmap.
+   *
+   * @param aDocShell The document shell to paint.
+   * @param aRect The rectangle relative to the viewport to use.
+   * @param aScale The coordinate scale to use. The size of the resolved
+   *   surface will be `aRect.Size() * aScale`, with aScale clamped to
+   *   at least kMinPaintScale. See the implementation for the current
+   *   minimum value.
+   * @param aBackgroundColor The background color to use.
+   * @param aPromise The promise to resolve with a dom::ImageBitmap.
+   */
+  static void StartLocal(nsIDocShell* aRoot,
+                         const IntRect& aRect,
+                         float aScale,
+                         nscolor aBackgroundColor,
+                         dom::Promise* aPromise);
+
+  /**
+   * Begin an asynchronous paint of a cross process document tree starting at
+   * a remote tab. An async paint for the remote tab will be queued, then async
+   * paints will be recursively queued for remote subframes. Once all subframes
+   * have been recorded, the final image will be resolved, and the promise will
+   * be resolved with a dom::ImageBitmap.
+   *
+   * @param aDocShell The document shell to paint.
+   * @param aRect The rectangle relative to the viewport to use.
+   * @param aScale The coordinate scale to use. The size of the resolved
+   *   surface will be `aRect.Size() * aScale`, with aScale clamped to
+   *   at least kMinPaintScale. See the implementation for the current
+   *   minimum value.
+   * @param aBackgroundColor The background color to use.
+   * @param aPromise The promise to resolve with a dom::ImageBitmap.
+   */
+  static void StartRemote(dom::TabId aRoot,
+                          const IntRect& aRect,
+                          float aScale,
+                          nscolor aBackgroundColor,
+                          dom::Promise* aPromise);
+
+  void ReceiveFragment(dom::TabId aId, PaintFragment&& aFragment);
+  void LostFragment(dom::TabId aId);
+private:
+  typedef nsRefPtrHashtable<nsUint64HashKey, SourceSurface> ResolvedSurfaceMap;
+  typedef nsDataHashtable<nsUint64HashKey, PaintFragment> ReceivedFragmentMap;
+
+  CrossProcessPaint(dom::Promise* aPromise,
+                    float aScale,
+                    nscolor aBackgroundColor,
+                    dom::TabId aRootId);
+  ~CrossProcessPaint();
+
+  void QueueRootPaint(dom::TabId aId,
+                      const IntRect& aRect,
+                      float aScale,
+                      nscolor aBackgroundColor);
+  void QueueSubPaint(dom::TabId aId);
+
+  /// Clear the state of this paint so that it cannot be resolved or receive
+  /// any paint fragments.
+  void Clear();
+
+  /// Returns if this paint has been cleared.
+  bool IsCleared() const;
+
+  /// Resolves the paint fragments if we have none pending and resolves the
+  /// promise.
+  void MaybeResolve();
+  bool ResolveInternal(dom::TabId aId,
+                       ResolvedSurfaceMap* aResolved);
+
+  RefPtr<dom::Promise> mPromise;
+  dom::TabId mRootId;
+  float mScale;
+  nscolor mBackgroundColor;
+  uint32_t mPendingFragments;
+  ReceivedFragmentMap mReceivedFragments;
+};
+
+} // namespace gfx
+} // namespace mozilla
+
+#endif // _include_mozilla_gfx_ipc_CrossProcessPaint_h_
--- a/gfx/ipc/GfxMessageUtils.h
+++ b/gfx/ipc/GfxMessageUtils.h
@@ -15,16 +15,17 @@
 #include "chrome/common/ipc_message_utils.h"
 #include "gfxFeature.h"
 #include "gfxFallback.h"
 #include "gfxPoint.h"
 #include "gfxRect.h"
 #include "gfxTelemetry.h"
 #include "gfxTypes.h"
 #include "ipc/IPCMessageUtils.h"
+#include "mozilla/gfx/CrossProcessPaint.h"
 #include "mozilla/gfx/Matrix.h"
 #include "nsRect.h"
 #include "nsRegion.h"
 #include "mozilla/Array.h"
 
 #include <stdint.h>
 
 #ifdef _MSC_VER
@@ -1274,11 +1275,28 @@ struct ParamTraits<mozilla::Array<T, Len
       if (!ReadParam<T>(aMsg, aIter, &aResult->operator[](i))) {
         return false;
       }
     }
     return true;
   }
 };
 
+template<>
+struct ParamTraits<mozilla::gfx::PaintFragment>
+{
+  typedef mozilla::gfx::PaintFragment paramType;
+  static void Write(Message* aMsg, paramType& aParam) {
+    WriteParam(aMsg, aParam.mSize);
+    WriteParam(aMsg, aParam.mRecording);
+    WriteParam(aMsg, aParam.mDependencies);
+  }
+
+  static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult) {
+    return ReadParam(aMsg, aIter, &aResult->mSize) &&
+           ReadParam(aMsg, aIter, &aResult->mRecording) &&
+           ReadParam(aMsg, aIter, &aResult->mDependencies);
+  }
+};
+
 } /* namespace IPC */
 
 #endif /* __GFXMESSAGEUTILS_H__ */
--- a/gfx/ipc/moz.build
+++ b/gfx/ipc/moz.build
@@ -8,16 +8,17 @@ with Files('**'):
     BUG_COMPONENT = ('Core', 'Graphics: Layers')
 
 EXPORTS.mozilla += [
     'D3DMessageUtils.h',
     'GfxMessageUtils.h'
 ]
 
 EXPORTS.mozilla.gfx += [
+    'CrossProcessPaint.h',
     'GPUChild.h',
     'GPUParent.h',
     'GPUProcessHost.h',
     'GPUProcessImpl.h',
     'GPUProcessListener.h',
     'GPUProcessManager.h',
     'SharedDIB.h',
     'VsyncBridgeChild.h',
@@ -44,16 +45,17 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'wind
     UNIFIED_SOURCES += [
         'SharedDIBSurface.cpp',
         'SharedDIBWin.cpp',
     ]
 
 UNIFIED_SOURCES += [
     'CompositorSession.cpp',
     'CompositorWidgetVsyncObserver.cpp',
+    'CrossProcessPaint.cpp',
     'D3DMessageUtils.cpp',
     'GPUChild.cpp',
     'GPUProcessHost.cpp',
     'GPUProcessImpl.cpp',
     'GPUProcessManager.cpp',
     'InProcessCompositorSession.cpp',
     'RemoteCompositorSession.cpp',
     'SharedDIB.cpp',