layout/painting/RetainedDisplayListBuilder.cpp
author Csoregi Natalia <ncsoregi@mozilla.com>
Fri, 10 May 2019 14:21:07 +0300
changeset 532195 b86c8998a2b246e0c5f13b32481ace92f1be0d99
parent 532162 7c1fce459b7aa1c21e92331ce7ddbf05a7a49f58
child 532713 2c73bd0fae85c2e877932e9f98ffd28fe91538f1
permissions -rw-r--r--
Backed out changeset 7c1fce459b7a (bug 1549909) for crashtest failures on 1458028.html. CLOSED TREE

/* -*- 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 "RetainedDisplayListBuilder.h"

#include "DisplayListChecker.h"
#include "gfxPrefs.h"
#include "nsPlaceholderFrame.h"
#include "nsSubDocumentFrame.h"
#include "nsViewManager.h"
#include "nsCanvasFrame.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/PresShell.h"

/**
 * Code for doing display list building for a modified subset of the window,
 * and then merging it into the existing display list (for the full window).
 *
 * The approach primarily hinges on the observation that the 'true' ordering
 * of display items is represented by a DAG (only items that intersect in 2d
 * space have a defined ordering). Our display list is just one of a many
 * possible linear representations of this ordering.
 *
 * Each time a frame changes (gets a new ComputedStyle, or has a size/position
 * change), we schedule a paint (as we do currently), but also reord the frame
 * that changed.
 *
 * When the next paint occurs we union the overflow areas (in screen space) of
 * the changed frames, and compute a rect/region that contains all changed
 * items. We then build a display list just for this subset of the screen and
 * merge it into the display list from last paint.
 *
 * Any items that exist in one list and not the other must not have a defined
 * ordering in the DAG, since they need to intersect to have an ordering and
 * we would have built both in the new list if they intersected. Given that, we
 * can align items that appear in both lists, and any items that appear between
 * matched items can be inserted into the merged list in any order.
 */

using namespace mozilla;
using mozilla::dom::Document;

void RetainedDisplayListData::AddModifiedFrame(nsIFrame* aFrame) {
  MOZ_ASSERT(!aFrame->IsFrameModified());
  Flags(aFrame) |= RetainedDisplayListData::FrameFlags::Modified;
  mModifiedFramesCount++;
}

RetainedDisplayListData* GetRetainedDisplayListData(nsIFrame* aRootFrame) {
  RetainedDisplayListData* data =
      aRootFrame->GetProperty(RetainedDisplayListData::DisplayListData());

  return data;
}

RetainedDisplayListData* GetOrSetRetainedDisplayListData(nsIFrame* aRootFrame) {
  RetainedDisplayListData* data = GetRetainedDisplayListData(aRootFrame);

  if (!data) {
    data = new RetainedDisplayListData();
    aRootFrame->SetProperty(RetainedDisplayListData::DisplayListData(), data);
  }

  MOZ_ASSERT(data);
  return data;
}

static void MarkFramesWithItemsAndImagesModified(nsDisplayList* aList) {
  for (nsDisplayItem* i = aList->GetBottom(); i != nullptr; i = i->GetAbove()) {
    if (!i->HasDeletedFrame() && i->CanBeReused() &&
        !i->Frame()->IsFrameModified()) {
      // If we have existing cached geometry for this item, then check that for
      // whether we need to invalidate for a sync decode. If we don't, then
      // use the item's flags.
      DisplayItemData* data = FrameLayerBuilder::GetOldDataFor(i);
      // XXX: handle webrender case
      bool invalidate = false;
      if (data && data->GetGeometry()) {
        invalidate = data->GetGeometry()->InvalidateForSyncDecodeImages();
      } else if (!(i->GetFlags() & TYPE_RENDERS_NO_IMAGES)) {
        invalidate = true;
      }

      if (invalidate) {
        i->FrameForInvalidation()->MarkNeedsDisplayItemRebuild();
        if (i->GetDependentFrame()) {
          i->GetDependentFrame()->MarkNeedsDisplayItemRebuild();
        }
      }
    }
    if (i->GetChildren()) {
      MarkFramesWithItemsAndImagesModified(i->GetChildren());
    }
  }
}

static AnimatedGeometryRoot* SelectAGRForFrame(
    nsIFrame* aFrame, AnimatedGeometryRoot* aParentAGR) {
  if (!aFrame->IsStackingContext() || !aFrame->IsFixedPosContainingBlock()) {
    return aParentAGR;
  }

  if (!aFrame->HasOverrideDirtyRegion()) {
    return nullptr;
  }

  nsDisplayListBuilder::DisplayListBuildingData* data =
      aFrame->GetProperty(nsDisplayListBuilder::DisplayListBuildingRect());

  return data && data->mModifiedAGR ? data->mModifiedAGR.get() : nullptr;
}

// Removes any display items that belonged to a frame that was deleted,
// and mark frames that belong to a different AGR so that get their
// items built again.
// TODO: We currently descend into all children even if we don't have an AGR
// to mark, as child stacking contexts might. It would be nice if we could
// jump into those immediately rather than walking the entire thing.
bool RetainedDisplayListBuilder::PreProcessDisplayList(
    RetainedDisplayList* aList, AnimatedGeometryRoot* aAGR,
    PartialUpdateResult& aUpdated, uint32_t aCallerKey, uint32_t aNestingDepth,
    bool aKeepLinked) {
  // The DAG merging algorithm does not have strong mechanisms in place to keep
  // the complexity of the resulting DAG under control. In some cases we can
  // build up edges very quickly. Detect those cases and force a full display
  // list build if we hit them.
  static const uint32_t kMaxEdgeRatio = 5;
  const bool initializeDAG = !aList->mDAG.Length();
  if (!aKeepLinked && !initializeDAG && aList->mDAG.mDirectPredecessorList.Length() >
                            (aList->mDAG.mNodesInfo.Length() * kMaxEdgeRatio)) {
    return false;
  }

  // If we had aKeepLinked=true for this list on the previous paint, then
  // mOldItems will already be initialized as it won't have been consumed during
  // a merge.
  const bool initializeOldItems = aList->mOldItems.IsEmpty();
  if (initializeOldItems) {
    aList->mOldItems.SetCapacity(aList->Count());
  } else {
    MOZ_RELEASE_ASSERT(!initializeDAG);
  }

  MOZ_RELEASE_ASSERT(
      initializeDAG ||
      aList->mDAG.Length() ==
          (initializeOldItems ? aList->Count() : aList->mOldItems.Length()));

  nsDisplayList out;

  size_t i = 0;
  while (nsDisplayItem* item = aList->RemoveBottom()) {
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
    item->SetMergedPreProcessed(false, true);
#endif

    // If we have a previously initialized old items list, then it can differ
    // from the current list due to items removed for having a deleted frame.
    // We can't easily remove these, since the DAG has entries for those indices
    // and it's hard to rewrite in-place.
    // Skip over entries with no current item to keep the iterations in sync.
    if (!initializeOldItems) {
      while (!aList->mOldItems[i].mItem) {
        i++;
      }
    }

    if (initializeDAG) {
      if (i == 0) {
        aList->mDAG.AddNode(Span<const MergedListIndex>());
      } else {
        MergedListIndex previous(i - 1);
        aList->mDAG.AddNode(Span<const MergedListIndex>(&previous, 1));
      }
    }

    if (!item->CanBeReused() || item->HasDeletedFrame()) {
      if (initializeOldItems) {
        aList->mOldItems.AppendElement(OldItemInfo(nullptr));
      } else {
        MOZ_RELEASE_ASSERT(aList->mOldItems[i].mItem == item);
        aList->mOldItems[i].mItem = nullptr;
      }
      item->Destroy(&mBuilder);

      i++;
      aUpdated = PartialUpdateResult::Updated;
      continue;
    }

    if (initializeOldItems) {
      aList->mOldItems.AppendElement(OldItemInfo(item));
    }

    // If we're not going to keep the list linked, then this old item entry
    // is the only pointer to the item. Let it know that it now strongly
    // owns the item, so it can destroy it if it goes away.
    aList->mOldItems[i].mOwnsItem = !aKeepLinked;

    item->SetOldListIndex(aList, OldListIndex(i), aCallerKey, aNestingDepth);

    nsIFrame* f = item->Frame();

    if (item->GetChildren()) {
      // If children inside this list were invalid, then we'd have walked the
      // ancestors and set ForceDescendIntoVisible on the current frame. If an
      // ancestor is modified, then we'll throw this away entirely. Either way,
      // we won't need to run merging on this sublist, and we can keep the items
      // linked into their display list.
      // The caret can move without invalidating, but we always set the force
      // descend into frame state bit on that frame, so check for that too.
      // TODO: AGR marking below can call MarkFrameForDisplayIfVisible and make
      // us think future siblings need to be merged, even though we don't really
      // need to.
      bool keepLinked = aKeepLinked;
      nsIFrame* invalid = item->FrameForInvalidation();
      if (!invalid->ForceDescendIntoIfVisible() &&
          !(invalid->GetStateBits() &
            NS_FRAME_FORCE_DISPLAY_LIST_DESCEND_INTO)) {
        keepLinked = true;
      }

      if (!PreProcessDisplayList(
              item->GetChildren(), SelectAGRForFrame(f, aAGR), aUpdated,
              item->GetPerFrameKey(), aNestingDepth + 1, keepLinked)) {
        MOZ_RELEASE_ASSERT(!aKeepLinked, "Can't early return since we need to move the out list back");
        return false;
      }
    }

    // TODO: We should be able to check the clipped bounds relative
    // to the common AGR (of both the existing item and the invalidated
    // frame) and determine if they can ever intersect.
    // TODO: We only really need to build the ancestor container item that is a
    // sibling of the changed thing to get correct ordering. The changed content
    // is a frame though, and it's hard to map that to container items in this
    // list.
    if (aAGR && item->GetAnimatedGeometryRoot()->GetAsyncAGR() != aAGR) {
      mBuilder.MarkFrameForDisplayIfVisible(f, mBuilder.RootReferenceFrame());
    }

    // TODO: This is here because we sometimes reuse the previous display list
    // completely. For optimization, we could only restore the state for reused
    // display items.
    item->RestoreState();

    // If we're going to keep this linked list and not merge it, then mark the
    // item as used and put it back into the list.
    if (aKeepLinked) {
      item->SetReused(true);
      if (item->GetChildren()) {
        item->UpdateBounds(Builder());
      }
      if (item->GetType() == DisplayItemType::TYPE_SUBDOCUMENT) {
        IncrementSubDocPresShellPaintCount(item);
      }
      out.AppendToTop(item);
    }
    i++;
  }

  MOZ_RELEASE_ASSERT(aList->mOldItems.Length() == aList->mDAG.Length());
  aList->RestoreState();

  if (aKeepLinked) {
    aList->AppendToTop(&out);
  }
  return true;
}

void RetainedDisplayListBuilder::IncrementSubDocPresShellPaintCount(
    nsDisplayItem* aItem) {
  MOZ_ASSERT(aItem->GetType() == DisplayItemType::TYPE_SUBDOCUMENT);

  nsSubDocumentFrame* subDocFrame =
      static_cast<nsDisplaySubDocument*>(aItem)->SubDocumentFrame();
  MOZ_ASSERT(subDocFrame);

  PresShell* presShell = subDocFrame->GetSubdocumentPresShellForPainting(0);
  MOZ_ASSERT(presShell);

  mBuilder.IncrementPresShellPaintCount(presShell);
}

bool AnyContentAncestorModified(nsIFrame* aFrame, nsIFrame* aStopAtFrame) {
  nsIFrame* f = aFrame;
  while (f) {
    if (f->IsFrameModified()) {
      return true;
    }

    if (aStopAtFrame && f == aStopAtFrame) {
      break;
    }

    if (f->GetStateBits() & NS_FRAME_IS_PUSHED_FLOAT) {
      f = f->GetParent();
    } else {
      f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f);
    }
  }

  return false;
}

static Maybe<const ActiveScrolledRoot*> SelectContainerASR(
    const DisplayItemClipChain* aClipChain, const ActiveScrolledRoot* aItemASR,
    Maybe<const ActiveScrolledRoot*>& aContainerASR) {
  const ActiveScrolledRoot* itemClipASR =
      aClipChain ? aClipChain->mASR : nullptr;

  const ActiveScrolledRoot* finiteBoundsASR =
      ActiveScrolledRoot::PickDescendant(itemClipASR, aItemASR);

  if (!aContainerASR) {
    return Some(finiteBoundsASR);
  }

  return Some(
      ActiveScrolledRoot::PickAncestor(*aContainerASR, finiteBoundsASR));
}

static void UpdateASR(nsDisplayItem* aItem,
                      Maybe<const ActiveScrolledRoot*>& aContainerASR) {
  Maybe<const ActiveScrolledRoot*> asr;

  if (aItem->HasHitTestInfo()) {
    const HitTestInfo& info =
        static_cast<nsDisplayHitTestInfoItem*>(aItem)->GetHitTestInfo();
    asr = SelectContainerASR(info.mClipChain, info.mASR, aContainerASR);
  } else {
    asr = aContainerASR;
  }

  if (!asr) {
    return;
  }

  nsDisplayWrapList* wrapList = aItem->AsDisplayWrapList();
  if (!wrapList) {
    aItem->SetActiveScrolledRoot(*asr);
    return;
  }

  wrapList->SetActiveScrolledRoot(ActiveScrolledRoot::PickAncestor(
      wrapList->GetFrameActiveScrolledRoot(), *asr));

  wrapList->UpdateHitTestInfoActiveScrolledRoot(*asr);
}

static void CopyASR(nsDisplayItem* aOld, nsDisplayItem* aNew) {
  const ActiveScrolledRoot* hitTest = nullptr;
  if (aOld->HasHitTestInfo()) {
    MOZ_ASSERT(aNew->HasHitTestInfo());
    const HitTestInfo& info =
        static_cast<nsDisplayHitTestInfoItem*>(aOld)->GetHitTestInfo();
    hitTest = info.mASR;
  }

  aNew->SetActiveScrolledRoot(aOld->GetActiveScrolledRoot());

  // SetActiveScrolledRoot for most items will also set the hit-test info item's
  // asr, so we need to manually set that again to what we saved earlier.
  if (aOld->HasHitTestInfo()) {
    static_cast<nsDisplayHitTestInfoItem*>(aNew)
        ->UpdateHitTestInfoActiveScrolledRoot(hitTest);
  }
}

OldItemInfo::OldItemInfo(nsDisplayItem* aItem)
    : mItem(aItem), mUsed(false), mDiscarded(false), mOwnsItem(false) {
  if (mItem) {
    // Clear cached modified frame state when adding an item to the old list.
    mItem->SetModifiedFrame(false);
  }
}

void OldItemInfo::AddedMatchToMergedList(RetainedDisplayListBuilder* aBuilder,
                                         MergedListIndex aIndex) {
  AddedToMergedList(aIndex);
}

void OldItemInfo::Discard(RetainedDisplayListBuilder* aBuilder,
                          nsTArray<MergedListIndex>&& aDirectPredecessors) {
  MOZ_ASSERT(!IsUsed());
  mUsed = mDiscarded = true;
  mDirectPredecessors = std::move(aDirectPredecessors);
  if (mItem) {
    MOZ_ASSERT(mOwnsItem);
    mItem->Destroy(aBuilder->Builder());
  }
  mItem = nullptr;
}

bool OldItemInfo::IsChanged() {
  return !mItem || !mItem->CanBeReused() || mItem->HasDeletedFrame();
}

/**
 * A C++ implementation of Markus Stange's merge-dags algorithm.
 * https://github.com/mstange/merge-dags
 *
 * MergeState handles combining a new list of display items into an existing
 * DAG and computes the new DAG in a single pass.
 * Each time we add a new item, we resolve all dependencies for it, so that the
 * resulting list and DAG are built in topological ordering.
 */
class MergeState {
 public:
  MergeState(RetainedDisplayListBuilder* aBuilder,
             RetainedDisplayList& aOldList, nsDisplayItem* aOuterItem)
      : mBuilder(aBuilder),
        mOldList(&aOldList),
        mOldItems(std::move(aOldList.mOldItems)),
        mOldDAG(
            std::move(*reinterpret_cast<DirectedAcyclicGraph<OldListUnits>*>(
                &aOldList.mDAG))),
        mOuterItem(aOuterItem),
        mResultIsModified(false) {
    mMergedDAG.EnsureCapacityFor(mOldDAG);
    MOZ_RELEASE_ASSERT(mOldItems.Length() == mOldDAG.Length());
  }

  Maybe<MergedListIndex> ProcessItemFromNewList(
      nsDisplayItem* aNewItem, const Maybe<MergedListIndex>& aPreviousItem) {
    OldListIndex oldIndex;
    MOZ_DIAGNOSTIC_ASSERT(aNewItem->HasModifiedFrame() ==
                          HasModifiedFrame(aNewItem));
    if (!aNewItem->HasModifiedFrame() &&
        HasMatchingItemInOldList(aNewItem, &oldIndex)) {
      nsDisplayItem* oldItem = mOldItems[oldIndex.val].mItem;
      MOZ_DIAGNOSTIC_ASSERT(oldItem->GetPerFrameKey() ==
                                aNewItem->GetPerFrameKey() &&
                            oldItem->Frame() == aNewItem->Frame());
      if (!mOldItems[oldIndex.val].IsChanged()) {
        MOZ_DIAGNOSTIC_ASSERT(!mOldItems[oldIndex.val].IsUsed());
        nsDisplayItem* destItem;
        if (ShouldUseNewItem(aNewItem)) {
          destItem = aNewItem;
        } else {
          destItem = oldItem;
          // The building rect can depend on the overflow rect (when the parent
          // frame is position:fixed), which can change without invalidating
          // the frame/items. If we're using the old item, copy the building
          // rect across from the new item.
          oldItem->SetBuildingRect(aNewItem->GetBuildingRect());
        }

        MergeChildLists(aNewItem, oldItem, destItem);

        AutoTArray<MergedListIndex, 2> directPredecessors =
            ProcessPredecessorsOfOldNode(oldIndex);
        MergedListIndex newIndex = AddNewNode(
            destItem, Some(oldIndex), directPredecessors, aPreviousItem);
        mOldItems[oldIndex.val].AddedMatchToMergedList(mBuilder, newIndex);
        if (destItem == aNewItem) {
          oldItem->Destroy(mBuilder->Builder());
        } else {
          aNewItem->Destroy(mBuilder->Builder());
        }
        return Some(newIndex);
      }
    }
    mResultIsModified = true;
    return Some(AddNewNode(aNewItem, Nothing(), Span<MergedListIndex>(),
                           aPreviousItem));
  }

  void MergeChildLists(nsDisplayItem* aNewItem, nsDisplayItem* aOldItem,
                       nsDisplayItem* aOutItem) {
    if (!aOutItem->GetChildren()) {
      return;
    }

    Maybe<const ActiveScrolledRoot*> containerASRForChildren;
    nsDisplayList empty;
    const bool modified = mBuilder->MergeDisplayLists(
        aNewItem ? aNewItem->GetChildren() : &empty, aOldItem->GetChildren(),
        aOutItem->GetChildren(), containerASRForChildren, aOutItem);
    if (modified) {
      aOutItem->InvalidateCachedChildInfo(mBuilder->Builder());
      UpdateASR(aOutItem, containerASRForChildren);
      mResultIsModified = true;
    } else if (aOutItem == aNewItem) {
      // If nothing changed, but we copied the contents across to
      // the new item, then also copy the ASR data.
      CopyASR(aOldItem, aNewItem);
    }
    // Ideally we'd only UpdateBounds if something changed, but
    // nsDisplayWrapList also uses this to update the clip chain for the
    // current ASR, which gets reset during RestoreState(), so we always need
    // to run it again.
    aOutItem->UpdateBounds(mBuilder->Builder());
  }

  bool ShouldUseNewItem(nsDisplayItem* aNewItem) {
    // Generally we want to use the old item when the frame isn't marked as
    // modified so that any cached information on the item (or referencing the
    // item) gets retained. Quite a few FrameLayerBuilder performance
    // improvements benefit by this. Sometimes, however, we can end up where the
    // new item paints something different from the old item, even though we
    // haven't modified the frame, and it's hard to fix. In these cases we just
    // always use the new item to be safe.
    DisplayItemType type = aNewItem->GetType();
    if (type == DisplayItemType::TYPE_CANVAS_BACKGROUND_COLOR ||
        type == DisplayItemType::TYPE_SOLID_COLOR) {
      // The canvas background color item can paint the color from another
      // frame, and even though we schedule a paint, we don't mark the canvas
      // frame as invalid.
      return true;
    }

    if (type == DisplayItemType::TYPE_TABLE_BORDER_COLLAPSE) {
      // We intentionally don't mark the root table frame as modified when a
      // subframe changes, even though the border collapse item for the root
      // frame is what paints the changed border. Marking the root frame as
      // modified would rebuild display items for the whole table area, and we
      // don't want that.
      return true;
    }

    if (type == DisplayItemType::TYPE_TEXT_OVERFLOW) {
      // Text overflow marker items are created with the wrapping block as their
      // frame, and have an index value to note which line they are created for.
      // Their rendering can change if the items on that line change, which may
      // not mark the block as modified. We rebuild them if we build any item on
      // the line, so we should always get new items if they might have changed
      // rendering, and it's easier to just use the new items rather than
      // computing if we actually need them.
      return true;
    }

    if (type == DisplayItemType::TYPE_SUBDOCUMENT) {
      // nsDisplaySubDocument::mShouldFlatten can change without an invalidation
      // (and is the reason we unconditionally build the subdocument item), so
      // always use the new one to make sure we get the right value.
      return true;
    }

    if (type == DisplayItemType::TYPE_CARET) {
      // The caret can change position while still being owned by the same frame
      // and we don't invalidate in that case. Use the new version since the
      // changed bounds are needed for DLBI.
      return true;
    }

    if (type == DisplayItemType::TYPE_MASK ||
        type == DisplayItemType::TYPE_FILTER ||
        type == DisplayItemType::TYPE_SVG_WRAPPER) {
      // SVG items have some invalidation issues, see bugs 1494110 and 1494663.
      return true;
    }

    if (type == DisplayItemType::TYPE_TRANSFORM) {
      // Prerendering of transforms can change without frame invalidation.
      return true;
    }

    return false;
  }

  RetainedDisplayList Finalize() {
    for (size_t i = 0; i < mOldDAG.Length(); i++) {
      if (mOldItems[i].IsUsed()) {
        continue;
      }

      AutoTArray<MergedListIndex, 2> directPredecessors =
          ResolveNodeIndexesOldToMerged(
              mOldDAG.GetDirectPredecessors(OldListIndex(i)));
      ProcessOldNode(OldListIndex(i), std::move(directPredecessors));
    }

    RetainedDisplayList result;
    result.AppendToTop(&mMergedItems);
    result.mDAG = std::move(mMergedDAG);
    MOZ_RELEASE_ASSERT(result.mDAG.Length() == result.Count());
    return result;
  }

  bool HasMatchingItemInOldList(nsDisplayItem* aItem, OldListIndex* aOutIndex) {
    nsIFrame::DisplayItemArray* items =
        aItem->Frame()->GetProperty(nsIFrame::DisplayItems());
    // Look for an item that matches aItem's frame and per-frame-key, but isn't
    // the same item.
    uint32_t outerKey = mOuterItem ? mOuterItem->GetPerFrameKey() : 0;
    for (nsDisplayItemBase* i : *items) {
      if (i != aItem && i->Frame() == aItem->Frame() &&
          i->GetPerFrameKey() == aItem->GetPerFrameKey()) {
        if (i->GetOldListIndex(mOldList, outerKey, aOutIndex)) {
          return true;
        }
      }
    }
    return false;
  }

  bool HasModifiedFrame(nsDisplayItem* aItem) {
    nsIFrame* stopFrame = mOuterItem ? mOuterItem->Frame() : nullptr;
    return AnyContentAncestorModified(aItem->FrameForInvalidation(), stopFrame);
  }

  void UpdateContainerASR(nsDisplayItem* aItem) {
    mContainerASR = SelectContainerASR(
        aItem->GetClipChain(), aItem->GetActiveScrolledRoot(), mContainerASR);
  }

  MergedListIndex AddNewNode(
      nsDisplayItem* aItem, const Maybe<OldListIndex>& aOldIndex,
      Span<const MergedListIndex> aDirectPredecessors,
      const Maybe<MergedListIndex>& aExtraDirectPredecessor) {
    UpdateContainerASR(aItem);
    aItem->NotifyUsed(mBuilder->Builder());

#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
    nsIFrame::DisplayItemArray* items =
        aItem->Frame()->GetProperty(nsIFrame::DisplayItems());
    for (nsDisplayItemBase* i : *items) {
      if (i->Frame() == aItem->Frame() &&
          i->GetPerFrameKey() == aItem->GetPerFrameKey()) {
        MOZ_DIAGNOSTIC_ASSERT(!i->IsMergedItem());
      }
    }

    aItem->SetMergedPreProcessed(true, false);
#endif

    mMergedItems.AppendToTop(aItem);
    MergedListIndex newIndex =
        mMergedDAG.AddNode(aDirectPredecessors, aExtraDirectPredecessor);
    return newIndex;
  }

  void ProcessOldNode(OldListIndex aNode,
                      nsTArray<MergedListIndex>&& aDirectPredecessors) {
    nsDisplayItem* item = mOldItems[aNode.val].mItem;
    if (mOldItems[aNode.val].IsChanged() || HasModifiedFrame(item)) {
      mOldItems[aNode.val].Discard(mBuilder, std::move(aDirectPredecessors));
      mResultIsModified = true;
    } else {
      MergeChildLists(nullptr, item, item);

      if (item->GetType() == DisplayItemType::TYPE_SUBDOCUMENT) {
        mBuilder->IncrementSubDocPresShellPaintCount(item);
      }
      item->SetReused(true);
      mOldItems[aNode.val].AddedToMergedList(
          AddNewNode(item, Some(aNode), aDirectPredecessors, Nothing()));
    }
  }

  struct PredecessorStackItem {
    PredecessorStackItem(OldListIndex aNode, Span<OldListIndex> aPredecessors)
        : mNode(aNode),
          mDirectPredecessors(aPredecessors),
          mCurrentPredecessorIndex(0) {}

    bool IsFinished() {
      return mCurrentPredecessorIndex == mDirectPredecessors.Length();
    }

    OldListIndex GetAndIncrementCurrentPredecessor() {
      return mDirectPredecessors[mCurrentPredecessorIndex++];
    }

    OldListIndex mNode;
    Span<OldListIndex> mDirectPredecessors;
    size_t mCurrentPredecessorIndex;
  };

  AutoTArray<MergedListIndex, 2> ProcessPredecessorsOfOldNode(
      OldListIndex aNode) {
    AutoTArray<PredecessorStackItem, 256> mStack;
    mStack.AppendElement(
        PredecessorStackItem(aNode, mOldDAG.GetDirectPredecessors(aNode)));

    while (true) {
      if (mStack.LastElement().IsFinished()) {
        // If we've finished processing all the entries in the current set, then
        // pop it off the processing stack and process it.
        PredecessorStackItem item = mStack.PopLastElement();
        AutoTArray<MergedListIndex, 2> result =
            ResolveNodeIndexesOldToMerged(item.mDirectPredecessors);

        if (mStack.IsEmpty()) {
          return result;
        }

        ProcessOldNode(item.mNode, std::move(result));
      } else {
        // Grab the current predecessor, push predecessors of that onto the
        // processing stack (if it hasn't already been processed), and then
        // advance to the next entry.
        OldListIndex currentIndex =
            mStack.LastElement().GetAndIncrementCurrentPredecessor();
        if (!mOldItems[currentIndex.val].IsUsed()) {
          mStack.AppendElement(PredecessorStackItem(
              currentIndex, mOldDAG.GetDirectPredecessors(currentIndex)));
        }
      }
    }
  }

  AutoTArray<MergedListIndex, 2> ResolveNodeIndexesOldToMerged(
      Span<OldListIndex> aDirectPredecessors) {
    AutoTArray<MergedListIndex, 2> result;
    result.SetCapacity(aDirectPredecessors.Length());
    for (OldListIndex index : aDirectPredecessors) {
      OldItemInfo& oldItem = mOldItems[index.val];
      if (oldItem.IsDiscarded()) {
        for (MergedListIndex inner : oldItem.mDirectPredecessors) {
          if (!result.Contains(inner)) {
            result.AppendElement(inner);
          }
        }
      } else {
        result.AppendElement(oldItem.mIndex);
      }
    }
    return result;
  }

  RetainedDisplayListBuilder* mBuilder;
  RetainedDisplayList* mOldList;
  Maybe<const ActiveScrolledRoot*> mContainerASR;
  nsTArray<OldItemInfo> mOldItems;
  DirectedAcyclicGraph<OldListUnits> mOldDAG;
  // Unfortunately we can't use strong typing for the hashtables
  // since they internally encode the type with the mOps pointer,
  // and assert when we try swap the contents
  nsDisplayList mMergedItems;
  DirectedAcyclicGraph<MergedListUnits> mMergedDAG;
  nsDisplayItem* mOuterItem;
  bool mResultIsModified;
};

#ifdef DEBUG
void VerifyNotModified(nsDisplayList* aList) {
  for (nsDisplayItem* item = aList->GetBottom(); item;
       item = item->GetAbove()) {
    MOZ_ASSERT(!AnyContentAncestorModified(item->FrameForInvalidation()));

    if (item->GetChildren()) {
      VerifyNotModified(item->GetChildren());
    }
  }
}
#endif

/**
 * Takes two display lists and merges them into an output list.
 *
 * Display lists wthout an explicit DAG are interpreted as linear DAGs (with a
 * maximum of one direct predecessor and one direct successor per node). We add
 * the two DAGs together, and then output the topological sorted ordering as the
 * final display list.
 *
 * Once we've merged a list, we then retain the DAG (as part of the
 * RetainedDisplayList object) to use for future merges.
 */
bool RetainedDisplayListBuilder::MergeDisplayLists(
    nsDisplayList* aNewList, RetainedDisplayList* aOldList,
    RetainedDisplayList* aOutList,
    mozilla::Maybe<const mozilla::ActiveScrolledRoot*>& aOutContainerASR,
    nsDisplayItem* aOuterItem) {
  AUTO_PROFILER_LABEL_CATEGORY_PAIR(GRAPHICS_DisplayListMerging);

  if (!aOldList->IsEmpty()) {
    // If we still have items in the actual list, then it is because
    // PreProcessDisplayList decided that it was sure it can't be modified. We
    // can just use it directly, and throw any new items away.

    aNewList->DeleteAll(&mBuilder);
#ifdef DEBUG
    VerifyNotModified(aOldList);
#endif

    if (aOldList != aOutList) {
      *aOutList = std::move(*aOldList);
    }

    return false;
  }

  MergeState merge(this, *aOldList, aOuterItem);

  Maybe<MergedListIndex> previousItemIndex;
  while (nsDisplayItem* item = aNewList->RemoveBottom()) {
    previousItemIndex = merge.ProcessItemFromNewList(item, previousItemIndex);
  }

  *aOutList = merge.Finalize();
  aOutContainerASR = merge.mContainerASR;
  return merge.mResultIsModified;
}

static void TakeAndAddModifiedAndFramesWithPropsFromRootFrame(
    nsDisplayListBuilder* aBuilder, nsTArray<nsIFrame*>* aModifiedFrames,
    nsTArray<nsIFrame*>* aFramesWithProps, nsIFrame* aRootFrame) {
  MOZ_ASSERT(aRootFrame);

  RetainedDisplayListData* data = GetRetainedDisplayListData(aRootFrame);

  if (!data) {
    return;
  }

  for (auto it = data->Iterator(); !it.Done(); it.Next()) {
    nsIFrame* frame = it.Key();
    const RetainedDisplayListData::FrameFlags& flags = it.Data();

    if (flags & RetainedDisplayListData::FrameFlags::Modified) {
      aModifiedFrames->AppendElement(frame);
    }

    if (flags & RetainedDisplayListData::FrameFlags::HasProps) {
      aFramesWithProps->AppendElement(frame);
    }

    if (flags & RetainedDisplayListData::FrameFlags::HadWillChange) {
      aBuilder->RemoveFromWillChangeBudget(frame);
    }
  }

  data->Clear();
}

struct CbData {
  nsDisplayListBuilder* builder;
  nsTArray<nsIFrame*>* modifiedFrames;
  nsTArray<nsIFrame*>* framesWithProps;
};

static nsIFrame* GetRootFrameForPainting(nsDisplayListBuilder* aBuilder,
                                         Document* aDocument) {
  // Although this is the actual subdocument, it might not be
  // what painting uses. Walk up to the nsSubDocumentFrame owning
  // us, and then ask that which subdoc it's going to paint.

  PresShell* presShell = aDocument->GetPresShell();
  if (!presShell) {
    return nullptr;
  }
  nsView* rootView = presShell->GetViewManager()->GetRootView();
  if (!rootView) {
    return nullptr;
  }

  // There should be an anonymous inner view between the root view
  // of the subdoc, and the view for the nsSubDocumentFrame.
  nsView* innerView = rootView->GetParent();
  if (!innerView) {
    return nullptr;
  }

  nsView* subDocView = innerView->GetParent();
  if (!subDocView) {
    return nullptr;
  }

  nsIFrame* subDocFrame = subDocView->GetFrame();
  if (!subDocFrame) {
    return nullptr;
  }

  nsSubDocumentFrame* subdocumentFrame = do_QueryFrame(subDocFrame);
  MOZ_ASSERT(subdocumentFrame);
  presShell = subdocumentFrame->GetSubdocumentPresShellForPainting(
      aBuilder->IsIgnoringPaintSuppression()
          ? nsSubDocumentFrame::IGNORE_PAINT_SUPPRESSION
          : 0);
  return presShell ? presShell->GetRootFrame() : nullptr;
}

static bool SubDocEnumCb(Document* aDocument, void* aData) {
  MOZ_ASSERT(aDocument);
  MOZ_ASSERT(aData);

  CbData* data = static_cast<CbData*>(aData);

  nsIFrame* rootFrame = GetRootFrameForPainting(data->builder, aDocument);
  if (rootFrame) {
    TakeAndAddModifiedAndFramesWithPropsFromRootFrame(
        data->builder, data->modifiedFrames, data->framesWithProps, rootFrame);

    Document* innerDoc = rootFrame->PresShell()->GetDocument();
    if (innerDoc) {
      innerDoc->EnumerateSubDocuments(SubDocEnumCb, aData);
    }
  }
  return true;
}

static void GetModifiedAndFramesWithProps(
    nsDisplayListBuilder* aBuilder, nsTArray<nsIFrame*>* aOutModifiedFrames,
    nsTArray<nsIFrame*>* aOutFramesWithProps) {
  nsIFrame* rootFrame = aBuilder->RootReferenceFrame();
  MOZ_ASSERT(rootFrame);

  TakeAndAddModifiedAndFramesWithPropsFromRootFrame(
      aBuilder, aOutModifiedFrames, aOutFramesWithProps, rootFrame);

  Document* rootdoc = rootFrame->PresContext()->Document();
  if (rootdoc) {
    CbData data = {aBuilder, aOutModifiedFrames, aOutFramesWithProps};

    rootdoc->EnumerateSubDocuments(SubDocEnumCb, &data);
  }
}

// ComputeRebuildRegion  debugging
// #define CRR_DEBUG 1
#if CRR_DEBUG
#  define CRR_LOG(...) printf_stderr(__VA_ARGS__)
#else
#  define CRR_LOG(...)
#endif

static nsDisplayItem* GetFirstDisplayItemWithChildren(nsIFrame* aFrame) {
  nsIFrame::DisplayItemArray* items =
      aFrame->GetProperty(nsIFrame::DisplayItems());
  if (!items) {
    return nullptr;
  }

  for (nsDisplayItemBase* i : *items) {
    if (i->HasChildren()) {
      return static_cast<nsDisplayItem*>(i);
    }
  }
  return nullptr;
}

static bool IsInPreserve3DContext(const nsIFrame* aFrame) {
  return aFrame->Extend3DContext() ||
         aFrame->Combines3DTransformWithAncestors();
}

static bool ProcessFrameInternal(nsIFrame* aFrame,
                                 nsDisplayListBuilder& aBuilder,
                                 AnimatedGeometryRoot** aAGR, nsRect& aOverflow,
                                 nsIFrame* aStopAtFrame,
                                 nsTArray<nsIFrame*>& aOutFramesWithProps,
                                 const bool aStopAtStackingContext) {
  nsIFrame* currentFrame = aFrame;

  while (currentFrame != aStopAtFrame) {
    CRR_LOG("currentFrame: %p (placeholder=%d), aOverflow: %d %d %d %d\n",
            currentFrame, !aStopAtStackingContext, aOverflow.x, aOverflow.y,
            aOverflow.width, aOverflow.height);

    // If the current frame is an OOF frame, DisplayListBuildingData needs to be
    // set on all the ancestor stacking contexts of the  placeholder frame, up
    // to the containing block of the OOF frame. This is done to ensure that the
    // content that might be behind the OOF frame is built for merging.
    nsIFrame* placeholder = currentFrame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW)
                                ? currentFrame->GetPlaceholderFrame()
                                : nullptr;

    if (placeholder) {
      nsRect placeholderOverflow = aOverflow;
      auto rv = nsLayoutUtils::TransformRect(currentFrame, placeholder,
                                             placeholderOverflow);
      if (rv != nsLayoutUtils::TRANSFORM_SUCCEEDED) {
        placeholderOverflow = nsRect();
      }

      CRR_LOG("Processing placeholder %p for OOF frame %p\n", placeholder,
              currentFrame);

      CRR_LOG("OOF frame draw area: %d %d %d %d\n", placeholderOverflow.x,
              placeholderOverflow.y, placeholderOverflow.width,
              placeholderOverflow.height);

      // Tracking AGRs for the placeholder processing is not necessary, as the
      // goal is to only modify the DisplayListBuildingData rect.
      AnimatedGeometryRoot* dummyAGR = nullptr;

      // Find a common ancestor frame to handle frame continuations.
      // TODO: It might be possible to write a more specific and efficient
      // function for this.
      nsIFrame* ancestor = nsLayoutUtils::FindNearestCommonAncestorFrame(
          currentFrame->GetParent(), placeholder->GetParent());

      if (!ProcessFrameInternal(placeholder, aBuilder, &dummyAGR,
                                placeholderOverflow, ancestor,
                                aOutFramesWithProps, false)) {
        return false;
      }
    }

    // Convert 'aOverflow' into the coordinate space of the nearest stacking
    // context or display port ancestor and update 'currentFrame' to point to
    // that frame.
    aOverflow = nsLayoutUtils::TransformFrameRectToAncestor(
        currentFrame, aOverflow, aStopAtFrame, nullptr, nullptr,
        /* aStopAtStackingContextAndDisplayPortAndOOFFrame = */ true,
        &currentFrame);
    if (IsInPreserve3DContext(currentFrame)) {
      return false;
    }

    MOZ_ASSERT(currentFrame);

    if (nsLayoutUtils::FrameHasDisplayPort(currentFrame)) {
      CRR_LOG("Frame belongs to displayport frame %p\n", currentFrame);
      nsIScrollableFrame* sf = do_QueryFrame(currentFrame);
      MOZ_ASSERT(sf);
      nsRect displayPort;
      DebugOnly<bool> hasDisplayPort = nsLayoutUtils::GetDisplayPort(
          currentFrame->GetContent(), &displayPort, RelativeTo::ScrollPort);
      MOZ_ASSERT(hasDisplayPort);
      // get it relative to the scrollport (from the scrollframe)
      nsRect r = aOverflow - sf->GetScrollPortRect().TopLeft();
      r.IntersectRect(r, displayPort);
      if (!r.IsEmpty()) {
        nsRect* rect = currentFrame->GetProperty(
            nsDisplayListBuilder::DisplayListBuildingDisplayPortRect());
        if (!rect) {
          rect = new nsRect();
          currentFrame->SetProperty(
              nsDisplayListBuilder::DisplayListBuildingDisplayPortRect(), rect);
          currentFrame->SetHasOverrideDirtyRegion(true);
          aOutFramesWithProps.AppendElement(currentFrame);
        }
        rect->UnionRect(*rect, r);
        CRR_LOG("Adding area to displayport draw area: %d %d %d %d\n", r.x, r.y,
                r.width, r.height);

        // TODO: Can we just use MarkFrameForDisplayIfVisible, plus
        // MarkFramesForDifferentAGR to ensure that this displayport, plus any
        // items that move relative to it get rebuilt, and then not contribute
        // to the root dirty area?
        aOverflow = sf->GetScrollPortRect();
      } else {
        // Don't contribute to the root dirty area at all.
        aOverflow.SetEmpty();
      }
    } else {
      aOverflow.IntersectRect(
          aOverflow, currentFrame->GetVisualOverflowRectRelativeToSelf());
    }

    if (aOverflow.IsEmpty()) {
      break;
    }

    if (currentFrame != aBuilder.RootReferenceFrame() &&
        currentFrame->IsStackingContext() &&
        currentFrame->IsFixedPosContainingBlock()) {
      CRR_LOG("Frame belongs to stacking context frame %p\n", currentFrame);
      // If we found an intermediate stacking context with an existing display
      // item then we can store the dirty rect there and stop. If we couldn't
      // find one then we need to keep bubbling up to the next stacking context.
      nsDisplayItem* wrapperItem =
          GetFirstDisplayItemWithChildren(currentFrame);
      if (!wrapperItem) {
        continue;
      }

      // Store the stacking context relative dirty area such
      // that display list building will pick it up when it
      // gets to it.
      nsDisplayListBuilder::DisplayListBuildingData* data =
          currentFrame->GetProperty(
              nsDisplayListBuilder::DisplayListBuildingRect());
      if (!data) {
        data = new nsDisplayListBuilder::DisplayListBuildingData();
        currentFrame->SetProperty(
            nsDisplayListBuilder::DisplayListBuildingRect(), data);
        currentFrame->SetHasOverrideDirtyRegion(true);
        aOutFramesWithProps.AppendElement(currentFrame);
      }
      CRR_LOG("Adding area to stacking context draw area: %d %d %d %d\n",
              aOverflow.x, aOverflow.y, aOverflow.width, aOverflow.height);
      data->mDirtyRect.UnionRect(data->mDirtyRect, aOverflow);

      if (!aStopAtStackingContext) {
        // Continue ascending the frame tree until we reach aStopAtFrame.
        continue;
      }

      // Grab the visible (display list building) rect for children of this
      // wrapper item and convert into into coordinate relative to the current
      // frame.
      nsRect previousVisible = wrapperItem->GetBuildingRectForChildren();
      if (wrapperItem->ReferenceFrameForChildren() ==
          wrapperItem->ReferenceFrame()) {
        previousVisible -= wrapperItem->ToReferenceFrame();
      } else {
        MOZ_ASSERT(wrapperItem->ReferenceFrameForChildren() ==
                   wrapperItem->Frame());
      }

      if (!previousVisible.Contains(aOverflow)) {
        // If the overflow area of the changed frame isn't contained within the
        // old item, then we might change the size of the item and need to
        // update its sorting accordingly. Keep propagating the overflow area up
        // so that we build intersecting items for sorting.
        continue;
      }

      if (!data->mModifiedAGR) {
        data->mModifiedAGR = *aAGR;
      } else if (data->mModifiedAGR != *aAGR) {
        data->mDirtyRect = currentFrame->GetVisualOverflowRectRelativeToSelf();
        CRR_LOG(
            "Found multiple modified AGRs within this stacking context, "
            "giving up\n");
      }

      // Don't contribute to the root dirty area at all.
      aOverflow.SetEmpty();
      *aAGR = nullptr;

      break;
    }
  }
  return true;
}

bool RetainedDisplayListBuilder::ProcessFrame(
    nsIFrame* aFrame, nsDisplayListBuilder& aBuilder, nsIFrame* aStopAtFrame,
    nsTArray<nsIFrame*>& aOutFramesWithProps, const bool aStopAtStackingContext,
    nsRect* aOutDirty, AnimatedGeometryRoot** aOutModifiedAGR) {
  if (aFrame->HasOverrideDirtyRegion()) {
    aOutFramesWithProps.AppendElement(aFrame);
  }

  if (aFrame->HasAnyStateBits(NS_FRAME_IN_POPUP)) {
    return true;
  }

  // TODO: There is almost certainly a faster way of doing this, probably can be
  // combined with the ancestor walk for TransformFrameRectToAncestor.
  AnimatedGeometryRoot* agr =
      aBuilder.FindAnimatedGeometryRootFor(aFrame)->GetAsyncAGR();

  CRR_LOG("Processing frame %p with agr %p\n", aFrame, agr->mFrame);

  // Convert the frame's overflow rect into the coordinate space
  // of the nearest stacking context that has an existing display item.
  // We store that as a dirty rect on that stacking context so that we build
  // all items that intersect the changed frame within the stacking context,
  // and then we use MarkFrameForDisplayIfVisible to make sure the stacking
  // context itself gets built. We don't need to build items that intersect
  // outside of the stacking context, since we know the stacking context item
  // exists in the old list, so we can trivially merge without needing other
  // items.
  nsRect overflow = aFrame->GetVisualOverflowRectRelativeToSelf();

  // If the modified frame is also a caret frame, include the caret area.
  // This is needed because some frames (for example text frames without text)
  // might have an empty overflow rect.
  if (aFrame == aBuilder.GetCaretFrame()) {
    overflow.UnionRect(overflow, aBuilder.GetCaretRect());
  }

  if (!ProcessFrameInternal(aFrame, aBuilder, &agr, overflow, aStopAtFrame,
                            aOutFramesWithProps, aStopAtStackingContext)) {
    return false;
  }

  if (!overflow.IsEmpty()) {
    aOutDirty->UnionRect(*aOutDirty, overflow);
    CRR_LOG("Adding area to root draw area: %d %d %d %d\n", overflow.x,
            overflow.y, overflow.width, overflow.height);

    // If we get changed frames from multiple AGRS, then just give up as it gets
    // really complex to track which items would need to be marked in
    // MarkFramesForDifferentAGR.
    if (!*aOutModifiedAGR) {
      CRR_LOG("Setting %p as root stacking context AGR\n", agr);
      *aOutModifiedAGR = agr;
    } else if (agr && *aOutModifiedAGR != agr) {
      CRR_LOG("Found multiple AGRs in root stacking context, giving up\n");
      return false;
    }
  }
  return true;
}

static void AddFramesForContainingBlock(nsIFrame* aBlock,
                                        const nsFrameList& aFrames,
                                        nsTArray<nsIFrame*>& aExtraFrames) {
  for (nsIFrame* f : aFrames) {
    if (!f->IsFrameModified() && AnyContentAncestorModified(f, aBlock)) {
      CRR_LOG("Adding invalid OOF %p\n", f);
      aExtraFrames.AppendElement(f);
    }
  }
}

// Placeholder descendants of aFrame don't contribute to aFrame's overflow area.
// Find all the containing blocks that might own placeholders under us, walk
// their OOF frames list, and manually invalidate any frames that are
// descendants of a modified frame (us, or another frame we'll get to soon).
// This is combined with the work required for MarkFrameForDisplayIfVisible,
// so that we can avoid an extra ancestor walk, and we can reuse the flag
// to detect when we've already visited an ancestor (and thus all further
// ancestors must also be visited).
static void FindContainingBlocks(nsIFrame* aFrame,
                                 nsTArray<nsIFrame*>& aExtraFrames) {
  for (nsIFrame* f = aFrame; f;
       f = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(f)) {
    if (f->ForceDescendIntoIfVisible()) return;
    f->SetForceDescendIntoIfVisible(true);
    CRR_LOG("Considering OOFs for %p\n", f);

    AddFramesForContainingBlock(f, f->GetChildList(nsIFrame::kFloatList),
                                aExtraFrames);
    AddFramesForContainingBlock(f, f->GetChildList(f->GetAbsoluteListID()),
                                aExtraFrames);
  }
}

/**
 * Given a list of frames that has been modified, computes the region that we
 * need to do display list building for in order to build all modified display
 * items.
 *
 * When a modified frame is within a stacking context (with an existing display
 * item), then we only contribute to the build area within the stacking context,
 * as well as forcing display list building to descend to the stacking context.
 * We don't need to add build area outside of the stacking context (and force
 * items above/below the stacking context container item to be built), since
 * just matching the position of the stacking context container item is
 * sufficient to ensure correct ordering during merging.
 *
 * We need to rebuild all items that might intersect with the modified frame,
 * both now and during async changes on the compositor. We do this by rebuilding
 * the area covered by the changed frame, as well as rebuilding all items that
 * have a different (async) AGR to the changed frame. If we have changes to
 * multiple AGRs (within a stacking context), then we rebuild that stacking
 * context entirely.
 *
 * @param aModifiedFrames The list of modified frames.
 * @param aOutDirty The result region to use for display list building.
 * @param aOutModifiedAGR The modified AGR for the root stacking context.
 * @param aOutFramesWithProps The list of frames to which we attached partial
 * build data so that it can be cleaned up.
 *
 * @return true if we succesfully computed a partial rebuild region, false if a
 * full build is required.
 */
bool RetainedDisplayListBuilder::ComputeRebuildRegion(
    nsTArray<nsIFrame*>& aModifiedFrames, nsRect* aOutDirty,
    AnimatedGeometryRoot** aOutModifiedAGR,
    nsTArray<nsIFrame*>& aOutFramesWithProps) {
  CRR_LOG("Computing rebuild regions for %zu frames:\n",
          aModifiedFrames.Length());
  nsTArray<nsIFrame*> extraFrames;
  for (nsIFrame* f : aModifiedFrames) {
    MOZ_ASSERT(f);

    mBuilder.AddFrameMarkedForDisplayIfVisible(f);
    FindContainingBlocks(f, extraFrames);

    if (!ProcessFrame(f, mBuilder, mBuilder.RootReferenceFrame(),
                      aOutFramesWithProps, true, aOutDirty, aOutModifiedAGR)) {
      return false;
    }
  }

  for (nsIFrame* f : extraFrames) {
    mBuilder.MarkFrameModifiedDuringBuilding(f);

    if (!ProcessFrame(f, mBuilder, mBuilder.RootReferenceFrame(),
                      aOutFramesWithProps, true, aOutDirty, aOutModifiedAGR)) {
      return false;
    }
  }

  return true;
}

/*
 * A simple early exit heuristic to avoid slow partial display list rebuilds.
 */
static bool ShouldBuildPartial(nsTArray<nsIFrame*>& aModifiedFrames) {
  if (aModifiedFrames.Length() > gfxPrefs::LayoutRebuildFrameLimit()) {
    return false;
  }

  for (nsIFrame* f : aModifiedFrames) {
    MOZ_ASSERT(f);

    const LayoutFrameType type = f->Type();

    // If we have any modified frames of the following types, it is likely that
    // doing a partial rebuild of the display list will be slower than doing a
    // full rebuild.
    // This is because these frames either intersect or may intersect with most
    // of the page content. This is either due to display port size or different
    // async AGR.
    if (type == LayoutFrameType::Viewport ||
        type == LayoutFrameType::PageContent ||
        type == LayoutFrameType::Canvas || type == LayoutFrameType::Scrollbar) {
      return false;
    }
  }

  return true;
}

static void ClearFrameProps(nsTArray<nsIFrame*>& aFrames) {
  for (nsIFrame* f : aFrames) {
    if (f->HasOverrideDirtyRegion()) {
      f->SetHasOverrideDirtyRegion(false);
      f->DeleteProperty(nsDisplayListBuilder::DisplayListBuildingRect());
      f->DeleteProperty(
          nsDisplayListBuilder::DisplayListBuildingDisplayPortRect());
    }

    f->SetFrameIsModified(false);
  }
}

class AutoClearFramePropsArray {
 public:
  explicit AutoClearFramePropsArray(size_t aCapacity) : mFrames(aCapacity) {}

  AutoClearFramePropsArray() = default;

  ~AutoClearFramePropsArray() { ClearFrameProps(mFrames); }

  nsTArray<nsIFrame*>& Frames() { return mFrames; }

  bool IsEmpty() const { return mFrames.IsEmpty(); }

 private:
  nsTArray<nsIFrame*> mFrames;
};

void RetainedDisplayListBuilder::ClearFramesWithProps() {
  AutoClearFramePropsArray modifiedFrames;
  AutoClearFramePropsArray framesWithProps;
  GetModifiedAndFramesWithProps(&mBuilder, &modifiedFrames.Frames(),
                                &framesWithProps.Frames());
}

auto RetainedDisplayListBuilder::AttemptPartialUpdate(
    nscolor aBackstop, mozilla::DisplayListChecker* aChecker)
    -> PartialUpdateResult {
  mBuilder.RemoveModifiedWindowRegions();
  mBuilder.ClearWindowOpaqueRegion();

  if (mBuilder.ShouldSyncDecodeImages()) {
    MarkFramesWithItemsAndImagesModified(&mList);
  }

  mBuilder.EnterPresShell(mBuilder.RootReferenceFrame());

  // We set the override dirty regions during ComputeRebuildRegion or in
  // nsLayoutUtils::InvalidateForDisplayPortChange. The display port change also
  // marks the frame modified, so those regions are cleared here as well.
  AutoClearFramePropsArray modifiedFrames(64);
  AutoClearFramePropsArray framesWithProps;
  GetModifiedAndFramesWithProps(&mBuilder, &modifiedFrames.Frames(),
                                &framesWithProps.Frames());

  // Do not allow partial builds if the retained display list is empty, or if
  // ShouldBuildPartial heuristic fails.
  bool shouldBuildPartial =
      !mList.IsEmpty() && ShouldBuildPartial(modifiedFrames.Frames());

  // We don't support retaining with overlay scrollbars, since they require
  // us to look at the display list and pick the highest z-index, which
  // we can't do during partial building.
  if (mBuilder.DisablePartialUpdates()) {
    shouldBuildPartial = false;
    mBuilder.SetDisablePartialUpdates(false);
  }

  if (mPreviousCaret != mBuilder.GetCaretFrame()) {
    if (mPreviousCaret) {
      if (mBuilder.MarkFrameModifiedDuringBuilding(mPreviousCaret)) {
        modifiedFrames.Frames().AppendElement(mPreviousCaret);
      }
    }

    if (mBuilder.GetCaretFrame()) {
      if (mBuilder.MarkFrameModifiedDuringBuilding(mBuilder.GetCaretFrame())) {
        modifiedFrames.Frames().AppendElement(mBuilder.GetCaretFrame());
      }
    }

    mPreviousCaret = mBuilder.GetCaretFrame();
  }

  nsRect modifiedDirty;
  AnimatedGeometryRoot* modifiedAGR = nullptr;
  PartialUpdateResult result = PartialUpdateResult::NoChange;
  if (!shouldBuildPartial ||
      !ComputeRebuildRegion(modifiedFrames.Frames(), &modifiedDirty,
                            &modifiedAGR, framesWithProps.Frames()) ||
      !PreProcessDisplayList(&mList, modifiedAGR, result)) {
    mBuilder.LeavePresShell(mBuilder.RootReferenceFrame(), nullptr);
    mList.DeleteAll(&mBuilder);
    return PartialUpdateResult::Failed;
  }

  // This is normally handled by EnterPresShell, but we skipped it so that we
  // didn't call MarkFrameForDisplayIfVisible before ComputeRebuildRegion.
  nsIScrollableFrame* sf = mBuilder.RootReferenceFrame()
                               ->PresShell()
                               ->GetRootScrollFrameAsScrollable();
  if (sf) {
    nsCanvasFrame* canvasFrame = do_QueryFrame(sf->GetScrolledFrame());
    if (canvasFrame) {
      mBuilder.MarkFrameForDisplayIfVisible(canvasFrame,
                                            mBuilder.RootReferenceFrame());
    }
  }

  modifiedDirty.IntersectRect(
      modifiedDirty,
      mBuilder.RootReferenceFrame()->GetVisualOverflowRectRelativeToSelf());

  mBuilder.SetDirtyRect(modifiedDirty);
  mBuilder.SetPartialUpdate(true);

  nsDisplayList modifiedDL;
  mBuilder.RootReferenceFrame()->BuildDisplayListForStackingContext(
      &mBuilder, &modifiedDL);
  if (!modifiedDL.IsEmpty()) {
    nsLayoutUtils::AddExtraBackgroundItems(
        mBuilder, modifiedDL, mBuilder.RootReferenceFrame(),
        nsRect(nsPoint(0, 0), mBuilder.RootReferenceFrame()->GetSize()),
        mBuilder.RootReferenceFrame()->GetVisualOverflowRectRelativeToSelf(),
        aBackstop);
  }
  mBuilder.SetPartialUpdate(false);

  if (mBuilder.PartialBuildFailed()) {
    mBuilder.SetPartialBuildFailed(false);
    mBuilder.LeavePresShell(mBuilder.RootReferenceFrame(), nullptr);
    mList.DeleteAll(&mBuilder);
    modifiedDL.DeleteAll(&mBuilder);
    return PartialUpdateResult::Failed;
  }

  if (aChecker) {
    aChecker->Set(&modifiedDL, "TM");
  }

  // printf_stderr("Painting --- Modified list (dirty %d,%d,%d,%d):\n",
  //              modifiedDirty.x, modifiedDirty.y, modifiedDirty.width,
  //              modifiedDirty.height);
  // nsFrame::PrintDisplayList(&mBuilder, modifiedDL);

  // |modifiedDL| can sometimes be empty here. We still perform the
  // display list merging to prune unused items (for example, items that
  // are not visible anymore) from the old list.
  // TODO: Optimization opportunity. In this case, MergeDisplayLists()
  // unnecessarily creates a hashtable of the old items.
  // TODO: Ideally we could skip this if result is NoChange, but currently when
  // we call RestoreState on nsDisplayWrapList it resets the clip to the base
  // clip, and we need the UpdateBounds call (within MergeDisplayLists) to
  // move it to the correct inner clip.
  Maybe<const ActiveScrolledRoot*> dummy;
  if (MergeDisplayLists(&modifiedDL, &mList, &mList, dummy)) {
    result = PartialUpdateResult::Updated;
  }

  // printf_stderr("Painting --- Merged list:\n");
  // nsFrame::PrintDisplayList(&mBuilder, mList);

  mBuilder.LeavePresShell(mBuilder.RootReferenceFrame(), List());
  return result;
}