[TTVD] Refine OutputSurfaceProxy threading 15/315615/5
authorJakub Gajownik <j.gajownik2@samsung.com>
Wed, 20 Nov 2024 17:05:43 +0000 (18:05 +0100)
committerBot Blink <blinkbot@samsung.com>
Tue, 3 Dec 2024 15:21:32 +0000 (15:21 +0000)
OutputSurfaceProxy has rather complicated logic regarding
threading, as it must properly synchronize logic between
3 of them, without unnecessary locking any of them.

This CL adds unit tests for this class and introduces needed
changes to fix all the encountered threading/logic issues,
such as:
- stop worker thread before destroying other fields to
  ensure they are not used from worker thread,
- ignore initialization done on wrong sequence,
- ensure notification is not triggered multiple
  times for single sequence,
- fix disappearing notification after changing context
  for single collection,
- ensure previous collection notification is triggered
  on proper sequence,
- fix initialization might not finish after changing
  sequences.

Bug: https://jira-eu.sec.samsung.net/browse/VDGAME-618
Change-Id: I4893807b7345c3e619db5f83caf38221b4b1409a
Signed-off-by: Jakub Gajownik <j.gajownik2@samsung.com>
tizen_src/chromium_impl/media/test/BUILD.gn
tizen_src/chromium_impl/ui/ozone/platform/efl/output_surface_proxy.cc
tizen_src/chromium_impl/ui/ozone/platform/efl/output_surface_proxy.h
tizen_src/chromium_impl/ui/ozone/platform/efl/output_surface_proxy_test.cc [new file with mode: 0644]

index 1b6d24688cf733f58c22e3a80e13657c9a12439b..fab27f117a2377aecf3f06880ebe7181e3255947 100644 (file)
@@ -29,6 +29,7 @@ test("tizen_media_unittests") {
     "//media/filters/tizen/decoder_promotion_test.cc",
     "//media/filters/tizen/lazy_frame_ranges_test.cc",
     "//media/filters/tizen/ttvd_video_decoder_test.cc",
+    "//tizen_src/chromium_impl/ui/ozone/platform/efl/output_surface_proxy_test.cc",
   ]
 
   if (tizen_tv_upstream_multimedia_omx || tizen_tv_upstream_multimedia_omx_ppi) {
index f216ed657f1eca0aaf952e5d654d8c976aac840f..ca816e7a11633d7d2b794304658b814b05799e2e 100644 (file)
@@ -46,6 +46,11 @@ OutputSurfaceProxy::OutputSurfaceProxy(
 
 OutputSurfaceProxy::~OutputSurfaceProxy() {
   TIZEN_MEDIA_LOG(INFO);
+  // Ensure thread is stopped before deleting anything else, worker thread might
+  // use some of variables.
+  if (worker_thread_.IsRunning()) {
+    worker_thread_.Stop();
+  }
 }
 
 bool OutputSurfaceProxy::Initialize(
@@ -107,6 +112,12 @@ void OutputSurfaceProxy::InitializationDone(bool success) {
   TRACE_EVENT0("viz", "OutputSurfaceProxy::InitializationDone");
   base::AutoLock client_auto_lock(client_lock_);
 
+  // Old callback was triggered, ignore. It should be called once
+  // again on proper sequence.
+  if (client_accessor_ && !client_accessor_->IsCurrent()) {
+    return;
+  }
+
   TIZEN_MEDIA_LOG(INFO) << "Initialization done: " << success;
   initialized_on_client_ = success;
 
@@ -114,6 +125,8 @@ void OutputSurfaceProxy::InitializationDone(bool success) {
     return;
   }
 
+  const auto prev_last_result_on_client = last_result_on_client_;
+
   TIZEN_MEDIA_LOG(INFO) << "Has pending prepare to render: "
                         << pending_prepare_to_render_on_client_.has_value();
   if (!success) {
@@ -137,7 +150,10 @@ void OutputSurfaceProxy::InitializationDone(bool success) {
     }
   }
   pending_prepare_to_render_on_client_.reset();
-  mode_cb_on_client_.Run(last_result_on_client_);
+  // Notification should be only done when there was actual change.
+  if (prev_last_result_on_client != last_result_on_client_) {
+    mode_cb_on_client_.Run(last_result_on_client_);
+  }
 }
 
 void OutputSurfaceProxy::InitializeOnWorker(
@@ -217,7 +233,13 @@ gfx::VideoOutputMode OutputSurfaceProxy::PrepareToRender(
   TRACE_EVENT0("viz", "OutputSurfaceProxy::PrepareToRender");
   base::AutoLock client_auto_lock(client_lock_);
 
-  MakeCurrent();
+  scoped_refptr<base::SequencedTaskRunner> old_task_runner;
+  if (client_accessor_) {
+    old_task_runner = client_accessor_->client_task_runner;
+  }
+  // Do not invalidate old context, as we might trigger notification
+  // later on in this method.
+  auto prev_context_releaser = MakeCurrent();
 
   // Check for errors at first to avoid posting task.
   if (last_result_on_client_ == gfx::VideoOutputMode::kTexture) {
@@ -236,7 +258,13 @@ gfx::VideoOutputMode OutputSurfaceProxy::PrepareToRender(
     // Notify previous collection about mode change. It might wait to reach
     // overlay mode (like preparation during decoder start).
     if (mode_cb_on_client_) {
-      mode_cb_on_client_.Run(gfx::VideoOutputMode::kTexture);
+      if (prev_context_releaser) {
+        old_task_runner->PostTask(
+            FROM_HERE,
+            base::BindOnce(mode_cb_on_client_, gfx::VideoOutputMode::kTexture));
+      } else {
+        mode_cb_on_client_.Run(gfx::VideoOutputMode::kTexture);
+      }
     }
     collection_token_on_client_ = collection_token;
     last_result_on_client_ = gfx::VideoOutputMode::kTransitionUnmuting;
@@ -253,15 +281,26 @@ gfx::VideoOutputMode OutputSurfaceProxy::PrepareToRender(
   auto bound_mode_cb = base::BindPostTaskToCurrentDefault(base::BindRepeating(
       &OutputSurfaceProxy::UpdateMode, client_accessor_->weak_ptr));
   if (!initialized_on_client_.has_value()) {
-    if (pending_prepare_to_render_on_client_ &&
-        pending_prepare_to_render_on_client_->mode_cb) {
-      TIZEN_MEDIA_LOG(VERBOSE) << "Notify old pending prepare of stealing";
-      pending_prepare_to_render_on_client_->mode_cb.Run(
-          gfx::VideoOutputMode::kTexture);
-    }
+    // Note that previous collection was already notified, so no need
+    // to do this again.
     TIZEN_MEDIA_LOG(VERBOSE) << "Set pending prepare to render";
     pending_prepare_to_render_on_client_ = {collection_token,
                                             can_render_texture, bound_mode_cb};
+
+    // Initialization is ongoing and it was potentially started on different
+    // sequence (context must have changed then), so we cannot invalidate
+    // weak ptr passed with the initialization callback.
+    if (prev_context_releaser) {
+      TIZEN_MEDIA_LOG(VERBOSE) << "Context changed";
+      base::AutoLock auto_lock(lock_);
+      while (!initialized_) {
+        TIZEN_MEDIA_LOG(INFO) << "Still initializing, wait";
+        initialization_done_.Wait();
+      }
+      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+          FROM_HERE, base::BindOnce(&OutputSurfaceProxy::InitializationDone,
+                                    client_accessor_->weak_ptr, initialized_));
+    }
   } else if (*initialized_on_client_) {
     auto preparation_closure = base::BindOnce(
         &OutputSurfaceProxy::PrepareToRenderOnWorker, weak_this_on_worker_,
@@ -312,10 +351,8 @@ void OutputSurfaceProxy::PrepareToRenderOnWorker(
   can_render_texture_ = can_render_texture;
 
   auto result = impl_->PrepareToRender(collection_token, can_render_texture);
-  if (result != last_result_on_worker_) {
-    last_result_on_worker_ = result;
-    mode_cb_on_worker_.Run(last_result_on_worker_);
-  }
+  last_result_on_worker_ = result;
+  mode_cb_on_worker_.Run(last_result_on_worker_);
 
   // Initializing output surface is not immediate process and it might
   // take few steps for it to being able to properly render frames.
@@ -435,6 +472,10 @@ base::TokenCB OutputSurfaceProxy::ResourceConflict() {
 void OutputSurfaceProxy::UpdateMode(gfx::VideoOutputMode mode) {
   TRACE_EVENT0("viz", "OutputSurfaceProxy::UpdateMode");
   base::AutoLock client_auto_lock(client_lock_);
+  // Don't need to notify same client multiple time about same mode.
+  if (last_result_on_client_ == mode) {
+    return;
+  }
   last_result_on_client_ = mode;
   mode_cb_on_client_.Run(mode);
 }
@@ -445,16 +486,21 @@ void OutputSurfaceProxy::FirstPreparationFinishedOnClient() {
   preparing_on_client_ = false;
 }
 
-void OutputSurfaceProxy::MakeCurrent() {
+base::ScopedClosureRunner OutputSurfaceProxy::MakeCurrent() {
   if (client_accessor_ && client_accessor_->IsCurrent()) {
-    return;
+    return {};
   }
 
+  base::ScopedClosureRunner releaser;
   if (client_accessor_) {
-    auto task_runner = client_accessor_->client_task_runner;
-    task_runner->DeleteSoon(FROM_HERE, std::move(client_accessor_));
+    releaser = base::ScopedClosureRunner(base::BindPostTask(
+        client_accessor_->client_task_runner,
+        base::BindOnce([](std::unique_ptr<ClientThreadAccessor>) {},
+                       std::move(client_accessor_)),
+        FROM_HERE));
   }
   client_accessor_ = ClientThreadAccessor::Create(this);
+  return releaser;
 }
 
 void OutputSurfaceProxy::UpdateSurfaceOnWorker(
index 30f7fbb25a3e9cdbba4843eec9488538eb802d5b..97a9f7f9dd376e29a52b00677c07f8f5eafdab74 100644 (file)
@@ -50,6 +50,10 @@ class OutputSurfaceProxy
 
   Plane plane() const { return plane_; }
 
+  scoped_refptr<base::SequencedTaskRunner> WorkerTaskRunnerForTesting() {
+    return worker_thread_.task_runner();
+  }
+
  private:
   friend class base::RefCountedThreadSafe<OutputSurfaceProxy>;
   ~OutputSurfaceProxy() override;
@@ -61,7 +65,7 @@ class OutputSurfaceProxy
   void InitializationDone(bool success);
   void UpdateMode(gfx::VideoOutputMode mode);
   void FirstPreparationFinishedOnClient();
-  void MakeCurrent();
+  base::ScopedClosureRunner MakeCurrent();
 
   // Only on worker thread.
   void InitializeOnWorker(int device_id,
diff --git a/tizen_src/chromium_impl/ui/ozone/platform/efl/output_surface_proxy_test.cc b/tizen_src/chromium_impl/ui/ozone/platform/efl/output_surface_proxy_test.cc
new file mode 100644 (file)
index 0000000..d068ab0
--- /dev/null
@@ -0,0 +1,687 @@
+// Copyright 2024 Samsung Electronics Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/ozone/platform/efl/output_surface_proxy.h"
+#include "base/test/mock_callback.h"
+#include "base/test/scoped_run_loop_timeout.h"
+#include "base/test/task_environment.h"
+#include "base/tizen/global_resource_manager.h"
+#include "base/tizen/resource_manager.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace base {
+class MockResourceManager : public ResourceManager {
+ public:
+  MockResourceManager() = default;
+  ~MockResourceManager() override = default;
+  MOCK_METHOD(absl::optional<AllocatedResource>,
+              AllocateResource,
+              (int, int, ReleaseCB));
+  MOCK_METHOD(bool, CanAllocate, (int, int));
+  MOCK_METHOD(bool, SetPriority, (int));
+  MOCK_METHOD(bool, IsCategorySupported, (int));
+  MOCK_METHOD(int, GetJpegCategoryId, (const char*, int));
+  MOCK_METHOD(int,
+              GetCapableVideoCategoryId,
+              (const char*, int, int, int, int, int, PreferTexturingSupport));
+  MOCK_METHOD(bool, HasUHDVideoDecoder, ());
+};
+
+class SingleAllocationResourceManager : public MockResourceManager {
+ public:
+  SingleAllocationResourceManager() {
+    base::SetGlobalResourceManagerForTesting(this);
+
+    EXPECT_CALL(*this, SetPriority(testing::_))
+        .WillRepeatedly(testing::Return(true));
+    EXPECT_CALL(*this, HasUHDVideoDecoder())
+        .WillRepeatedly(testing::Return(true));
+    EXPECT_CALL(*this, IsCategorySupported(testing::_))
+        .WillRepeatedly(testing::Return(true));
+
+    int category = 1;
+    EXPECT_CALL(*this, GetCapableVideoCategoryId(
+                           testing::_, testing::_, testing::_, testing::_,
+                           testing::_, testing::_, testing::_))
+        .WillRepeatedly([&]() { return category++; });
+    EXPECT_CALL(*this, AllocateResource(testing::_, testing::_, testing::_))
+        .WillOnce([](int, int, base::ResourceManager::ReleaseCB)
+                      -> absl::optional<base::AllocatedResource> {
+          return base::AllocatedResource(
+              base::MockResourceManager::CreateTokenForTesting(
+                  1, base::DoNothing()),
+              "testing", 0);
+        });
+  }
+  ~SingleAllocationResourceManager() override = default;
+};
+}  // namespace base
+
+namespace ui {
+
+class MockOutputSurfaceScaler : public OutputSurfaceScaler {
+ public:
+  MockOutputSurfaceScaler() = default;
+  ~MockOutputSurfaceScaler() override = default;
+
+  // OutputSurfaceScaler mock.
+  MOCK_METHOD(bool, Initialize, (Plane));
+  MOCK_METHOD(gfx::VideoOutputMode,
+              PrepareToRender,
+              (base::UnguessableToken, bool));
+  MOCK_METHOD(void, SetBelowParent, ());
+
+  // OutputSurface mock.
+  MOCK_METHOD(void,
+              Render,
+              (const gfx::OverlayPlaneData&,
+               gfx::Size,
+               gfx::OverlayRenderData));
+};
+
+void WaitOnTaskRunner(base::TimeDelta duration) {
+  base::RunLoop run_loop;
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
+      FROM_HERE,
+      base::BindOnce(&base::RunLoop::Quit, base::Unretained(&run_loop)),
+      duration);
+  run_loop.Run();
+}
+
+class OutputSurfaceProxyTest : public ::testing::Test {
+ protected:
+  bool Initialize() {
+    return output_surface_proxy->Initialize(OutputSurface::Plane::kMain,
+                                            mock_confict_cb.Get());
+  }
+
+  void WaitForInitialization() {
+    base::WaitableEvent event;
+    output_surface_proxy->WorkerTaskRunnerForTesting()->PostTask(
+        FROM_HERE,
+        base::BindOnce(&base::WaitableEvent::Signal, base::Unretained(&event)));
+    event.Wait();
+  }
+
+  bool InitializeAndWait() {
+    if (!Initialize()) {
+      return false;
+    }
+
+    WaitForInitialization();
+    return true;
+  }
+
+  base::test::TaskEnvironment task_environment{
+      base::test::TaskEnvironment::MainThreadType::IO};
+
+  base::SingleAllocationResourceManager resource_manager;
+
+  RenderingWorkarounds rendering_workarounds;
+
+  scoped_refptr<OutputSurfaceProxy> output_surface_proxy;
+
+  base::MockCallback<base::OnceCallback<void(OutputSurface::Plane)>>
+      mock_confict_cb;
+};
+
+TEST(BareOutputSurfaceProxyTest, DoesNothingBeforeInitialization) {
+  base::MockResourceManager mock_resource_manager;
+  base::SetGlobalResourceManagerForTesting(&mock_resource_manager);
+
+  RenderingWorkarounds rendering_workarounds;
+
+  auto output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      base::BindRepeating([](int, const RenderingWorkarounds&)
+                              -> std::unique_ptr<OutputSurfaceScaler> {
+        return std::make_unique<MockOutputSurfaceScaler>();
+      }));
+}
+
+TEST_F(OutputSurfaceProxyTest, InitializationIsSuccessful) {
+  output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      base::BindRepeating([](int, const RenderingWorkarounds&)
+                              -> std::unique_ptr<OutputSurfaceScaler> {
+        auto result = std::make_unique<MockOutputSurfaceScaler>();
+        EXPECT_CALL(*result, Initialize).WillOnce(testing::Return(true));
+        return result;
+      }));
+
+  EXPECT_TRUE(Initialize());
+
+  // We need to wait for worker thread to finish its job. Without this one
+  // instance is destroyed on worker thread which tries to join it leading
+  // to crash.
+  WaitForInitialization();
+}
+
+base::RepeatingCallback<
+    std::unique_ptr<OutputSurfaceScaler>(int, const RenderingWorkarounds&)>
+OutputSurfaceProxyCreator(gfx::VideoOutputMode output_mode) {
+  return base::BindRepeating(
+      [](gfx::VideoOutputMode output_mode, int,
+         const RenderingWorkarounds&) -> std::unique_ptr<OutputSurfaceScaler> {
+        auto result = std::make_unique<MockOutputSurfaceScaler>();
+        // Initialize should always be called.
+        EXPECT_CALL(*result, Initialize).WillOnce(testing::Return(true));
+        // Since it might be done on separate sequence, we should provide
+        // return value here, but not expect it's called indeed.
+        EXPECT_CALL(*result, PrepareToRender)
+            .WillRepeatedly(testing::Return(output_mode));
+        EXPECT_CALL(*result, Render).Times(0);
+        return result;
+      },
+      output_mode);
+}
+
+TEST_F(OutputSurfaceProxyTest, ImmediatePrepareToRenderIsSuccessful) {
+  output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      OutputSurfaceProxyCreator(gfx::VideoOutputMode::kTransitionUnmuting));
+
+  ASSERT_TRUE(Initialize());
+
+  constexpr const bool kCanRenderTexture = true;
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb;
+  const auto token = base::UnguessableToken::Create();
+  EXPECT_EQ(output_surface_proxy->PrepareToRender(
+                token, kCanRenderTexture, mock_video_output_mode_cb.Get()),
+            gfx::VideoOutputMode::kTransitionUnmuting);
+
+  // We need to wait for worker thread to finish its job. Without this one
+  // instance is destroyed on worker thread which tries to join it leading
+  // to crash.
+  WaitForInitialization();
+}
+
+TEST_F(OutputSurfaceProxyTest, PrepareToRenderAfterInitIsSuccessful) {
+  output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      OutputSurfaceProxyCreator(gfx::VideoOutputMode::kTransitionUnmuting));
+
+  ASSERT_TRUE(InitializeAndWait());
+
+  constexpr const bool kCanRenderTexture = true;
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb;
+  const auto token = base::UnguessableToken::Create();
+  EXPECT_EQ(output_surface_proxy->PrepareToRender(
+                token, kCanRenderTexture, mock_video_output_mode_cb.Get()),
+            gfx::VideoOutputMode::kTransitionUnmuting);
+
+  // Ensure that |mock_video_output_mode_cb| is not called in a while.
+  WaitOnTaskRunner(base::Milliseconds(500));
+}
+
+TEST_F(OutputSurfaceProxyTest, SyncingIsPropagatedInCallback) {
+  output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      base::BindRepeating([](int, const RenderingWorkarounds&)
+                              -> std::unique_ptr<OutputSurfaceScaler> {
+        auto result = std::make_unique<MockOutputSurfaceScaler>();
+        EXPECT_CALL(*result, Initialize).WillOnce(testing::Return(true));
+        EXPECT_CALL(*result, PrepareToRender)
+            .WillOnce(
+                testing::Return(gfx::VideoOutputMode::kTransitionUnmuting))
+            .WillOnce(
+                testing::Return(gfx::VideoOutputMode::kTransitionSyncing));
+        return result;
+      }));
+
+  ASSERT_TRUE(InitializeAndWait());
+
+  // Reaching syncing should not take longer than 0.5s, since it's not doing
+  // any real initialization.
+  base::test::ScopedRunLoopTimeout default_timeout(FROM_HERE,
+                                                   base::Milliseconds(500));
+  base::RunLoop run_loop;
+
+  constexpr const bool kCanRenderTexture = true;
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb;
+  EXPECT_CALL(mock_video_output_mode_cb,
+              Run(gfx::VideoOutputMode::kTransitionSyncing))
+      .WillOnce([&]() { run_loop.Quit(); });
+  const auto token = base::UnguessableToken::Create();
+  EXPECT_EQ(output_surface_proxy->PrepareToRender(
+                token, kCanRenderTexture, mock_video_output_mode_cb.Get()),
+            gfx::VideoOutputMode::kTransitionUnmuting);
+
+  run_loop.Run();
+}
+
+enum class InitializationRunType {
+  kDuringInitialization,
+  kAfterInitialization,
+};
+
+enum class ThreadingType {
+  kSameThread,
+  kDifferentThreads,
+};
+
+class OutputSurfaceProxyTestWithParam
+    : public OutputSurfaceProxyTest,
+      public testing::WithParamInterface<
+          std::tuple<InitializationRunType, ThreadingType>> {};
+
+TEST_P(OutputSurfaceProxyTestWithParam, RenderIsNotCalledAfterSteal) {
+  output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      OutputSurfaceProxyCreator(gfx::VideoOutputMode::kTransitionUnmuting));
+
+  if (std::get<0>(GetParam()) == InitializationRunType::kDuringInitialization) {
+    ASSERT_TRUE(Initialize());
+  } else {
+    ASSERT_TRUE(InitializeAndWait());
+  }
+
+  const auto token_1 = base::UnguessableToken::Create();
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb_1;
+  EXPECT_CALL(mock_video_output_mode_cb_1, Run(testing::_))
+      .WillRepeatedly([]() {});
+
+  const auto token_2 = base::UnguessableToken::Create();
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb_2;
+  EXPECT_CALL(mock_video_output_mode_cb_2, Run(testing::_))
+      .WillRepeatedly([]() {});
+
+  constexpr const bool kCanRenderTexture = true;
+  if (std::get<1>(GetParam()) == ThreadingType::kSameThread) {
+    output_surface_proxy->PrepareToRender(token_1, kCanRenderTexture,
+                                          mock_video_output_mode_cb_1.Get());
+    output_surface_proxy->PrepareToRender(token_2, kCanRenderTexture,
+                                          mock_video_output_mode_cb_2.Get());
+    output_surface_proxy->Render(
+        gfx::OverlayPlaneData{}, gfx::Size{},
+        gfx::OverlayRenderData{.collection_token = token_1});
+
+    WaitOnTaskRunner(base::Milliseconds(500));
+  } else {
+    base::Thread thread_1("decoder1");
+    ASSERT_TRUE(thread_1.StartAndWaitForTesting());
+    ASSERT_TRUE(thread_1.task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(base::IgnoreResult(&OutputSurfaceProxy::PrepareToRender),
+                       base::Unretained(output_surface_proxy.get()), token_1,
+                       kCanRenderTexture, mock_video_output_mode_cb_1.Get())));
+    thread_1.FlushForTesting();
+
+    base::Thread thread_2("decoder2");
+    ASSERT_TRUE(thread_2.StartAndWaitForTesting());
+    ASSERT_TRUE(thread_2.task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(base::IgnoreResult(&OutputSurfaceProxy::PrepareToRender),
+                       base::Unretained(output_surface_proxy.get()), token_2,
+                       kCanRenderTexture, mock_video_output_mode_cb_2.Get())));
+    thread_2.FlushForTesting();
+
+    ASSERT_TRUE(thread_1.task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(base::IgnoreResult(&OutputSurfaceProxy::Render),
+                       base::Unretained(output_surface_proxy.get()),
+                       gfx::OverlayPlaneData{}, gfx::Size{},
+                       gfx::OverlayRenderData{.collection_token = token_1})));
+    thread_1.FlushForTesting();
+  }
+}
+
+TEST_P(OutputSurfaceProxyTestWithParam,
+       FirstCollectionGetsTextureNotification) {
+  output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      OutputSurfaceProxyCreator(gfx::VideoOutputMode::kTransitionUnmuting));
+
+  if (std::get<0>(GetParam()) == InitializationRunType::kDuringInitialization) {
+    ASSERT_TRUE(Initialize());
+  } else {
+    ASSERT_TRUE(InitializeAndWait());
+  }
+
+  base::RunLoop run_loop;
+
+  const auto token_1 = base::UnguessableToken::Create();
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb_1;
+  EXPECT_CALL(mock_video_output_mode_cb_1, Run(gfx::VideoOutputMode::kTexture))
+      .WillOnce([&]() { run_loop.Quit(); });
+
+  const auto token_2 = base::UnguessableToken::Create();
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb_2;
+  EXPECT_CALL(mock_video_output_mode_cb_2, Run(testing::_))
+      .WillRepeatedly([]() {});
+
+  constexpr const bool kCanRenderTexture = true;
+  if (std::get<1>(GetParam()) == ThreadingType::kSameThread) {
+    output_surface_proxy->PrepareToRender(token_1, kCanRenderTexture,
+                                          mock_video_output_mode_cb_1.Get());
+    output_surface_proxy->PrepareToRender(token_2, kCanRenderTexture,
+                                          mock_video_output_mode_cb_2.Get());
+
+    WaitOnTaskRunner(base::Milliseconds(500));
+
+    run_loop.Run();
+  } else {
+    base::Thread thread_1("decoder1");
+    ASSERT_TRUE(thread_1.StartAndWaitForTesting());
+    ASSERT_TRUE(thread_1.task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(base::IgnoreResult(&OutputSurfaceProxy::PrepareToRender),
+                       base::Unretained(output_surface_proxy.get()), token_1,
+                       kCanRenderTexture, mock_video_output_mode_cb_1.Get())));
+    thread_1.FlushForTesting();
+
+    base::Thread thread_2("decoder2");
+    ASSERT_TRUE(thread_2.StartAndWaitForTesting());
+    ASSERT_TRUE(thread_2.task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(base::IgnoreResult(&OutputSurfaceProxy::PrepareToRender),
+                       base::Unretained(output_surface_proxy.get()), token_2,
+                       kCanRenderTexture, mock_video_output_mode_cb_2.Get())));
+    thread_2.FlushForTesting();
+
+    run_loop.Run();
+  }
+}
+
+TEST_P(OutputSurfaceProxyTestWithParam, SecondCollectionGetsModeNotification) {
+  output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      OutputSurfaceProxyCreator(gfx::VideoOutputMode::kOverlay));
+
+  if (std::get<0>(GetParam()) == InitializationRunType::kDuringInitialization) {
+    ASSERT_TRUE(Initialize());
+  } else {
+    ASSERT_TRUE(InitializeAndWait());
+  }
+
+  base::RunLoop run_loop;
+
+  const auto token_1 = base::UnguessableToken::Create();
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb_1;
+  EXPECT_CALL(mock_video_output_mode_cb_1, Run(testing::_))
+      .WillRepeatedly([]() {});
+  // It's possible that first collection gets mode changed before
+  // second one steals surface from it.
+  EXPECT_CALL(mock_video_output_mode_cb_1, Run(gfx::VideoOutputMode::kOverlay))
+      .Times(::testing::AtMost(1))
+      .WillOnce([&]() { run_loop.Quit(); });
+
+  const auto token_2 = base::UnguessableToken::Create();
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb_2;
+  EXPECT_CALL(mock_video_output_mode_cb_2, Run(gfx::VideoOutputMode::kOverlay))
+      .Times(::testing::AtMost(1))
+      .WillOnce([&]() { run_loop.Quit(); });
+
+  constexpr const bool kCanRenderTexture = true;
+  if (std::get<1>(GetParam()) == ThreadingType::kSameThread) {
+    output_surface_proxy->PrepareToRender(token_1, kCanRenderTexture,
+                                          mock_video_output_mode_cb_1.Get());
+    output_surface_proxy->PrepareToRender(token_2, kCanRenderTexture,
+                                          mock_video_output_mode_cb_2.Get());
+
+    WaitOnTaskRunner(base::Milliseconds(500));
+
+    run_loop.Run();
+  } else {
+    base::Thread thread_1("decoder1");
+    ASSERT_TRUE(thread_1.StartAndWaitForTesting());
+    ASSERT_TRUE(thread_1.task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(base::IgnoreResult(&OutputSurfaceProxy::PrepareToRender),
+                       base::Unretained(output_surface_proxy.get()), token_1,
+                       kCanRenderTexture, mock_video_output_mode_cb_1.Get())));
+    thread_1.FlushForTesting();
+
+    base::Thread thread_2("decoder2");
+    ASSERT_TRUE(thread_2.StartAndWaitForTesting());
+    ASSERT_TRUE(thread_2.task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(base::IgnoreResult(&OutputSurfaceProxy::PrepareToRender),
+                       base::Unretained(output_surface_proxy.get()), token_2,
+                       kCanRenderTexture, mock_video_output_mode_cb_2.Get())));
+    thread_2.FlushForTesting();
+
+    run_loop.Run();
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    StealingGroup,
+    OutputSurfaceProxyTestWithParam,
+    testing::Combine(
+        testing::Values(InitializationRunType::kDuringInitialization,
+                        InitializationRunType::kAfterInitialization),
+        testing::Values(ThreadingType::kSameThread,
+                        ThreadingType::kDifferentThreads)),
+    [](const testing::TestParamInfo<OutputSurfaceProxyTestWithParam::ParamType>&
+           info) {
+      std::string name = "";
+      switch (std::get<0>(info.param)) {
+        case InitializationRunType::kDuringInitialization:
+          name += "DuringInitialization";
+          break;
+        case InitializationRunType::kAfterInitialization:
+          name += "AfterInitialization";
+          break;
+      }
+      switch (std::get<1>(info.param)) {
+        case ThreadingType::kSameThread:
+          name += "SameThread";
+          break;
+        case ThreadingType::kDifferentThreads:
+          name += "DifferentThreads";
+          break;
+      }
+      return name;
+    });
+
+class OutputSurfaceProxyTestWithInitializationParam
+    : public OutputSurfaceProxyTest,
+      public testing::WithParamInterface<InitializationRunType> {};
+
+TEST_P(OutputSurfaceProxyTestWithInitializationParam,
+       PrepareToRenderFromSameThreadIsEffective) {
+  output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      OutputSurfaceProxyCreator(gfx::VideoOutputMode::kOverlay));
+
+  if (GetParam() == InitializationRunType::kDuringInitialization) {
+    ASSERT_TRUE(Initialize());
+  } else {
+    ASSERT_TRUE(InitializeAndWait());
+  }
+
+  // Reaching syncing should not take longer than 0.5s, since it's not doing
+  // any real initialization.
+  base::test::ScopedRunLoopTimeout default_timeout(FROM_HERE,
+                                                   base::Milliseconds(500));
+  base::RunLoop run_loop;
+
+  constexpr const bool kCanRenderTexture = true;
+  const auto token = base::UnguessableToken::Create();
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb;
+  EXPECT_CALL(mock_video_output_mode_cb, Run(gfx::VideoOutputMode::kOverlay))
+      .WillOnce([&]() { run_loop.Quit(); });
+
+  output_surface_proxy->PrepareToRender(token, kCanRenderTexture,
+                                        mock_video_output_mode_cb.Get());
+
+  run_loop.Run();
+}
+
+TEST_P(OutputSurfaceProxyTestWithInitializationParam,
+       PrepareToRenderFromDifferentThreadIsEffective) {
+  output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      OutputSurfaceProxyCreator(gfx::VideoOutputMode::kOverlay));
+
+  // Start thread before initialization to ensure later it starts its job as
+  // fast as possible.
+  base::Thread thread("decoder1");
+  ASSERT_TRUE(thread.StartAndWaitForTesting());
+
+  if (GetParam() == InitializationRunType::kDuringInitialization) {
+    ASSERT_TRUE(Initialize());
+  } else {
+    ASSERT_TRUE(InitializeAndWait());
+  }
+
+  // Reaching syncing should not take longer than 0.5s, since it's not doing
+  // any real initialization.
+  base::test::ScopedRunLoopTimeout default_timeout(FROM_HERE,
+                                                   base::Milliseconds(500));
+  base::RunLoop run_loop;
+
+  constexpr const bool kCanRenderTexture = true;
+  const auto token = base::UnguessableToken::Create();
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb;
+  EXPECT_CALL(mock_video_output_mode_cb, Run(gfx::VideoOutputMode::kOverlay))
+      .WillOnce([&]() { run_loop.Quit(); });
+
+  ASSERT_TRUE(thread.task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(base::IgnoreResult(&OutputSurfaceProxy::PrepareToRender),
+                     base::Unretained(output_surface_proxy.get()), token,
+                     kCanRenderTexture, mock_video_output_mode_cb.Get())));
+
+  run_loop.Run();
+}
+
+TEST_P(OutputSurfaceProxyTestWithInitializationParam,
+       AfterSwitchingThreadsNotificationIsDone) {
+  output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      OutputSurfaceProxyCreator(gfx::VideoOutputMode::kOverlay));
+
+  // Start thread before initialization to ensure later it starts its job as
+  // fast as possible.
+  base::Thread thread("decoder1");
+  ASSERT_TRUE(thread.StartAndWaitForTesting());
+
+  if (GetParam() == InitializationRunType::kDuringInitialization) {
+    ASSERT_TRUE(Initialize());
+  } else {
+    ASSERT_TRUE(InitializeAndWait());
+  }
+
+  // Reaching syncing should not take longer than 0.5s, since it's not doing
+  // any real initialization.
+  base::test::ScopedRunLoopTimeout default_timeout(FROM_HERE,
+                                                   base::Milliseconds(500));
+  base::RunLoop run_loop;
+
+  constexpr const bool kCanRenderTexture = true;
+  const auto token = base::UnguessableToken::Create();
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb;
+  EXPECT_CALL(mock_video_output_mode_cb, Run(gfx::VideoOutputMode::kOverlay))
+      .WillOnce([&]() { run_loop.Quit(); });
+
+  output_surface_proxy->PrepareToRender(token, kCanRenderTexture,
+                                        mock_video_output_mode_cb.Get());
+
+  ASSERT_TRUE(thread.task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(base::IgnoreResult(&OutputSurfaceProxy::PrepareToRender),
+                     base::Unretained(output_surface_proxy.get()), token,
+                     kCanRenderTexture, mock_video_output_mode_cb.Get())));
+
+  run_loop.Run();
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    SameCollection,
+    OutputSurfaceProxyTestWithInitializationParam,
+    testing::Values(InitializationRunType::kDuringInitialization,
+                    InitializationRunType::kAfterInitialization),
+    [](const testing::TestParamInfo<
+        OutputSurfaceProxyTestWithInitializationParam::ParamType>& info) {
+      std::string name = "";
+      switch (info.param) {
+        case InitializationRunType::kDuringInitialization:
+          name += "DuringInitialization";
+          break;
+        case InitializationRunType::kAfterInitialization:
+          name += "AfterInitialization";
+          break;
+      }
+      return name;
+    });
+
+TEST_F(OutputSurfaceProxyTest,
+       DuringInitializationAfterSwitchingThreadsTextureNotificationIsDone) {
+  // Processing on should not take longer than 0.5s, since it's not doing
+  // any real initialization.
+  base::test::ScopedRunLoopTimeout default_timeout(FROM_HERE,
+                                                   base::Milliseconds(500));
+  base::RunLoop run_loop;
+
+  base::WaitableEvent initialization_finish;
+  output_surface_proxy = base::MakeRefCounted<OutputSurfaceProxy>(
+      rendering_workarounds,
+      base::BindRepeating(
+          [](base::WaitableEvent* event, int, const RenderingWorkarounds&)
+              -> std::unique_ptr<OutputSurfaceScaler> {
+            auto result = std::make_unique<MockOutputSurfaceScaler>();
+            EXPECT_CALL(*result, Initialize).WillOnce([event]() {
+              event->Wait();
+              return true;
+            });
+            EXPECT_CALL(*result, PrepareToRender)
+                .WillRepeatedly(
+                    testing::Return(gfx::VideoOutputMode::kTransitionSyncing));
+            EXPECT_CALL(*result, Render).Times(0);
+            return result;
+          },
+          base::Unretained(&initialization_finish)));
+
+  // Start thread before initialization to ensure later it starts its job as
+  // fast as possible.
+  base::Thread thread("side_thread");
+  ASSERT_TRUE(thread.StartAndWaitForTesting());
+
+  ASSERT_TRUE(Initialize());
+
+  constexpr const bool kCanRenderTexture = true;
+  const auto token = base::UnguessableToken::Create();
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb;
+  EXPECT_CALL(mock_video_output_mode_cb, Run(testing::_)).Times(0);
+
+  output_surface_proxy->PrepareToRender(token, kCanRenderTexture,
+                                        mock_video_output_mode_cb.Get());
+
+  base::MockCallback<base::RepeatingCallback<void(gfx::VideoOutputMode)>>
+      mock_video_output_mode_cb_side_thread;
+  EXPECT_CALL(mock_video_output_mode_cb_side_thread,
+              Run(gfx::VideoOutputMode::kTransitionSyncing))
+      .WillOnce([&]() { run_loop.Quit(); });
+  ASSERT_TRUE(thread.task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(base::IgnoreResult(&OutputSurfaceProxy::PrepareToRender),
+                     base::Unretained(output_surface_proxy.get()), token,
+                     kCanRenderTexture,
+                     mock_video_output_mode_cb_side_thread.Get())));
+
+  // Ensure that all |PrepareToRender| already started, unfortunately there
+  // is no other way (for now) to do this.
+  WaitOnTaskRunner(base::Milliseconds(200));
+  initialization_finish.Signal();
+
+  run_loop.Run();
+}
+
+}  // namespace ui