// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/video/renderable_mappable_shared_image_video_frame_pool.h"

#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/notimplemented.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "components/viz/common/resources/shared_image_format.h"
#include "components/viz/common/resources/shared_image_format_utils.h"
#include "components/viz/test/test_context_provider.h"
#include "gpu/command_buffer/client/client_shared_image.h"
#include "gpu/command_buffer/common/shared_image_usage.h"
#include "gpu/config/gpu_finch_features.h"
#include "media/base/format_utils.h"
#include "media/base/media_switches.h"
#include "media/base/video_frame.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;

namespace media {

namespace {

gfx::ColorSpace GetColorSpaceForPixelFormat(media::VideoPixelFormat format) {
  switch (format) {
    case media::PIXEL_FORMAT_NV12:
      return gfx::ColorSpace::CreateREC709();
    case media::PIXEL_FORMAT_ARGB:
    case media::PIXEL_FORMAT_ABGR:
      return gfx::ColorSpace::CreateSRGB();
    default:
      NOTREACHED();
  }
}

class FakeContext
    : public RenderableMappableSharedImageVideoFramePool::Context {
 public:
  FakeContext()
      : context_provider_(viz::TestContextProvider::CreateGLES()),
        weak_factory_(this) {}
  ~FakeContext() override = default;

  scoped_refptr<gpu::ClientSharedImage> CreateSharedImage(
      const gfx::Size& size,
      gfx::BufferUsage buffer_usage,
      const viz::SharedImageFormat& si_format,
      const gfx::ColorSpace& color_space,
      gpu::SharedImageUsageSet usage,
      gpu::SyncToken& sync_token) override {
    DoCreateMappableSharedImage(size, buffer_usage, si_format, color_space,
                                usage, sync_token);
    return context_provider_->SharedImageInterface()->CreateSharedImage(
        {si_format, size, color_space, usage,
         "RenderableMappableSharedImageVideoFramePoolTest"},
        gpu::kNullSurfaceHandle, buffer_usage);
  }

  const gpu::SharedImageCapabilities& GetCapabilities() override {
    return context_provider_->SharedImageInterface()->GetCapabilities();
  }

  MOCK_METHOD6(DoCreateMappableSharedImage,
               void(const gfx::Size& size,
                    gfx::BufferUsage buffer_usage,
                    const viz::SharedImageFormat& si_format,
                    const gfx::ColorSpace& color_space,
                    gpu::SharedImageUsageSet usage,
                    gpu::SyncToken& sync_token));

  MOCK_METHOD2(DestroySharedImage,
               void(const gpu::SyncToken& sync_token,
                    scoped_refptr<gpu::ClientSharedImage> shared_image));

  base::WeakPtr<FakeContext> GetWeakPtr() { return weak_factory_.GetWeakPtr(); }

 private:
  scoped_refptr<viz::TestContextProvider> context_provider_;
  base::WeakPtrFactory<FakeContext> weak_factory_;
};

class RenderableMappableSharedImageVideoFramePoolTest
    : public testing::TestWithParam<VideoPixelFormat> {
 public:
  RenderableMappableSharedImageVideoFramePoolTest() : format_(GetParam()) {}

 protected:
  void VerifySharedImageCreation(FakeContext* context) {
    viz::SharedImageFormat si_format;
    switch (format_) {
      case PIXEL_FORMAT_NV12:
        si_format = viz::MultiPlaneFormat::kNV12;
        break;
      case PIXEL_FORMAT_ARGB:
        si_format = viz::SinglePlaneFormat::kBGRA_8888;
        break;
      case PIXEL_FORMAT_ABGR:
        si_format = viz::SinglePlaneFormat::kRGBA_8888;
        break;
      default:
        NOTREACHED();
    }
    EXPECT_CALL(*context,
                DoCreateMappableSharedImage(_, _, si_format, _, _, _));
  }

  VideoPixelFormat format_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

TEST_P(RenderableMappableSharedImageVideoFramePoolTest, SimpleLifetimes) {
  base::test::SingleThreadTaskEnvironment task_environment;
  const gfx::Size size0(128, 256);
  const gfx::ColorSpace color_space0 = GetColorSpaceForPixelFormat(format_);

  base::WeakPtr<FakeContext> context;
  std::unique_ptr<RenderableMappableSharedImageVideoFramePool> pool;
  {
    auto context_strong = std::make_unique<FakeContext>();
    context = context_strong->GetWeakPtr();
    pool = RenderableMappableSharedImageVideoFramePool::Create(
        std::move(context_strong), format_);
  }

  // Create a new frame.
  VerifySharedImageCreation(context.get());
  auto video_frame0 = pool->MaybeCreateVideoFrame(size0, color_space0);
  video_frame0 = nullptr;
  task_environment.RunUntilIdle();

  // Expect the frame to be reused.
  EXPECT_CALL(*context, DoCreateMappableSharedImage(_, _, _, _, _, _)).Times(0);

  auto video_frame1 = pool->MaybeCreateVideoFrame(size0, color_space0);

  // Expect a new frame to be created.
  VerifySharedImageCreation(context.get());
  auto video_frame2 = pool->MaybeCreateVideoFrame(size0, color_space0);

  // Expect a new frame to be created.
  VerifySharedImageCreation(context.get());
  auto video_frame3 = pool->MaybeCreateVideoFrame(size0, color_space0);

  // Freeing two frames will not result in any frames being destroyed, because
  // we allow unused 2 frames to exist.
  video_frame1 = nullptr;
  video_frame2 = nullptr;
  task_environment.RunUntilIdle();

  // Freeing the third frame will result in one of the frames being destroyed.
  EXPECT_CALL(*context, DestroySharedImage(_, _));
  video_frame3 = nullptr;
  task_environment.RunUntilIdle();

  // Destroying the pool will result in the remaining two frames being
  // destroyed.
  EXPECT_TRUE(!!context);
  EXPECT_CALL(*context, DestroySharedImage(_, _)).Times(2);
  pool.reset();
  task_environment.RunUntilIdle();
  EXPECT_FALSE(!!context);
}

TEST_P(RenderableMappableSharedImageVideoFramePoolTest, FrameFreedAfterPool) {
  base::test::SingleThreadTaskEnvironment task_environment;
  const gfx::Size size0(128, 256);
  const gfx::ColorSpace color_space0 = GetColorSpaceForPixelFormat(format_);

  base::WeakPtr<FakeContext> context;
  std::unique_ptr<RenderableMappableSharedImageVideoFramePool> pool;
  {
    auto context_strong = std::make_unique<FakeContext>();
    context = context_strong->GetWeakPtr();
    pool = RenderableMappableSharedImageVideoFramePool::Create(
        std::move(context_strong), format_);
  }
  // Create a new frame.
  VerifySharedImageCreation(context.get());
  auto video_frame0 = pool->MaybeCreateVideoFrame(size0, color_space0);
  task_environment.RunUntilIdle();

  // If the pool is destroyed, but a frame still exists, the context will not
  // be destroyed.
  pool.reset();
  task_environment.RunUntilIdle();
  EXPECT_TRUE(context);

  // Destroy the frame. Still nothing will happen, because its destruction will
  // happen after a posted task is run.
  video_frame0 = nullptr;

  // The shared images will be destroyed once the posted task is run.
  EXPECT_CALL(*context, DestroySharedImage(_, _));
  task_environment.RunUntilIdle();
  EXPECT_FALSE(!!context);
}

TEST_P(RenderableMappableSharedImageVideoFramePoolTest, CrossThread) {
  base::test::TaskEnvironment task_environment{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  const gfx::Size size0(128, 256);
  const gfx::ColorSpace color_space0 = GetColorSpaceForPixelFormat(format_);

  // Create a pool on the main thread.
  auto pool = RenderableMappableSharedImageVideoFramePool::Create(
      std::make_unique<FakeContext>(), format_);

  base::ThreadPool::CreateSequencedTaskRunner({})->PostTaskAndReplyWithResult(
      FROM_HERE,
      // Create a frame on another thread.
      base::BindLambdaForTesting(
          [&]() { return pool->MaybeCreateVideoFrame(size0, color_space0); }),
      // Destroy the video frame on the main thread.
      base::BindLambdaForTesting(
          [&](scoped_refptr<VideoFrame> video_frame0) {}));
  task_environment.RunUntilIdle();

  // Destroy the pool.
  pool = nullptr;
  task_environment.RunUntilIdle();
}

TEST_P(RenderableMappableSharedImageVideoFramePoolTest,
       VideoFramesDestroyedConcurrently) {
  base::test::TaskEnvironment task_environment{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  const gfx::Size size0(128, 256);
  const gfx::ColorSpace color_space0 = GetColorSpaceForPixelFormat(format_);

  // Create a pool and several frames on the main thread.
  base::WeakPtr<FakeContext> context;
  std::unique_ptr<RenderableMappableSharedImageVideoFramePool> pool;
  {
    auto context_strong = std::make_unique<FakeContext>();
    context = context_strong->GetWeakPtr();
    pool = RenderableMappableSharedImageVideoFramePool::Create(
        std::move(context_strong), format_);
  }

  std::vector<scoped_refptr<VideoFrame>> frames;
  static constexpr int kNumFrames = 3;
  for (int i = 0; i < kNumFrames; i++) {
    VerifySharedImageCreation(context.get());
    frames.emplace_back(pool->MaybeCreateVideoFrame(size0, color_space0));
  }
  task_environment.RunUntilIdle();

  // Expect all frames to be destroyed eventually.
  EXPECT_CALL(*context, DestroySharedImage(_, _)).Times(kNumFrames);

  // Destroy frames on separate threads. TSAN will tell us if there's a problem.
  for (int i = 0; i < kNumFrames; i++) {
    base::ThreadPool::CreateSequencedTaskRunner({})->PostTask(
        FROM_HERE, base::DoNothingWithBoundArgs(std::move(frames[i])));
  }

  pool.reset();
  task_environment.RunUntilIdle();
  EXPECT_FALSE(!!context);
}

TEST_P(RenderableMappableSharedImageVideoFramePoolTest,
       ConcurrentCreateDestroy) {
  base::test::TaskEnvironment task_environment{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  const gfx::Size size0(128, 256);
  const gfx::ColorSpace color_space0 = GetColorSpaceForPixelFormat(format_);

  // Create a pool on the main thread.
  auto pool = RenderableMappableSharedImageVideoFramePool::Create(
      std::make_unique<FakeContext>(), format_);

  // Create a frame on the main thread.
  auto video_frame0 = pool->MaybeCreateVideoFrame(size0, color_space0);
  task_environment.RunUntilIdle();

  // Destroy the frame on another thread. TSAN will tell us if there's a
  // problem.
  base::ThreadPool::CreateSequencedTaskRunner({})->PostTask(
      FROM_HERE, base::DoNothingWithBoundArgs(std::move(video_frame0)));

  // Create another frame on the main thread.
  auto video_frame1 = pool->MaybeCreateVideoFrame(size0, color_space0);
  task_environment.RunUntilIdle();

  video_frame1 = nullptr;
  pool.reset();
  task_environment.RunUntilIdle();
}

TEST_P(RenderableMappableSharedImageVideoFramePoolTest,
       RespectSizeAndColorSpace) {
  base::test::SingleThreadTaskEnvironment task_environment;
  const gfx::Size size0(128, 256);
  const gfx::ColorSpace color_space0 = GetColorSpaceForPixelFormat(format_);
  const gfx::Size size1(256, 256);
  const gfx::ColorSpace color_space1 = gfx::ColorSpace::CreateREC601();

  base::WeakPtr<FakeContext> context;
  std::unique_ptr<RenderableMappableSharedImageVideoFramePool> pool;
  {
    auto context_strong = std::make_unique<FakeContext>();
    context = context_strong->GetWeakPtr();
    pool = RenderableMappableSharedImageVideoFramePool::Create(
        std::move(context_strong), format_);
  }

  // Create a new frame.
  VerifySharedImageCreation(context.get());
  auto video_frame0 = pool->MaybeCreateVideoFrame(size0, color_space0);
  video_frame0 = nullptr;
  task_environment.RunUntilIdle();

  // Expect the frame to be reused.
  EXPECT_CALL(*context, DoCreateMappableSharedImage(_, _, _, _, _, _)).Times(0);

  video_frame0 = pool->MaybeCreateVideoFrame(size0, color_space0);
  video_frame0 = nullptr;
  task_environment.RunUntilIdle();

  // Change the size, expect a new frame to be created (and the previous frame
  // to be destroyed).
  EXPECT_CALL(*context, DestroySharedImage(_, _));
  VerifySharedImageCreation(context.get());
  video_frame0 = pool->MaybeCreateVideoFrame(size1, color_space0);
  video_frame0 = nullptr;
  task_environment.RunUntilIdle();

  // Expect that frame to be reused.
  EXPECT_CALL(*context, DoCreateMappableSharedImage(_, _, _, _, _, _)).Times(0);

  video_frame0 = pool->MaybeCreateVideoFrame(size1, color_space0);
  video_frame0 = nullptr;
  task_environment.RunUntilIdle();

  // Change the color space, expect a new frame to be created (and the previous
  // frame to be destroyed).
  EXPECT_CALL(*context, DestroySharedImage(_, _));
  VerifySharedImageCreation(context.get());
  video_frame0 = pool->MaybeCreateVideoFrame(size1, color_space1);
  video_frame0 = nullptr;
  task_environment.RunUntilIdle();

  // Expect that frame to be reused.
  EXPECT_CALL(*context, DoCreateMappableSharedImage(_, _, _, _, _, _)).Times(0);

  video_frame0 = pool->MaybeCreateVideoFrame(size1, color_space1);
  video_frame0 = nullptr;
  task_environment.RunUntilIdle();

  EXPECT_CALL(*context, DestroySharedImage(_, _));
  pool.reset();
  task_environment.RunUntilIdle();
  EXPECT_FALSE(!!context);
}

// Verifies that the requires_cpu_access flag controls gfx::BufferUsage in
// RenderableMappableSharedImageVideoFramePool allocations. On Linux,
// setting the flag to false avoids GBM linear allocations by switching
// from SCANOUT_CPU_READ_WRITE to SCANOUT. Ensures that the expected
// buffer usage value propagates to the shared image creation code path.
TEST_P(RenderableMappableSharedImageVideoFramePoolTest,
       RequiresCpuAccessAffectsBufferUsage) {
  base::test::SingleThreadTaskEnvironment task_environment;
  const gfx::Size size(128, 256);
  const gfx::ColorSpace color_space = GetColorSpaceForPixelFormat(format_);

  base::WeakPtr<FakeContext> context;
  std::unique_ptr<RenderableMappableSharedImageVideoFramePool> pool;

  // Case 1: requires_cpu_access = true
  {
    auto context_strong = std::make_unique<FakeContext>();
    context = context_strong->GetWeakPtr();
    pool = RenderableMappableSharedImageVideoFramePool::Create(
        std::move(context_strong), format_, /*requires_cpu_access=*/true);
  }

  ASSERT_TRUE(pool);

#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS)
  gfx::BufferUsage expected_usage = gfx::BufferUsage::SCANOUT_VEA_CPU_READ;
#elif BUILDFLAG(IS_LINUX)
  gfx::BufferUsage expected_usage = gfx::BufferUsage::SCANOUT_CPU_READ_WRITE;
#else
  gfx::BufferUsage expected_usage = gfx::BufferUsage::SCANOUT_CPU_READ_WRITE;
#endif

  EXPECT_CALL(*context,
              DoCreateMappableSharedImage(_, expected_usage, _, _, _, _))
      .Times(1);

  auto frame = pool->MaybeCreateVideoFrame(size, color_space);

  // Expect one frame to be destroyed.
  int destroy_count = 0;
  EXPECT_CALL(*context, DestroySharedImage(_, _)).WillOnce([&]() {
    ++destroy_count;
  });

  frame = nullptr;
  pool.reset();

  EXPECT_TRUE(base::test::RunUntil([&]() { return destroy_count == 1; }));
  EXPECT_EQ(destroy_count, 1);

  // Case 2: requires_cpu_access = false
  {
    auto context_strong = std::make_unique<FakeContext>();
    context = context_strong->GetWeakPtr();
    pool = RenderableMappableSharedImageVideoFramePool::Create(
        std::move(context_strong), format_, /*requires_cpu_access=*/false);
  }

  ASSERT_TRUE(pool);

#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS)
  gfx::BufferUsage expected_usage2 = gfx::BufferUsage::SCANOUT_VEA_CPU_READ;
#elif BUILDFLAG(IS_LINUX)
  gfx::BufferUsage expected_usage2 = gfx::BufferUsage::SCANOUT;
#else
  gfx::BufferUsage expected_usage2 = gfx::BufferUsage::SCANOUT_CPU_READ_WRITE;
#endif

  EXPECT_CALL(*context,
              DoCreateMappableSharedImage(_, expected_usage2, _, _, _, _))
      .Times(1);

  frame = pool->MaybeCreateVideoFrame(size, color_space);

  // Expect the frame to be destroyed.
  destroy_count = 0;
  EXPECT_CALL(*context, DestroySharedImage(_, _)).WillRepeatedly([&]() {
    ++destroy_count;
  });

  frame = nullptr;
  pool.reset();
  EXPECT_TRUE(base::test::RunUntil([&]() { return destroy_count == 1; }));
  EXPECT_EQ(destroy_count, 1);
}

INSTANTIATE_TEST_SUITE_P(
    All,
    RenderableMappableSharedImageVideoFramePoolTest,
    testing::Values(media::VideoPixelFormat::PIXEL_FORMAT_NV12,
                    media::VideoPixelFormat::PIXEL_FORMAT_ARGB,
                    media::VideoPixelFormat::PIXEL_FORMAT_ABGR));

}  // namespace

}  // namespace media
