// Copyright 2019 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/gpu/chromeos/platform_video_frame_pool.h"

#include <stddef.h>
#include <stdint.h>

#include <memory>
#include <vector>

#include "base/functional/callback_helpers.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/task_environment.h"
#include "media/base/format_utils.h"
#include "media/base/video_types.h"
#include "media/base/video_util.h"
#include "media/gpu/chromeos/fourcc.h"
#include "media/gpu/chromeos/mock_native_pixmap_dmabuf.h"
#include "media/gpu/chromeos/native_pixmap_frame_resource.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace media {

namespace {

template <uint64_t modifier>
CroStatus::Or<scoped_refptr<FrameResource>> CreateGpuMemoryBufferFrameResource(
    VideoPixelFormat format,
    const gfx::Size& coded_size,
    const gfx::Rect& visible_rect,
    const gfx::Size& natural_size,
    bool use_protected,
    bool use_linear_buffers,
    bool needs_detiling,
    base::TimeDelta timestamp) {
  constexpr gfx::BufferUsage kBufferUsage = gfx::BufferUsage::GPU_READ;

  DCHECK(VideoPixelFormatToSharedImageFormat(format));
  scoped_refptr<const gfx::NativePixmapDmaBuf> pixmap =
      CreateMockNativePixmapDmaBuf(format, coded_size, modifier);

  return static_cast<scoped_refptr<FrameResource>>(
      NativePixmapFrameResource::Create(visible_rect, natural_size, timestamp,
                                        kBufferUsage, std::move(pixmap)));
}

}  // namespace

class PlatformVideoFramePoolTestBase : public ::testing::Test {
 public:
  PlatformVideoFramePoolTestBase()
      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
        pool_(new PlatformVideoFramePool()) {
    pool_->SetCustomFrameAllocator(
        base::BindRepeating(&CreateGpuMemoryBufferFrameResource<
                            gfx::NativePixmapHandle::kNoModifier>),
        VideoFrame::STORAGE_DMABUFS);
    pool_->set_parent_task_runner(
        base::SingleThreadTaskRunner::GetCurrentDefault());
  }

  bool Initialize(const Fourcc& fourcc) {
    constexpr gfx::Size kCodedSize(320, 240);
    return Initialize(kCodedSize, /*visible_rect=*/gfx::Rect(kCodedSize),
                      fourcc);
  }

  bool Initialize(const gfx::Size& coded_size,
                  const gfx::Rect& visible_rect,
                  const Fourcc& fourcc) {
    constexpr size_t kNumFrames = 10;
    visible_rect_ = visible_rect;
    natural_size_ = visible_rect.size();
    auto status_or_layout = pool_->Initialize(fourcc, coded_size, visible_rect_,
                                              natural_size_, kNumFrames,
                                              /*use_protected=*/false,
                                              /*use_linear_buffers=*/false);
    if (!status_or_layout.has_value()) {
      return false;
    }
    layout_ = std::move(status_or_layout).value();
    return true;
  }

  scoped_refptr<FrameResource> GetFrame(int timestamp_ms) {
    scoped_refptr<FrameResource> frame = pool_->GetFrame();
    frame->set_timestamp(base::Milliseconds(timestamp_ms));

    EXPECT_EQ(layout_->modifier(), frame->layout().modifier());
    EXPECT_EQ(layout_->fourcc(),
              *Fourcc::FromVideoPixelFormat(frame->format()));
    EXPECT_EQ(layout_->size(), frame->coded_size());
    EXPECT_EQ(visible_rect_, frame->visible_rect());
    EXPECT_EQ(natural_size_, frame->natural_size());
    // We can't assert any of the |frame| metadata because the frame creation
    // callback is a fake.

    return frame;
  }

 protected:
  base::test::TaskEnvironment task_environment_;
  std::unique_ptr<PlatformVideoFramePool> pool_;

  std::optional<GpuBufferLayout> layout_;
  gfx::Rect visible_rect_;
  gfx::Size natural_size_;
};

class PlatformVideoFramePoolTest
    : public PlatformVideoFramePoolTestBase,
      public testing::WithParamInterface<VideoPixelFormat> {};

constexpr VideoPixelFormat kPixelFormats[] = {
    PIXEL_FORMAT_YV12, PIXEL_FORMAT_NV12, PIXEL_FORMAT_P010LE};
INSTANTIATE_TEST_SUITE_P(
    All,
    PlatformVideoFramePoolTest,
    testing::ValuesIn(kPixelFormats),
    [](const ::testing::TestParamInfo<VideoPixelFormat>& info) {
      return VideoPixelFormatToString(info.param);
    });

TEST_P(PlatformVideoFramePoolTest, SingleFrameReuse) {
  const auto fourcc = Fourcc::FromVideoPixelFormat(GetParam());
  ASSERT_TRUE(fourcc.has_value());
  ASSERT_TRUE(Initialize(fourcc.value()));
  scoped_refptr<FrameResource> frame = GetFrame(10);
  base::UnguessableToken id = frame->tracking_token();

  // Clear frame reference to return the frame to the pool.
  frame = nullptr;
  task_environment_.RunUntilIdle();

  // Verify that the next frame from the pool uses the same memory.
  scoped_refptr<FrameResource> new_frame = GetFrame(20);
  EXPECT_EQ(id, new_frame->tracking_token());
}

TEST_P(PlatformVideoFramePoolTest, MultipleFrameReuse) {
  const auto fourcc = Fourcc::FromVideoPixelFormat(GetParam());
  ASSERT_TRUE(fourcc.has_value());
  ASSERT_TRUE(Initialize(fourcc.value()));
  scoped_refptr<FrameResource> frame1 = GetFrame(10);
  scoped_refptr<FrameResource> frame2 = GetFrame(20);
  base::UnguessableToken id1 = frame1->tracking_token();
  base::UnguessableToken id2 = frame2->tracking_token();

  frame1 = nullptr;
  task_environment_.RunUntilIdle();
  frame1 = GetFrame(30);
  EXPECT_EQ(id1, frame1->tracking_token());

  frame2 = nullptr;
  task_environment_.RunUntilIdle();
  frame2 = GetFrame(40);
  EXPECT_EQ(id2, frame2->tracking_token());

  frame1 = nullptr;
  frame2 = nullptr;
  task_environment_.RunUntilIdle();
  EXPECT_EQ(2u, pool_->GetPoolSizeForTesting());
}

TEST_P(PlatformVideoFramePoolTest, InitializeWithDifferentFourcc) {
  const auto fourcc = Fourcc::FromVideoPixelFormat(GetParam());
  ASSERT_TRUE(fourcc.has_value());
  ASSERT_TRUE(Initialize(fourcc.value()));
  scoped_refptr<FrameResource> frame_a = GetFrame(10);
  scoped_refptr<FrameResource> frame_b = GetFrame(10);

  // Clear frame references to return the frames to the pool.
  frame_a = nullptr;
  frame_b = nullptr;
  task_environment_.RunUntilIdle();

  // Verify that both frames are in the pool.
  EXPECT_EQ(2u, pool_->GetPoolSizeForTesting());

  // Verify that requesting a frame with a different format causes the pool
  // to get drained.
  const Fourcc different_fourcc(*fourcc != Fourcc(Fourcc::NV12) ? Fourcc::NV12
                                                                : Fourcc::P010);
  ASSERT_TRUE(Initialize(different_fourcc));
  scoped_refptr<FrameResource> new_frame = GetFrame(10);
  EXPECT_EQ(0u, pool_->GetPoolSizeForTesting());
}

TEST_P(PlatformVideoFramePoolTest, InitializeWithDifferentUsableArea) {
  const auto fourcc = Fourcc::FromVideoPixelFormat(GetParam());
  ASSERT_TRUE(fourcc.has_value());

  constexpr gfx::Size kCodedSize(640, 368);
  constexpr gfx::Rect kInitialVisibleRect(10, 20, 300, 200);
  ASSERT_TRUE(Initialize(kCodedSize, kInitialVisibleRect, fourcc.value()));
  scoped_refptr<FrameResource> frame_a = GetFrame(10);
  scoped_refptr<FrameResource> frame_b = GetFrame(10);

  // Clear frame references to return the frames to the pool.
  frame_a = nullptr;
  frame_b = nullptr;
  task_environment_.RunUntilIdle();

  // Verify that both frames are in the pool.
  EXPECT_EQ(2u, pool_->GetPoolSizeForTesting());

  // Verify that requesting a frame with a different "usable area" causes the
  // pool to get drained. The usable area is the area of the buffer starting at
  // (0, 0) and extending to the bottom-right corner of the visible rectangle.
  // It corresponds to the area used to import the video frame into a graphics
  // API or to create the DRM framebuffer for hardware overlay purposes.
  constexpr gfx::Rect kDifferentVisibleRect(5, 15, 300, 200);
  ASSERT_EQ(kDifferentVisibleRect.size(), natural_size_);
  ASSERT_NE(GetRectSizeFromOrigin(kInitialVisibleRect),
            GetRectSizeFromOrigin(kDifferentVisibleRect));
  ASSERT_TRUE(Initialize(kCodedSize, kDifferentVisibleRect, fourcc.value()));
  scoped_refptr<FrameResource> new_frame = GetFrame(10);
  EXPECT_EQ(0u, pool_->GetPoolSizeForTesting());
}

TEST_P(PlatformVideoFramePoolTest, InitializeWithDifferentCodedSize) {
  const auto fourcc = Fourcc::FromVideoPixelFormat(GetParam());
  ASSERT_TRUE(fourcc.has_value());

  constexpr gfx::Size kInitialCodedSize(640, 368);
  constexpr gfx::Rect kVisibleRect(10, 20, 300, 200);
  ASSERT_TRUE(Initialize(kInitialCodedSize, kVisibleRect, fourcc.value()));
  scoped_refptr<FrameResource> frame_a = GetFrame(10);
  scoped_refptr<FrameResource> frame_b = GetFrame(10);

  // Clear frame references to return the frames to the pool.
  frame_a = nullptr;
  frame_b = nullptr;
  task_environment_.RunUntilIdle();

  // Verify that both frames are in the pool.
  EXPECT_EQ(2u, pool_->GetPoolSizeForTesting());

  // Verify that requesting a frame with a different coded size causes the pool
  // to get drained.
  constexpr gfx::Size kDifferentCodedSize(624, 368);
  ASSERT_TRUE(Initialize(kDifferentCodedSize, kVisibleRect, fourcc.value()));
  scoped_refptr<FrameResource> new_frame = GetFrame(10);
  EXPECT_EQ(0u, pool_->GetPoolSizeForTesting());
}

TEST_P(PlatformVideoFramePoolTest, GetOriginalFrame) {
  const auto fourcc = Fourcc::FromVideoPixelFormat(GetParam());
  ASSERT_TRUE(fourcc.has_value());
  ASSERT_TRUE(Initialize(fourcc.value()));
  scoped_refptr<FrameResource> frame_1 = GetFrame(10);
  scoped_refptr<FrameResource> frame_2 = frame_1->CreateWrappingFrame();
  EXPECT_EQ(pool_->GetOriginalFrame(frame_1->tracking_token()),
            pool_->GetOriginalFrame(frame_2->tracking_token()));
  EXPECT_EQ(frame_1->tracking_token(), frame_2->tracking_token());

  scoped_refptr<FrameResource> frame_3 = GetFrame(20);
  EXPECT_NE(pool_->GetOriginalFrame(frame_1->tracking_token()),
            pool_->GetOriginalFrame(frame_3->tracking_token()));
  EXPECT_NE(frame_1->tracking_token(), frame_3->tracking_token());
}

TEST_P(PlatformVideoFramePoolTest,
       InitializeWithSameFourccUsableAreaAndCodedSize) {
  const auto fourcc = Fourcc::FromVideoPixelFormat(GetParam());
  ASSERT_TRUE(fourcc.has_value());

  constexpr gfx::Size kCodedSize(640, 368);
  constexpr gfx::Rect kInitialVisibleRect(kCodedSize);
  ASSERT_TRUE(Initialize(kCodedSize, kInitialVisibleRect, fourcc.value()));
  scoped_refptr<FrameResource> frame1 = GetFrame(10);
  base::UnguessableToken id1 = frame1->tracking_token();

  // Clear frame references to return the frames to the pool.
  frame1 = nullptr;
  task_environment_.RunUntilIdle();

  // Request frame with the same format, coded size, and "usable area." To make
  // things interesting, we change the visible rect while keeping the
  // "usable area" constant. The usable area is the area of the buffer starting
  // at (0, 0) and extending to the bottom-right corner of the visible
  // rectangle. It corresponds to the area used to import the video frame into a
  // graphics API or to create the DRM framebuffer for hardware overlay
  // purposes. The pool should not request new frames.
  constexpr gfx::Rect kDifferentVisibleRect(10, 20, 630, 348);
  ASSERT_EQ(GetRectSizeFromOrigin(kInitialVisibleRect),
            GetRectSizeFromOrigin(kDifferentVisibleRect));
  ASSERT_TRUE(Initialize(kCodedSize, kDifferentVisibleRect, fourcc.value()));

  scoped_refptr<FrameResource> frame2 = GetFrame(20);
  base::UnguessableToken id2 = frame2->tracking_token();
  EXPECT_EQ(id1, id2);
}

TEST_P(PlatformVideoFramePoolTest, InitializeFail) {
  const auto fourcc = Fourcc::FromVideoPixelFormat(GetParam());
  ASSERT_TRUE(fourcc.has_value());
  pool_->SetCustomFrameAllocator(
      base::BindRepeating([](VideoPixelFormat format,
                             const gfx::Size& coded_size,
                             const gfx::Rect& visible_rect,
                             const gfx::Size& natural_size, bool use_protected,
                             bool use_linear_buffers, bool needs_detiling,
                             base::TimeDelta timestamp) {
        return CroStatus::Or<scoped_refptr<FrameResource>>(
            CroStatus::Codes::kFailedToCreateVideoFrame);
      }),
      VideoFrame::STORAGE_DMABUFS);

  EXPECT_FALSE(Initialize(fourcc.value()));
}

TEST_P(PlatformVideoFramePoolTest, ModifierIsPassed) {
  const uint64_t kSampleModifier = 0x001234567890abcdULL;
  const auto fourcc = Fourcc::FromVideoPixelFormat(GetParam());
  ASSERT_TRUE(fourcc.has_value());
  pool_->SetCustomFrameAllocator(
      base::BindRepeating(&CreateGpuMemoryBufferFrameResource<kSampleModifier>),
      VideoFrame::STORAGE_DMABUFS);
  ASSERT_TRUE(Initialize(fourcc.value()));

  EXPECT_EQ(layout_->modifier(), kSampleModifier);
  EXPECT_TRUE(GetFrame(10));
}

// TODO(akahuang): Add a testcase to verify calling Initialize() only with
// different |max_num_frames|.

}  // namespace media
