Store and set stale frame content screenshot when the frame is evicted

This patch captures the web content surface as a texture and displays
it on the RenderWidgetHostView until a new compositor frame is submitted
by the viz client. This allows the browser to display stale content
instead of a white solid color during animations.

The decision to show stale content or a solid color is done by the
WebContentsDelegate associated with the RenderWidgetHostView and its
DelegatedFrameHost. For this patch, the default is to show a solid
color on frame eviction. In the case of a tabbed browser, the Browser
(which implements the WebContentsDelegate) chooses to show stale content
if the frame being evicted is from an active tab.

Bug: 897826
Change-Id: I3346f3dec79780d107d2c96b4382886690a69570
Component: RenderWidgetHostViewAura, DelegatedFrameHost, ui layer
Reviewed-on: https://chromium-review.googlesource.com/c/1369714
Reviewed-by: Sadrul Chowdhury <sadrul@chromium.org>
Reviewed-by: Scott Violet <sky@chromium.org>
Reviewed-by: danakj <danakj@chromium.org>
Reviewed-by: Saman Sami <samans@chromium.org>
Reviewed-by: François Doray <fdoray@chromium.org>
Commit-Queue: Malay Keshav <malaykeshav@chromium.org>
Cr-Commit-Position: refs/heads/master@{#622255}
diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc
index 27290da..a2cc679 100644
--- a/chrome/browser/ui/browser.cc
+++ b/chrome/browser/ui/browser.cc
@@ -1319,6 +1319,10 @@
   return tab_strip_model_->ReplaceWebContentsAt(index, std::move(new_contents));
 }
 
+bool Browser::ShouldShowStaleContentOnEviction(content::WebContents* source) {
+  return source == tab_strip_model_->GetActiveWebContents();
+}
+
 bool Browser::IsMouseLocked() const {
   return exclusive_access_manager_->mouse_lock_controller()->IsMouseLocked();
 }
diff --git a/chrome/browser/ui/browser.h b/chrome/browser/ui/browser.h
index 29e83e11..4677cab8 100644
--- a/chrome/browser/ui/browser.h
+++ b/chrome/browser/ui/browser.h
@@ -538,6 +538,7 @@
       std::unique_ptr<content::WebContents> new_contents,
       bool did_start_load,
       bool did_finish_load) override;
+  bool ShouldShowStaleContentOnEviction(content::WebContents* source) override;
 
   bool is_type_tabbed() const { return type_ == TYPE_TABBED; }
   bool is_type_popup() const { return type_ == TYPE_POPUP; }
diff --git a/content/browser/renderer_host/DEPS b/content/browser/renderer_host/DEPS
index a38288a..0e360ca4 100644
--- a/content/browser/renderer_host/DEPS
+++ b/content/browser/renderer_host/DEPS
@@ -24,6 +24,7 @@
     "+content/browser/frame_host",
     "+content/browser/web_contents",
     "+content/public/browser/web_contents.h",
+    "+content/public/browser/web_contents_delegate.h",
     "+content/public/browser/web_contents_view.h",
   ],
   "render_process_host_impl\.cc": [
diff --git a/content/browser/renderer_host/browser_compositor_view_mac.h b/content/browser/renderer_host/browser_compositor_view_mac.h
index da7e339..454c4cf 100644
--- a/content/browser/renderer_host/browser_compositor_view_mac.h
+++ b/content/browser/renderer_host/browser_compositor_view_mac.h
@@ -129,6 +129,7 @@
   float GetDeviceScaleFactor() const override;
   void InvalidateLocalSurfaceIdOnEviction() override;
   std::vector<viz::SurfaceId> CollectSurfaceIdsForEviction() override;
+  bool ShouldShowStaleContentOnEviction() override;
 
   base::WeakPtr<BrowserCompositorMac> GetWeakPtr() {
     return weak_factory_.GetWeakPtr();
diff --git a/content/browser/renderer_host/browser_compositor_view_mac.mm b/content/browser/renderer_host/browser_compositor_view_mac.mm
index 21d3b34..0817b4e 100644
--- a/content/browser/renderer_host/browser_compositor_view_mac.mm
+++ b/content/browser/renderer_host/browser_compositor_view_mac.mm
@@ -358,6 +358,10 @@
   return client_->CollectSurfaceIdsForEviction();
 }
 
+bool BrowserCompositorMac::ShouldShowStaleContentOnEviction() {
+  return false;
+}
+
 void BrowserCompositorMac::DidNavigate() {
   if (render_widget_host_is_hidden_) {
     // Navigating while hidden should not allocate a new LocalSurfaceID. Once
diff --git a/content/browser/renderer_host/delegated_frame_host.cc b/content/browser/renderer_host/delegated_frame_host.cc
index 0bfce4f..ecc0e45 100644
--- a/content/browser/renderer_host/delegated_frame_host.cc
+++ b/content/browser/renderer_host/delegated_frame_host.cc
@@ -28,10 +28,19 @@
 #include "content/browser/gpu/compositor_util.h"
 #include "content/common/tab_switching_time_callback.h"
 #include "content/public/common/content_switches.h"
+#include "third_party/khronos/GLES2/gl2.h"
 #include "third_party/skia/include/core/SkColor.h"
 #include "ui/gfx/geometry/dip_util.h"
 
 namespace content {
+namespace {
+
+// Normalized value [0..1] where 1 is full quality and 0 is empty. This sets
+// the quality of the captured texture by reducing its dimensions by this
+// factor.
+constexpr float kFrameContentCaptureQuality = 0.5f;
+
+}  // namespace
 
 ////////////////////////////////////////////////////////////////////////////////
 // DelegatedFrameHost
@@ -64,6 +73,10 @@
                                                    "DelegatedFrameHost");
   CreateCompositorFrameSinkSupport();
   frame_evictor_->SetVisible(client_->DelegatedFrameHostIsVisible());
+
+  stale_content_layer_ =
+      std::make_unique<ui::Layer>(ui::LayerType::LAYER_SOLID_COLOR);
+  stale_content_layer_->SetVisible(false);
 }
 
 DelegatedFrameHost::~DelegatedFrameHost() {
@@ -80,6 +93,9 @@
     const viz::LocalSurfaceId& new_local_surface_id,
     const gfx::Size& new_dip_size,
     bool record_presentation_time) {
+  // Cancel any pending frame eviction and unpause it if paused.
+  frame_eviction_state_ = FrameEvictionState::kNotStarted;
+
   frame_evictor_->SetVisible(true);
   if (record_presentation_time && compositor_) {
     compositor_->RequestPresentationTimeForNextFrame(
@@ -90,6 +106,12 @@
   // TODO(fsamuel): Investigate if there is a better deadline to use here.
   EmbedSurface(new_local_surface_id, new_dip_size,
                cc::DeadlinePolicy::UseDefaultDeadline());
+
+  // Remove stale content that might be displayed.
+  if (stale_content_layer_->has_external_content()) {
+    stale_content_layer_->SetShowSolidColorContent();
+    stale_content_layer_->SetVisible(false);
+  }
 }
 
 bool DelegatedFrameHost::HasSavedFrame() const {
@@ -104,17 +126,35 @@
     const gfx::Rect& src_subrect,
     const gfx::Size& output_size,
     base::OnceCallback<void(const SkBitmap&)> callback) {
+  CopyFromCompositingSurfaceInternal(
+      src_subrect, output_size,
+      viz::CopyOutputRequest::ResultFormat::RGBA_BITMAP,
+      base::BindOnce(
+          [](base::OnceCallback<void(const SkBitmap&)> callback,
+             std::unique_ptr<viz::CopyOutputResult> result) {
+            std::move(callback).Run(result->AsSkBitmap());
+          },
+          std::move(callback)));
+}
+
+void DelegatedFrameHost::CopyFromCompositingSurfaceAsTexture(
+    const gfx::Rect& src_subrect,
+    const gfx::Size& output_size,
+    viz::CopyOutputRequest::CopyOutputRequestCallback callback) {
+  CopyFromCompositingSurfaceInternal(
+      src_subrect, output_size,
+      viz::CopyOutputRequest::ResultFormat::RGBA_TEXTURE, std::move(callback));
+}
+
+void DelegatedFrameHost::CopyFromCompositingSurfaceInternal(
+    const gfx::Rect& src_subrect,
+    const gfx::Size& output_size,
+    viz::CopyOutputRequest::ResultFormat format,
+    viz::CopyOutputRequest::CopyOutputRequestCallback callback) {
   DCHECK(CanCopyFromCompositingSurface());
 
-  std::unique_ptr<viz::CopyOutputRequest> request =
-      std::make_unique<viz::CopyOutputRequest>(
-          viz::CopyOutputRequest::ResultFormat::RGBA_BITMAP,
-          base::BindOnce(
-              [](base::OnceCallback<void(const SkBitmap&)> callback,
-                 std::unique_ptr<viz::CopyOutputResult> result) {
-                std::move(callback).Run(result->AsSkBitmap());
-              },
-              std::move(callback)));
+  auto request =
+      std::make_unique<viz::CopyOutputRequest>(format, std::move(callback));
 
   if (!src_subrect.IsEmpty()) {
     request->set_area(
@@ -344,6 +384,71 @@
 }
 
 void DelegatedFrameHost::EvictDelegatedFrame() {
+  // There is already an eviction request pending.
+  if (frame_eviction_state_ == FrameEvictionState::kPendingEvictionRequests) {
+    frame_evictor_->OnSurfaceDiscarded();
+    return;
+  }
+
+  if (!HasSavedFrame()) {
+    ContinueDelegatedFrameEviction();
+    return;
+  }
+
+  // Requests a copy of the compositing surface of the frame if one doesn't
+  // already exist. The copy (stale content) will be set on the surface until
+  // a new compositor frame is submitted. Setting a stale content prevents blank
+  // white screens from being displayed during various animations such as the
+  // CrOS overview mode.
+  if (client_->ShouldShowStaleContentOnEviction() &&
+      !stale_content_layer_->has_external_content()) {
+    frame_eviction_state_ = FrameEvictionState::kPendingEvictionRequests;
+    auto callback =
+        base::BindOnce(&DelegatedFrameHost::DidCopyStaleContent, GetWeakPtr());
+
+    // NOTE: This will not return any texture on non CrOS platforms as hiding
+    // the window on non CrOS platform disables drawing all together.
+    CopyFromCompositingSurfaceAsTexture(
+        gfx::Rect(),
+        gfx::ScaleToRoundedSize(surface_dip_size_, kFrameContentCaptureQuality),
+        std::move(callback));
+  } else {
+    ContinueDelegatedFrameEviction();
+  }
+  frame_evictor_->OnSurfaceDiscarded();
+}
+
+void DelegatedFrameHost::DidCopyStaleContent(
+    std::unique_ptr<viz::CopyOutputResult> result) {
+  // host may have become visible by the time the request to capture surface is
+  // completed.
+  if (frame_evictor_->visible() || result->IsEmpty())
+    return;
+
+  DCHECK_EQ(result->format(), viz::CopyOutputResult::Format::RGBA_TEXTURE);
+
+  if (frame_eviction_state_ == FrameEvictionState::kPendingEvictionRequests) {
+    frame_eviction_state_ = FrameEvictionState::kNotStarted;
+    ContinueDelegatedFrameEviction();
+  }
+
+  auto transfer_resource = viz::TransferableResource::MakeGL(
+      result->GetTextureResult()->mailbox, GL_LINEAR, GL_TEXTURE_2D,
+      result->GetTextureResult()->sync_token);
+  std::unique_ptr<viz::SingleReleaseCallback> release_callback =
+      result->TakeTextureOwnership();
+
+  if (stale_content_layer_->parent() != client_->DelegatedFrameHostGetLayer())
+    client_->DelegatedFrameHostGetLayer()->Add(stale_content_layer_.get());
+
+  DCHECK(!stale_content_layer_->has_external_content());
+  stale_content_layer_->SetVisible(true);
+  stale_content_layer_->SetBounds(gfx::Rect(surface_dip_size_));
+  stale_content_layer_->SetTransferableResource(
+      transfer_resource, std::move(release_callback), surface_dip_size_);
+}
+
+void DelegatedFrameHost::ContinueDelegatedFrameEviction() {
   // Reset primary surface.
   if (HasPrimarySurface()) {
     client_->DelegatedFrameHostGetLayer()->SetShowSurface(
@@ -365,7 +470,6 @@
     DCHECK(host_frame_sink_manager_);
     host_frame_sink_manager_->EvictSurfaces(surface_ids);
   }
-  frame_evictor_->OnSurfaceDiscarded();
   client_->InvalidateLocalSurfaceIdOnEviction();
 }
 
diff --git a/content/browser/renderer_host/delegated_frame_host.h b/content/browser/renderer_host/delegated_frame_host.h
index 3a855f7..e22ddf5 100644
--- a/content/browser/renderer_host/delegated_frame_host.h
+++ b/content/browser/renderer_host/delegated_frame_host.h
@@ -52,6 +52,7 @@
   virtual float GetDeviceScaleFactor() const = 0;
   virtual void InvalidateLocalSurfaceIdOnEviction() = 0;
   virtual std::vector<viz::SurfaceId> CollectSurfaceIdsForEviction() = 0;
+  virtual bool ShouldShowStaleContentOnEviction() = 0;
 };
 
 // The DelegatedFrameHost is used to host all of the RenderWidgetHostView state
@@ -121,6 +122,10 @@
   bool HasSavedFrame() const;
   void AttachToCompositor(ui::Compositor* compositor);
   void DetachFromCompositor();
+
+  // Copies |src_subrect| from the compositing surface into a bitmap (first
+  // overload) or texture (second overload). |output_size| specifies the size of
+  // the output bitmap or texture.
   // Note: |src_subrect| is specified in DIP dimensions while |output_size|
   // expects pixels. If |src_subrect| is empty, the entire surface area is
   // copied.
@@ -128,6 +133,11 @@
       const gfx::Rect& src_subrect,
       const gfx::Size& output_size,
       base::OnceCallback<void(const SkBitmap&)> callback);
+  void CopyFromCompositingSurfaceAsTexture(
+      const gfx::Rect& src_subrect,
+      const gfx::Size& output_size,
+      viz::CopyOutputRequest::CopyOutputRequestCallback callback);
+
   bool CanCopyFromCompositingSurface() const;
   const viz::FrameSinkId& frame_sink_id() const { return frame_sink_id_; }
 
@@ -177,19 +187,31 @@
 
  private:
   friend class DelegatedFrameHostClient;
-  FRIEND_TEST_ALL_PREFIXES(RenderWidgetHostViewAuraTest,
-                           SkippedDelegatedFrames);
-  FRIEND_TEST_ALL_PREFIXES(RenderWidgetHostViewAuraTest,
-                           DiscardDelegatedFramesWithLocking);
+  FRIEND_TEST_ALL_PREFIXES(RenderWidgetHostViewAuraBrowserTest,
+                           StaleFrameContentOnEvictionNormal);
+  FRIEND_TEST_ALL_PREFIXES(RenderWidgetHostViewAuraBrowserTest,
+                           StaleFrameContentOnEvictionRejected);
+  FRIEND_TEST_ALL_PREFIXES(RenderWidgetHostViewAuraBrowserTest,
+                           StaleFrameContentOnEvictionNone);
 
   // FrameEvictorClient implementation.
   void EvictDelegatedFrame() override;
 
+  void DidCopyStaleContent(std::unique_ptr<viz::CopyOutputResult> result);
+
+  void ContinueDelegatedFrameEviction();
+
   SkColor GetGutterColor() const;
 
   void CreateCompositorFrameSinkSupport();
   void ResetCompositorFrameSinkSupport();
 
+  void CopyFromCompositingSurfaceInternal(
+      const gfx::Rect& src_subrect,
+      const gfx::Size& output_size,
+      viz::CopyOutputRequest::ResultFormat format,
+      viz::CopyOutputRequest::CopyOutputRequestCallback callback);
+
   const viz::FrameSinkId frame_sink_id_;
   DelegatedFrameHostClient* const client_;
   const bool enable_viz_;
@@ -225,6 +247,18 @@
   bool seen_first_activation_ = false;
 #endif
 
+  enum class FrameEvictionState {
+    kNotStarted = 0,          // Frame eviction is ready.
+    kPendingEvictionRequests  // Frame eviction is paused with pending requests.
+  };
+
+  FrameEvictionState frame_eviction_state_ = FrameEvictionState::kNotStarted;
+
+  // Layer responsible for displaying the stale content for the DFHC when the
+  // actual web content frame has been evicted. This will be reset when a new
+  // compositor frame is submitted.
+  std::unique_ptr<ui::Layer> stale_content_layer_;
+
   base::WeakPtrFactory<DelegatedFrameHost> weak_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(DelegatedFrameHost);
diff --git a/content/browser/renderer_host/delegated_frame_host_client_aura.cc b/content/browser/renderer_host/delegated_frame_host_client_aura.cc
index 75a43f1..f002a3c9 100644
--- a/content/browser/renderer_host/delegated_frame_host_client_aura.cc
+++ b/content/browser/renderer_host/delegated_frame_host_client_aura.cc
@@ -64,4 +64,8 @@
   return render_widget_host_view_->host()->CollectSurfaceIdsForEviction();
 }
 
+bool DelegatedFrameHostClientAura::ShouldShowStaleContentOnEviction() {
+  return render_widget_host_view_->ShouldShowStaleContentOnEviction();
+}
+
 }  // namespace content
diff --git a/content/browser/renderer_host/delegated_frame_host_client_aura.h b/content/browser/renderer_host/delegated_frame_host_client_aura.h
index 40e1f52e..e7c62fb 100644
--- a/content/browser/renderer_host/delegated_frame_host_client_aura.h
+++ b/content/browser/renderer_host/delegated_frame_host_client_aura.h
@@ -35,6 +35,7 @@
   float GetDeviceScaleFactor() const override;
   void InvalidateLocalSurfaceIdOnEviction() override;
   std::vector<viz::SurfaceId> CollectSurfaceIdsForEviction() override;
+  bool ShouldShowStaleContentOnEviction() override;
 
  private:
   RenderWidgetHostViewAura* render_widget_host_view_;
diff --git a/content/browser/renderer_host/render_widget_host_delegate.cc b/content/browser/renderer_host/render_widget_host_delegate.cc
index efd2a7a..3b51afe5 100644
--- a/content/browser/renderer_host/render_widget_host_delegate.cc
+++ b/content/browser/renderer_host/render_widget_host_delegate.cc
@@ -86,6 +86,10 @@
   return false;
 }
 
+bool RenderWidgetHostDelegate::ShouldShowStaleContentOnEviction() {
+  return false;
+}
+
 blink::WebDisplayMode RenderWidgetHostDelegate::GetDisplayMode(
     RenderWidgetHostImpl* render_widget_host) const {
   return blink::kWebDisplayModeBrowser;
diff --git a/content/browser/renderer_host/render_widget_host_delegate.h b/content/browser/renderer_host/render_widget_host_delegate.h
index 83878e9..aef0cf02 100644
--- a/content/browser/renderer_host/render_widget_host_delegate.h
+++ b/content/browser/renderer_host/render_widget_host_delegate.h
@@ -201,6 +201,11 @@
   // Returns whether the associated tab is in fullscreen mode.
   virtual bool IsFullscreenForCurrentTab();
 
+  // Returns true if the widget's frame content needs to be stored before
+  // eviction and displayed until a new frame is generated. If false, a white
+  // solid color is displayed instead.
+  virtual bool ShouldShowStaleContentOnEviction();
+
   // Returns the display mode for the view.
   virtual blink::WebDisplayMode GetDisplayMode(
       RenderWidgetHostImpl* render_widget_host) const;
diff --git a/content/browser/renderer_host/render_widget_host_impl.cc b/content/browser/renderer_host/render_widget_host_impl.cc
index 72b1edb..17a49b9 100644
--- a/content/browser/renderer_host/render_widget_host_impl.cc
+++ b/content/browser/renderer_host/render_widget_host_impl.cc
@@ -674,6 +674,10 @@
     view_->OnRenderWidgetInit();
 }
 
+bool RenderWidgetHostImpl::ShouldShowStaleContentOnEviction() {
+  return delegate_->ShouldShowStaleContentOnEviction();
+}
+
 void RenderWidgetHostImpl::ShutdownAndDestroyWidget(bool also_delete) {
   CancelKeyboardLock();
   RejectMouseLockOrUnlockIfNecessary();
diff --git a/content/browser/renderer_host/render_widget_host_impl.h b/content/browser/renderer_host/render_widget_host_impl.h
index 768c9f0..3c8816a 100644
--- a/content/browser/renderer_host/render_widget_host_impl.h
+++ b/content/browser/renderer_host/render_widget_host_impl.h
@@ -277,6 +277,9 @@
   // Initializes a RenderWidgetHost that is attached to a RenderFrameHost.
   void InitForFrame();
 
+  // Returns true if the frame content needs be stored before being evicted.
+  bool ShouldShowStaleContentOnEviction();
+
   // Signal whether this RenderWidgetHost is owned by a RenderFrameHost, in
   // which case it does not do self-deletion.
   void set_owned_by_render_frame_host(bool owned_by_rfh) {
diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc
index 6003c62..5df169e 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura.cc
@@ -720,6 +720,10 @@
 #endif
 }
 
+bool RenderWidgetHostViewAura::ShouldShowStaleContentOnEviction() {
+  return host()->ShouldShowStaleContentOnEviction();
+}
+
 gfx::Rect RenderWidgetHostViewAura::GetViewBounds() const {
   return window_->GetBoundsInScreen();
 }
diff --git a/content/browser/renderer_host/render_widget_host_view_aura.h b/content/browser/renderer_host/render_widget_host_view_aura.h
index 77208f6..44e7879 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura.h
+++ b/content/browser/renderer_host/render_widget_host_view_aura.h
@@ -367,6 +367,7 @@
   friend class DelegatedFrameHostClientAura;
   friend class InputMethodAuraTestBase;
   friend class RenderWidgetHostViewAuraTest;
+  friend class RenderWidgetHostViewAuraBrowserTest;
   friend class RenderWidgetHostViewAuraCopyRequestTest;
   friend class TestInputMethodObserver;
 #if defined(OS_WIN)
@@ -463,6 +464,12 @@
 
   void CreateAuraWindow(aura::client::WindowType type);
 
+  // Returns true if a stale frame content needs to be set for the current RWHV.
+  // This is primarily useful during various CrOS animations to prevent showing
+  // a white screen and instead showing a snapshot of the frame content that
+  // was most recently evicted.
+  bool ShouldShowStaleContentOnEviction();
+
   void CreateDelegatedFrameHostClient();
 
   void UpdateCursorIfOverSelf();
diff --git a/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc b/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
new file mode 100644
index 0000000..cba3ee9
--- /dev/null
+++ b/content/browser/renderer_host/render_widget_host_view_aura_browsertest.cc
@@ -0,0 +1,196 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/render_widget_host_view_aura.h"
+
+#include "base/bind.h"
+#include "base/macros.h"
+#include "base/run_loop.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "content/browser/renderer_host/delegated_frame_host.h"
+#include "content/browser/renderer_host/render_widget_host_impl.h"
+#include "content/browser/renderer_host/render_widget_host_view_aura.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_view_host.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_contents_delegate.h"
+#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
+#include "content/shell/browser/shell.h"
+
+namespace content {
+namespace {
+
+#if defined(OS_CHROMEOS)
+const char kMinimalPageDataURL[] =
+    "data:text/html,<html><head></head><body>Hello, world</body></html>";
+
+// Run the current message loop for a short time without unwinding the current
+// call stack.
+void GiveItSomeTime() {
+  base::RunLoop run_loop;
+  base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+      FROM_HERE, run_loop.QuitClosure(),
+      base::TimeDelta::FromMilliseconds(250));
+  run_loop.Run();
+}
+#endif  // defined(OS_CHROMEOS)
+
+class FakeWebContentsDelegate : public WebContentsDelegate {
+ public:
+  FakeWebContentsDelegate() = default;
+  ~FakeWebContentsDelegate() override = default;
+
+  void SetShowStaleContentOnEviction(bool value) {
+    show_stale_content_on_eviction_ = value;
+  }
+
+  bool ShouldShowStaleContentOnEviction(WebContents* source) override {
+    return show_stale_content_on_eviction_;
+  }
+
+ private:
+  bool show_stale_content_on_eviction_ = false;
+
+  DISALLOW_COPY_AND_ASSIGN(FakeWebContentsDelegate);
+};
+
+}  // namespace
+
+class RenderWidgetHostViewAuraBrowserTest : public ContentBrowserTest {
+ public:
+  RenderViewHost* GetRenderViewHost() const {
+    RenderViewHost* const rvh = shell()->web_contents()->GetRenderViewHost();
+    CHECK(rvh);
+    return rvh;
+  }
+
+  RenderWidgetHostViewAura* GetRenderWidgetHostView() const {
+    return static_cast<RenderWidgetHostViewAura*>(
+        GetRenderViewHost()->GetWidget()->GetView());
+  }
+
+  DelegatedFrameHost* GetDelegatedFrameHost() const {
+    return GetRenderWidgetHostView()->delegated_frame_host_.get();
+  }
+};
+
+#if defined(OS_CHROMEOS)
+IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewAuraBrowserTest,
+                       StaleFrameContentOnEvictionNormal) {
+  NavigateToURL(shell(), GURL(kMinimalPageDataURL));
+
+  // Wait for first frame activation when a surface is embedded.
+  while (!GetDelegatedFrameHost()->HasSavedFrame())
+    GiveItSomeTime();
+
+  FakeWebContentsDelegate delegate;
+  delegate.SetShowStaleContentOnEviction(true);
+  shell()->web_contents()->SetDelegate(&delegate);
+
+  // Initially there should be no stale content set.
+  EXPECT_FALSE(
+      GetDelegatedFrameHost()->stale_content_layer_->has_external_content());
+  EXPECT_EQ(GetDelegatedFrameHost()->frame_eviction_state_,
+            DelegatedFrameHost::FrameEvictionState::kNotStarted);
+
+  // Hide the view and evict the frame. This should trigger a copy of the stale
+  // frame content.
+  GetRenderWidgetHostView()->Hide();
+  static_cast<viz::FrameEvictorClient*>(GetDelegatedFrameHost())
+      ->EvictDelegatedFrame();
+  EXPECT_EQ(GetDelegatedFrameHost()->frame_eviction_state_,
+            DelegatedFrameHost::FrameEvictionState::kPendingEvictionRequests);
+
+  // Wait until the stale frame content is copied and set onto the layer.
+  while (!GetDelegatedFrameHost()->stale_content_layer_->has_external_content())
+    GiveItSomeTime();
+
+  EXPECT_EQ(GetDelegatedFrameHost()->frame_eviction_state_,
+            DelegatedFrameHost::FrameEvictionState::kNotStarted);
+
+  // Unhidding the view should reset the stale content layer to show the new
+  // frame content.
+  GetRenderWidgetHostView()->Show();
+  EXPECT_FALSE(
+      GetDelegatedFrameHost()->stale_content_layer_->has_external_content());
+}
+
+IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewAuraBrowserTest,
+                       StaleFrameContentOnEvictionRejected) {
+  NavigateToURL(shell(), GURL(kMinimalPageDataURL));
+
+  // Wait for first frame activation when a surface is embedded.
+  while (!GetDelegatedFrameHost()->HasSavedFrame())
+    GiveItSomeTime();
+
+  FakeWebContentsDelegate delegate;
+  delegate.SetShowStaleContentOnEviction(true);
+  shell()->web_contents()->SetDelegate(&delegate);
+
+  // Initially there should be no stale content set.
+  EXPECT_FALSE(
+      GetDelegatedFrameHost()->stale_content_layer_->has_external_content());
+  EXPECT_EQ(GetDelegatedFrameHost()->frame_eviction_state_,
+            DelegatedFrameHost::FrameEvictionState::kNotStarted);
+
+  // Hide the view and evict the frame. This should trigger a copy of the stale
+  // frame content.
+  GetRenderWidgetHostView()->Hide();
+  static_cast<viz::FrameEvictorClient*>(GetDelegatedFrameHost())
+      ->EvictDelegatedFrame();
+  EXPECT_EQ(GetDelegatedFrameHost()->frame_eviction_state_,
+            DelegatedFrameHost::FrameEvictionState::kPendingEvictionRequests);
+
+  GetRenderWidgetHostView()->Show();
+  EXPECT_EQ(GetDelegatedFrameHost()->frame_eviction_state_,
+            DelegatedFrameHost::FrameEvictionState::kNotStarted);
+
+  // Wait until the stale frame content is copied and the result callback is
+  // complete.
+  GiveItSomeTime();
+
+  // This should however not set the stale content as the view is visible and
+  // new frames are being submitted.
+  EXPECT_FALSE(
+      GetDelegatedFrameHost()->stale_content_layer_->has_external_content());
+}
+
+IN_PROC_BROWSER_TEST_F(RenderWidgetHostViewAuraBrowserTest,
+                       StaleFrameContentOnEvictionNone) {
+  NavigateToURL(shell(), GURL(kMinimalPageDataURL));
+
+  // Wait for first frame activation when a surface is embedded.
+  while (!GetDelegatedFrameHost()->HasSavedFrame())
+    GiveItSomeTime();
+
+  FakeWebContentsDelegate delegate;
+  delegate.SetShowStaleContentOnEviction(false);
+  shell()->web_contents()->SetDelegate(&delegate);
+
+  // Initially there should be no stale content set.
+  EXPECT_FALSE(
+      GetDelegatedFrameHost()->stale_content_layer_->has_external_content());
+  EXPECT_EQ(GetDelegatedFrameHost()->frame_eviction_state_,
+            DelegatedFrameHost::FrameEvictionState::kNotStarted);
+
+  // Hide the view and evict the frame. This should not trigger a copy of the
+  // stale frame content as the WebContentDelegate returns false.
+  GetRenderWidgetHostView()->Hide();
+  static_cast<viz::FrameEvictorClient*>(GetDelegatedFrameHost())
+      ->EvictDelegatedFrame();
+
+  EXPECT_EQ(GetDelegatedFrameHost()->frame_eviction_state_,
+            DelegatedFrameHost::FrameEvictionState::kNotStarted);
+
+  // Wait for a while to ensure any copy requests that were sent out are not
+  // completed. There shouldnt be any requests sent however.
+  GiveItSomeTime();
+  EXPECT_FALSE(
+      GetDelegatedFrameHost()->stale_content_layer_->has_external_content());
+}
+#endif  // #if defined(OS_CHROMEOS)
+
+}  // namespace content
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index 4bdfeba..88acf7d 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -2508,6 +2508,10 @@
   return delegate_ ? delegate_->IsFullscreenForTabOrPending(this) : false;
 }
 
+bool WebContentsImpl::ShouldShowStaleContentOnEviction() {
+  return GetDelegate() && GetDelegate()->ShouldShowStaleContentOnEviction(this);
+}
+
 bool WebContentsImpl::IsFullscreen() {
   return IsFullscreenForCurrentTab();
 }
diff --git a/content/browser/web_contents/web_contents_impl.h b/content/browser/web_contents/web_contents_impl.h
index 3e652d8..d349a650 100644
--- a/content/browser/web_contents/web_contents_impl.h
+++ b/content/browser/web_contents/web_contents_impl.h
@@ -450,6 +450,7 @@
   bool WasEverAudible() override;
   void GetManifest(GetManifestCallback callback) override;
   bool IsFullscreenForCurrentTab() override;
+  bool ShouldShowStaleContentOnEviction() override;
   void ExitFullscreen(bool will_cause_resize) override;
   void ResumeLoadingCreatedWebContents() override;
   void SetIsOverlayContent(bool is_overlay_content) override;
diff --git a/content/public/browser/web_contents_delegate.cc b/content/public/browser/web_contents_delegate.cc
index 8981e93..f7ac4c2 100644
--- a/content/public/browser/web_contents_delegate.cc
+++ b/content/public/browser/web_contents_delegate.cc
@@ -300,4 +300,8 @@
   return new_contents;
 }
 
+bool WebContentsDelegate::ShouldShowStaleContentOnEviction(
+    WebContents* source) {
+  return false;
+}
 }  // namespace content
diff --git a/content/public/browser/web_contents_delegate.h b/content/public/browser/web_contents_delegate.h
index d7ce001..bdef12d 100644
--- a/content/public/browser/web_contents_delegate.h
+++ b/content/public/browser/web_contents_delegate.h
@@ -641,6 +641,11 @@
       bool did_start_load,
       bool did_finish_load);
 
+  // Returns true if the widget's frame content needs to be stored before
+  // eviction and displayed until a new frame is generated. If false, a white
+  // solid color is displayed instead.
+  virtual bool ShouldShowStaleContentOnEviction(WebContents* source);
+
  protected:
   virtual ~WebContentsDelegate();
 
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 3df2eb0..925911a 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -861,6 +861,7 @@
     "../browser/renderer_host/render_process_host_browsertest.cc",
     "../browser/renderer_host/render_view_host_browsertest.cc",
     "../browser/renderer_host/render_widget_host_browsertest.cc",
+    "../browser/renderer_host/render_widget_host_view_aura_browsertest.cc",
     "../browser/renderer_host/render_widget_host_view_browsertest.cc",
     "../browser/renderer_host/render_widget_host_view_child_frame_browsertest.cc",
     "../browser/renderer_host/render_widget_host_view_mac_browsertest.mm",
@@ -1270,6 +1271,7 @@
     sources -= [
       "../browser/accessibility/touch_accessibility_aura_browsertest.cc",
       "../browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc",
+      "../browser/renderer_host/render_widget_host_view_aura_browsertest.cc",
       "../browser/web_contents/web_contents_view_aura_browsertest.cc",
     ]
   }
diff --git a/ui/compositor/layer.cc b/ui/compositor/layer.cc
index a8aa03c6..b1c724f 100644
--- a/ui/compositor/layer.cc
+++ b/ui/compositor/layer.cc
@@ -219,6 +219,17 @@
 std::unique_ptr<Layer> Layer::Mirror() {
   auto mirror = Clone();
   mirrors_.emplace_back(std::make_unique<LayerMirror>(this, mirror.get()));
+
+  if (!transfer_resource_.mailbox_holder.mailbox.IsZero()) {
+    // Send an empty release callback because we don't want the resource to be
+    // freed up until the original layer releases it.
+    mirror->SetTransferableResource(
+        transfer_resource_,
+        viz::SingleReleaseCallback::Create(base::BindOnce(
+            [](const gpu::SyncToken& sync_token, bool is_lost) {})),
+        frame_size_in_dip_);
+  }
+
   return mirror;
 }
 
@@ -756,6 +767,16 @@
   transfer_release_callback_ = std::move(release_callback);
   transfer_resource_ = resource;
   SetTextureSize(texture_size_in_dip);
+
+  for (const auto& mirror : mirrors_) {
+    // The release callbacks should be empty as only the source layer
+    // should be able to release the texture resource.
+    mirror->dest()->SetTransferableResource(
+        transfer_resource_,
+        viz::SingleReleaseCallback::Create(base::BindOnce(
+            [](const gpu::SyncToken& sync_token, bool is_lost) {})),
+        frame_size_in_dip_);
+  }
 }
 
 void Layer::SetTextureSize(gfx::Size texture_size_in_dip) {
@@ -860,6 +881,9 @@
     transfer_release_callback_.reset();
   }
   RecomputeDrawsContentAndUVRect();
+
+  for (const auto& mirror : mirrors_)
+    mirror->dest()->SetShowSolidColorContent();
 }
 
 void Layer::UpdateNinePatchLayerImage(const gfx::ImageSkia& image) {
diff --git a/ui/compositor/layer_unittest.cc b/ui/compositor/layer_unittest.cc
index a4eaa87a..c416e42d 100644
--- a/ui/compositor/layer_unittest.cc
+++ b/ui/compositor/layer_unittest.cc
@@ -2052,6 +2052,57 @@
   EXPECT_EQ(surface_id, surface->surface_id());
 }
 
+TEST_F(LayerWithDelegateTest, TransferableResourceMirroring) {
+  std::unique_ptr<Layer> layer(CreateLayer(LAYER_SOLID_COLOR));
+
+  auto resource = viz::TransferableResource::MakeGL(
+      gpu::Mailbox::Generate(), GL_LINEAR, GL_TEXTURE_2D, gpu::SyncToken());
+  bool release_callback_run = false;
+
+  layer->SetTransferableResource(
+      resource,
+      viz::SingleReleaseCallback::Create(
+          base::BindOnce(ReturnMailbox, &release_callback_run)),
+      gfx::Size(10, 10));
+  EXPECT_FALSE(release_callback_run);
+  EXPECT_TRUE(layer->has_external_content());
+
+  auto mirror = layer->Mirror();
+  EXPECT_TRUE(mirror->has_external_content());
+
+  // Clearing the resource on a mirror layer should not release the source layer
+  // resource.
+  mirror.reset();
+  EXPECT_FALSE(release_callback_run);
+
+  mirror = layer->Mirror();
+  EXPECT_TRUE(mirror->has_external_content());
+
+  // Clearing the transferable resource on the source layer should clear it from
+  // the mirror layer as well.
+  layer->SetShowSolidColorContent();
+  EXPECT_TRUE(release_callback_run);
+  EXPECT_FALSE(layer->has_external_content());
+  EXPECT_FALSE(mirror->has_external_content());
+
+  resource = viz::TransferableResource::MakeGL(
+      gpu::Mailbox::Generate(), GL_LINEAR, GL_TEXTURE_2D, gpu::SyncToken());
+  release_callback_run = false;
+
+  // Setting a transferable resource on the source layer should set it on the
+  // mirror layers as well.
+  layer->SetTransferableResource(
+      resource,
+      viz::SingleReleaseCallback::Create(
+          base::BindOnce(ReturnMailbox, &release_callback_run)),
+      gfx::Size(10, 10));
+  EXPECT_FALSE(release_callback_run);
+  EXPECT_TRUE(layer->has_external_content());
+  EXPECT_TRUE(mirror->has_external_content());
+
+  layer.reset();
+}
+
 // Verifies that layer filters still attached after changing implementation
 // layer.
 TEST_F(LayerWithDelegateTest, LayerFiltersSurvival) {