chromium/media/gpu/chromeos/image_processor_perf_test.cc

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

#include <sys/mman.h>
#include <sys/poll.h>

#include <vector>

#include "base/bits.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "components/viz/common/resources/shared_image_format.h"
#include "gpu/command_buffer/common/mailbox.h"
#include "gpu/command_buffer/common/shared_image_usage.h"
#include "gpu/command_buffer/service/shared_context_state.h"
#include "gpu/command_buffer/service/shared_image/shared_image_backing.h"
#include "gpu/command_buffer/service/shared_image/shared_image_factory.h"
#include "gpu/command_buffer/service/shared_image/shared_image_manager.h"
#include "gpu/command_buffer/service/shared_image/shared_image_representation.h"
#include "gpu/config/gpu_feature_info.h"
#include "media/gpu/chromeos/chromeos_compressed_gpu_memory_buffer_video_frame_utils.h"
#include "media/gpu/chromeos/image_processor_factory.h"
#include "media/gpu/chromeos/perf_test_util.h"
#include "media/gpu/chromeos/platform_video_frame_utils.h"
#include "media/gpu/chromeos/vulkan_overlay_adaptor.h"
#include "media/gpu/test/image.h"
#include "media/gpu/test/video_test_environment.h"
#include "media/gpu/video_frame_mapper_factory.h"
#include "testing/perf/perf_result_reporter.h"
#include "third_party/libyuv/include/libyuv.h"
#include "ui/gl/gl_context.h"
#include "ui/gl/gl_surface.h"
#include "ui/gl/gl_utils.h"
#include "ui/gl/init/gl_factory.h"
#include "ui/gl/scoped_make_current.h"
#include "ui/gl/test/gl_surface_test_support.h"

#if BUILDFLAG(IS_OZONE)
#include "ui/ozone/public/ozone_platform.h"
#endif

// Only testing for V4L2 devices since we have no devices that need image
// processing and use VA-API.
#if !BUILDFLAG(USE_V4L2_CODEC)
#error "Only V4L2 implemented."
#endif

static constexpr int kMM21TileWidth = 32;
static constexpr int kMM21TileHeight = 16;

static constexpr int kNumberOfTestFrames = 10;
static constexpr int kNumberOfTestCycles = 200;
static constexpr int kNumberOfCappedTestCycles = 200;

static constexpr int kTestImageWidth = 1920;
static constexpr int kTestImageHeight = 1088;

static constexpr int kRandomFrameSeed = 1000;

static constexpr int kUsecPerFrameAt60fps = 16666;

static constexpr int kUpScalingOutputWidth = 1920;
static constexpr int kUpScalingOutputHeight = 1080;

static constexpr int kDownScalingOutputWidth = 480;
static constexpr int kDownScalingOutputHeight = 270;

namespace media {
namespace {

const char* usage_msg =
    "usage: image_processor_perftest\n"
    "[--gtest_help] [--help] [-v=<level>] [--vmodule=<config>] \n";

const char* help_msg =
    "Run the image processor perf tests.\n\n"
    "The following arguments are supported:\n"
    "  --gtest_help          display the gtest help and exit.\n"
    "  --help                display this help and exit.\n"
    "  --source_directory    specify test input files location.\n"
    "  --output_directory    specify test output file location. Defaults to "
    "                        execution directory if not specified.\n"
    "   -v                   enable verbose mode, e.g. -v=2.\n"
    "  --vmodule             enable verbose mode for the specified module.\n";

base::FilePath BuildSourceFilePath(const base::FilePath& filename) {
  return media::g_source_directory.Append(filename);
}

constexpr char kNV12Image720P[] =
    FILE_PATH_LITERAL("puppets-1280x720.nv12.yuv");

bool SupportsNecessaryGLExtension() {
  scoped_refptr<gl::GLSurface> gl_surface =
      gl::init::CreateOffscreenGLSurface(gl::GetDefaultDisplay(), gfx::Size());
  if (!gl_surface) {
    LOG(ERROR) << "Error creating GL surface";
    return false;
  }
  scoped_refptr<gl::GLContext> gl_context = gl::init::CreateGLContext(
      nullptr, gl_surface.get(), gl::GLContextAttribs());
  if (!gl_context) {
    LOG(ERROR) << "Error creating GL context";
    return false;
  }
  ui::ScopedMakeCurrent current_context(gl_context.get(), gl_surface.get());
  if (!current_context.IsContextCurrent()) {
    LOG(ERROR) << "Error making GL context current";
    return false;
  }

  return gl_context->HasExtension("GL_EXT_YUV_target");
}
scoped_refptr<VideoFrame> CreateNV12Frame(const gfx::Size& size,
                                          VideoFrame::StorageType type) {
  const gfx::Rect visible_rect(size);
  constexpr base::TimeDelta kNullTimestamp;
  if (type == VideoFrame::STORAGE_GPU_MEMORY_BUFFER) {
    return CreateGpuMemoryBufferVideoFrame(
        VideoPixelFormat::PIXEL_FORMAT_NV12, size, visible_rect, size,
        kNullTimestamp, gfx::BufferUsage::SCANOUT_CPU_READ_WRITE);
  } else if (type == VideoFrame::STORAGE_DMABUFS) {
    return CreatePlatformVideoFrame(VideoPixelFormat::PIXEL_FORMAT_NV12, size,
                                    visible_rect, size, kNullTimestamp,
                                    gfx::BufferUsage::SCANOUT_CPU_READ_WRITE);
  } else {
    DCHECK_EQ(type, VideoFrame::STORAGE_OWNED_MEMORY);
    return VideoFrame::CreateFrame(VideoPixelFormat::PIXEL_FORMAT_NV12, size,
                                   visible_rect, size, kNullTimestamp);
  }
}

scoped_refptr<VideoFrame> CreateRandomMM21Frame(const gfx::Size& size,
                                                VideoFrame::StorageType type) {
  DCHECK_EQ(size.width(), base::bits::AlignUpDeprecatedDoNotUse(
                              size.width(), kMM21TileWidth));
  DCHECK_EQ(size.height(), base::bits::AlignUpDeprecatedDoNotUse(
                               size.height(), kMM21TileHeight));

  scoped_refptr<VideoFrame> frame = CreateNV12Frame(size, type);
  if (!frame) {
    LOG(ERROR) << "Failed to create MM21 frame";
    return nullptr;
  }

  scoped_refptr<VideoFrame> mapped_frame;
  if (type != VideoFrame::STORAGE_OWNED_MEMORY) {
    // The MM21 path only makes sense for V4L2, so we should never get an Intel
    // media compressed buffer here.
    CHECK(!IsIntelMediaCompressedModifier(frame->layout().modifier()));
    std::unique_ptr<VideoFrameMapper> frame_mapper =
        VideoFrameMapperFactory::CreateMapper(
            VideoPixelFormat::PIXEL_FORMAT_NV12, type,
            /*force_linear_buffer_mapper=*/true,
            /*must_support_intel_media_compressed_buffers=*/false);
    if (!frame_mapper) {
      LOG(ERROR) << "Unable to create a VideoFrameMapper";
      return nullptr;
    }
    mapped_frame = frame_mapper->Map(frame, PROT_READ | PROT_WRITE);
    if (!mapped_frame) {
      LOG(ERROR) << "Unable to map MM21 frame";
      return nullptr;
    }
  } else {
    mapped_frame = frame;
  }

  uint8_t* y_plane_ptr =
      mapped_frame->GetWritableVisibleData(VideoFrame::Plane::kY);
  const auto y_plane_stride = mapped_frame->stride(VideoFrame::Plane::kY);
  base::span<uint8_t> y_plane =
      // TODO(crbug.com/338570700): VideoFrame should return spans instead of
      // unbounded pointers.
      UNSAFE_TODO(base::span(
          y_plane_ptr,
          y_plane_stride *
              base::checked_cast<size_t>(mapped_frame->coded_size().height())));

  uint8_t* uv_plane_ptr =
      mapped_frame->GetWritableVisibleData(VideoFrame::Plane::kUV);
  const auto uv_plane_stride = mapped_frame->stride(VideoFrame::Plane::kUV);
  base::span<uint8_t> uv_plane =
      // TODO(crbug.com/338570700): VideoFrame should return spans instead of
      // unbounded pointers. Note: Elsewhere the `height / 2` is rounded up, but
      // here it is not.
      UNSAFE_TODO(base::span(
          uv_plane_ptr,
          uv_plane_stride *
              base::checked_cast<size_t>(mapped_frame->coded_size().height()) /
              2u));
  const auto width =
      base::checked_cast<size_t>(mapped_frame->coded_size().width());

  for (int row = 0; row < size.height(); row++) {
    auto [row_bytes, rem] = y_plane.split_at(y_plane_stride);
    base::RandBytes(row_bytes.first(width));
    y_plane = rem;
  }
  for (int row = 0; row < size.height() / 2; row++) {
    auto [row_bytes, rem] = uv_plane.split_at(uv_plane_stride);
    base::RandBytes(row_bytes.first(width));
    uv_plane = rem;
  }

  return frame;
}

class ImageProcessorPerfTest : public ::testing::Test {
 public:
  enum TestType {
    kMM21Detiling,
    kNV12Downscaling,
    kNV12Upscaling,
  };

  void InitializeImageProcessorTest(bool use_cpu_memory, TestType test_type) {
    test_image_size_.SetSize(kTestImageWidth, kTestImageHeight);
    ASSERT_TRUE((test_image_size_.width() % kMM21TileWidth) == 0);
    ASSERT_TRUE((test_image_size_.height() % kMM21TileHeight) == 0);

    test_image_visible_rect_.set_size(test_image_size_);

    candidate_.size = test_image_size_;
    candidate_.fourcc = test_type == kMM21Detiling ? Fourcc(Fourcc::MM21)
                                                   : Fourcc(Fourcc::NV12);
    candidates_ = {candidate_};

    scoped_refptr<base::SequencedTaskRunner> client_task_runner_ =
        base::SequencedTaskRunner::GetCurrentDefault();
    base::RepeatingClosure quit_closure_ = run_loop_.QuitClosure();
    bool image_processor_error_ = false;

    input_frames_.reserve(kNumberOfTestFrames);
    for (int i = 0; i < kNumberOfTestFrames; i++) {
      input_frames_.push_back(CreateRandomMM21Frame(
          test_image_size_, use_cpu_memory ? VideoFrame::STORAGE_OWNED_MEMORY
                                           : VideoFrame::STORAGE_DMABUFS));
    }

    gfx::Size output_size = test_image_size_;
    if (test_type == kNV12Downscaling) {
      output_size =
          gfx::Size(kDownScalingOutputWidth, kDownScalingOutputHeight);
    } else if (test_type == kNV12Upscaling) {
      output_size = gfx::Size(kUpScalingOutputWidth, kUpScalingOutputHeight);
    }

    ASSERT_EQ(test_type == kMM21Detiling, output_size == test_image_size_);
    output_frame_ =
        CreateNV12Frame(output_size, VideoFrame::STORAGE_GPU_MEMORY_BUFFER);
    ASSERT_TRUE(output_frame_) << "Error creating output frame";

    error_cb_ = base::BindRepeating(
        [](scoped_refptr<base::SequencedTaskRunner> client_task_runner_,
           base::RepeatingClosure quit_closure_, bool* image_processor_error_) {
          CHECK(client_task_runner_->RunsTasksInCurrentSequence());
          ASSERT_TRUE(false);
          quit_closure_.Run();
        },
        client_task_runner_, quit_closure_, &image_processor_error_);

    pick_format_cb_ = base::BindRepeating(
        [](const std::vector<Fourcc>&, std::optional<Fourcc>) {
          return std::make_optional<Fourcc>(Fourcc::NV12);
        });
  }

  void InitializeInputImage(bool use_cpu_memory) {
    std::unique_ptr<test::Image> input_image = std::make_unique<test::Image>(
        BuildSourceFilePath(base::FilePath(kNV12Image720P)));
    ASSERT_TRUE(input_image->Load());
    ASSERT_TRUE(input_image->LoadMetadata());
    const auto input_layout = test::CreateVideoFrameLayout(
        input_image->PixelFormat(), input_image->Size());
    ASSERT_TRUE(input_layout) << "Error creating input layout.";

    scoped_refptr<const VideoFrame> tmp_video_frame =
        CreateVideoFrameFromImage(*input_image);
    ASSERT_TRUE(tmp_video_frame);

    input_image_frame_ = test::CloneVideoFrame(
        tmp_video_frame.get(), *input_layout,
        use_cpu_memory ? VideoFrame::STORAGE_OWNED_MEMORY
                       : VideoFrame::STORAGE_GPU_MEMORY_BUFFER,
        gfx::BufferUsage::SCANOUT_CPU_READ_WRITE);
    ASSERT_TRUE(input_image_frame_) << "Error creating input frame.";

    candidate_.fourcc = Fourcc(Fourcc::NV12);
    candidate_.size = input_image->Size();
    candidates_ = {candidate_};
  }

  gfx::Size test_image_size_;
  gfx::Rect test_image_visible_rect_;
  ImageProcessor::PixelLayoutCandidate candidate_{Fourcc(Fourcc::MM21),
                                                  gfx::Size()};
  std::vector<ImageProcessor::PixelLayoutCandidate> candidates_;
  base::RunLoop run_loop_;
  std::vector<scoped_refptr<VideoFrame>> input_frames_;
  scoped_refptr<VideoFrame> output_frame_;
  ImageProcessor::ErrorCB error_cb_;
  ImageProcessorFactory::PickFormatCB pick_format_cb_;
  scoped_refptr<VideoFrame> input_image_frame_;
};

// Tests GLImageProcessor by feeding in |kNumberOfTestFrames| unique input
// frames looped over |kNumberOfTestCycles| iterations to the GLImageProcessor
// as fast as possible. Will print out elapsed processing time.
TEST_F(ImageProcessorPerfTest, UncappedGLImageProcessorPerfTest) {
  if (!SupportsNecessaryGLExtension()) {
    GTEST_SKIP() << "Skipping GL Backend test, unsupported platform.";
  }

  InitializeImageProcessorTest(/*use_cpu_memory=*/false, kMM21Detiling);

  scoped_refptr<base::SequencedTaskRunner> client_task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  base::RepeatingClosure quit_closure = run_loop_.QuitClosure();
  std::unique_ptr<ImageProcessor> gl_image_processor = ImageProcessorFactory::
      CreateGLImageProcessorWithInputCandidatesForTesting(
          candidates_, test_image_visible_rect_, test_image_size_,
          /*num_buffers=*/1, client_task_runner, pick_format_cb_, error_cb_);
  ASSERT_TRUE(gl_image_processor) << "Error creating GLImageProcessor";

  LOG(INFO) << "Running GLImageProcessor Uncapped Perf Test";
  int outstanding_processors = kNumberOfTestCycles;
  for (int num_cycles = outstanding_processors; num_cycles > 0; num_cycles--) {
    ImageProcessor::FrameReadyCB gl_callback =
        base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> frame) {
          CHECK(client_task_runner->RunsTasksInCurrentSequence());
          if (!(--outstanding_processors)) {
            quit_closure.Run();
          }
        });
    gl_image_processor->Process(input_frames_[num_cycles % kNumberOfTestFrames],
                                output_frame_, std::move(gl_callback));
  }

  auto start_time = base::TimeTicks::Now();
  run_loop_.Run();
  auto end_time = base::TimeTicks::Now();
  base::TimeDelta delta_time = end_time - start_time;
  const double fps = (kNumberOfTestCycles / delta_time.InSecondsF());

  WriteJsonResult({{"FramesDecoded", kNumberOfTestCycles},
                   {"TotalDurationMs", delta_time.InMicrosecondsF()},
                   {"FramesPerSecond", fps}});
}

// Tests the LibYUV by feeding in |kNumberOfTestFrames| unique input
// frames looped over |kNumberOfTestCycles| iterations to the LibYUV
// as fast as possible. Will print out elapsed processing time.
TEST_F(ImageProcessorPerfTest, UncappedLibYUVPerfTest) {
  InitializeImageProcessorTest(/*use_cpu_memory=*/true, kMM21Detiling);

  scoped_refptr<base::SequencedTaskRunner> client_task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  base::RepeatingClosure quit_closure = run_loop_.QuitClosure();

  std::unique_ptr<ImageProcessor> libyuv_image_processor =
      ImageProcessorFactory::
          CreateLibYUVImageProcessorWithInputCandidatesForTesting(
              candidates_, test_image_visible_rect_, test_image_size_,
              /*num_buffers=*/1, client_task_runner, pick_format_cb_,
              error_cb_);
  ASSERT_TRUE(libyuv_image_processor) << "Error creating LibYUV";

  LOG(INFO) << "Running LibYUV Uncapped Perf Test";
  int outstanding_processors = kNumberOfTestCycles;
  for (int num_cycles = outstanding_processors; num_cycles > 0; num_cycles--) {
    ImageProcessor::FrameReadyCB libyuv_callback =
        base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> frame) {
          CHECK(client_task_runner->RunsTasksInCurrentSequence());
          if (!(--outstanding_processors)) {
            quit_closure.Run();
          }
        });
    libyuv_image_processor->Process(
        input_frames_[num_cycles % kNumberOfTestFrames], output_frame_,
        std::move(libyuv_callback));
  }

  auto start_time = base::TimeTicks::Now();
  run_loop_.Run();
  auto end_time = base::TimeTicks::Now();
  base::TimeDelta delta_time = end_time - start_time;
  // Preventing integer division inaccuracies with |delta_time|.
  const double fps = (kNumberOfTestCycles / delta_time.InSecondsF());

  WriteJsonResult({{"FramesDecoded", kNumberOfTestCycles},
                   {"TotalDurationMs", delta_time.InMicroseconds()},
                   {"FramesPerSecond", fps}});
}

// Tests GLImageProcessor by feeding in |kNumberOfTestFrames| unique input
// frames looped over |kNumberOfCappedTestCycles| iterations to the
// GLImageProcessor at 60fps. Will print out elapsed processing time.
TEST_F(ImageProcessorPerfTest, CappedGLImageProcessorPerfTest) {
  if (!SupportsNecessaryGLExtension()) {
    GTEST_SKIP() << "Skipping GL Backend test, unsupported platform.";
  }

  InitializeImageProcessorTest(/*use_cpu_memory=*/false, kMM21Detiling);

  scoped_refptr<base::SequencedTaskRunner> client_task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  base::RepeatingClosure quit_closure = run_loop_.QuitClosure();
  std::unique_ptr<ImageProcessor> gl_image_processor = ImageProcessorFactory::
      CreateGLImageProcessorWithInputCandidatesForTesting(
          candidates_, test_image_visible_rect_, test_image_size_,
          /*num_buffers=*/1, client_task_runner, pick_format_cb_, error_cb_);
  ASSERT_TRUE(gl_image_processor) << "Error creating GLImageProcessor";

  LOG(INFO) << "Running GLImageProcessor Capped Perf Test";
  int loop_iterations = kNumberOfCappedTestCycles;
  base::TimeTicks start, end;
  base::RepeatingCallback<void(scoped_refptr<VideoFrame>)>* gl_callback_ptr;
  base::RepeatingCallback gl_callback =
      base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> frame) {
        CHECK(client_task_runner->RunsTasksInCurrentSequence());

        if (!(--loop_iterations)) {
          quit_closure.Run();
        } else {
          end = base::TimeTicks::Now();
          base::TimeDelta delta_time = end - start;
          if (delta_time.InMicroseconds() < kUsecPerFrameAt60fps) {
            usleep(kUsecPerFrameAt60fps - delta_time.InMicroseconds());
          } else {
            LOG(WARNING) << "Frame detiling was late by "
                         << (delta_time.InMicroseconds() - kUsecPerFrameAt60fps)
                         << "us";
          }
          start = base::TimeTicks::Now();
          gl_image_processor->Process(
              input_frames_[loop_iterations % kNumberOfTestFrames],
              output_frame_, *gl_callback_ptr);
        }
      });

  gl_callback_ptr = &gl_callback;

  gl_image_processor->Process(input_frames_[0], output_frame_,
                              *gl_callback_ptr);

  start = base::TimeTicks::Now();
  auto total_start_time = base::TimeTicks::Now();
  run_loop_.Run();

  auto total_end_time = base::TimeTicks::Now();
  base::TimeDelta total_delta_time = total_end_time - total_start_time;
  const double fps =
      (kNumberOfCappedTestCycles / total_delta_time.InSecondsF());

  WriteJsonResult({{"FramesDecoded", kNumberOfCappedTestCycles},
                   {"TotalDurationMs", total_delta_time.InMicroseconds()},
                   {"FramesPerSecond", fps}});
}

// Tests LibYUV by feeding in |kNumberOfTestFrames| unique input
// frames looped over |kNumberOfCappedTestCycles| iterations to the
// LibYUV at 60fps. Will print out elapsed processing time.
TEST_F(ImageProcessorPerfTest, CappedLibYUVPerfTest) {
  InitializeImageProcessorTest(/*use_cpu_memory=*/true, kMM21Detiling);

  scoped_refptr<base::SequencedTaskRunner> client_task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  base::RepeatingClosure quit_closure = run_loop_.QuitClosure();
  std::unique_ptr<ImageProcessor> libyuv_image_processor =
      ImageProcessorFactory::
          CreateLibYUVImageProcessorWithInputCandidatesForTesting(
              candidates_, test_image_visible_rect_, test_image_size_,
              /*num_buffers=*/1, client_task_runner, pick_format_cb_,
              error_cb_);
  ASSERT_TRUE(libyuv_image_processor) << "Error creating LibYUV";

  LOG(INFO) << "Running LibYUV Capped Perf Test";
  int loop_iterations = 0;
  base::TimeTicks start, end;
  base::RepeatingCallback<void(scoped_refptr<VideoFrame>)>* libyuv_callback_ptr;
  base::RepeatingCallback libyuv_callback =
      base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> frame) {
        CHECK(client_task_runner->RunsTasksInCurrentSequence());

        if ((++loop_iterations) >= kNumberOfCappedTestCycles) {
          quit_closure.Run();
        } else {
          end = base::TimeTicks::Now();
          base::TimeDelta delta_time = end - start;
          if (delta_time.InMicroseconds() < kUsecPerFrameAt60fps) {
            usleep(kUsecPerFrameAt60fps - delta_time.InMicroseconds());
          } else {
            LOG(WARNING) << "Frame detiling was late by "
                         << (delta_time.InMicroseconds() - kUsecPerFrameAt60fps)
                         << "us";
          }
          start = base::TimeTicks::Now();
          libyuv_image_processor->Process(
              input_frames_[loop_iterations % kNumberOfTestFrames],
              output_frame_, *libyuv_callback_ptr);
        }
      });

  libyuv_callback_ptr = &libyuv_callback;

  libyuv_image_processor->Process(input_frames_[0], output_frame_,
                                  *libyuv_callback_ptr);

  start = base::TimeTicks::Now();
  auto total_start_time = base::TimeTicks::Now();
  run_loop_.Run();

  auto total_end_time = base::TimeTicks::Now();
  base::TimeDelta total_delta_time = total_end_time - total_start_time;
  const double fps =
      (kNumberOfCappedTestCycles / total_delta_time.InSecondsF());

  WriteJsonResult({{"FramesDecoded", kNumberOfCappedTestCycles},
                   {"TotalDurationMs", total_delta_time.InMicroseconds()},
                   {"FramesPerSecond", fps}});
}

// Tests the GLImageProcessor by feeding in a 1280x720 NV12 input frame and
// scaling it up to 1920x1080 and then scaling it back down to its original
// size. Will print out the PSNR calculation for each plane and verify that
// the PSNR values are greater than 40.0.
TEST_F(ImageProcessorPerfTest, GLNV12ScalingComparisonTest) {
  InitializeInputImage(/*use_cpu_memory=*/false);

  base::RunLoop run_loop;
  auto client_task_runner = base::SequencedTaskRunner::GetCurrentDefault();
  base::RepeatingClosure quit_closure = run_loop.QuitClosure();
  bool image_processor_error = false;
  ImageProcessor::ErrorCB error_cb = base::BindRepeating(
      [](scoped_refptr<base::SequencedTaskRunner> client_task_runner,
         base::RepeatingClosure quit_closure, bool* image_processor_error) {
        CHECK(client_task_runner->RunsTasksInCurrentSequence());
        *image_processor_error = true;
        quit_closure.Run();
      },
      client_task_runner, quit_closure, &image_processor_error);
  ImageProcessorFactory::PickFormatCB pick_format_cb = base::BindRepeating(
      [](const std::vector<Fourcc>&, std::optional<Fourcc>) {
        return std::make_optional<Fourcc>(Fourcc::NV12);
      });

  std::unique_ptr<ImageProcessor> gl_upscaling_image_processor =
      ImageProcessorFactory::
          CreateGLImageProcessorWithInputCandidatesForTesting(
              candidates_, gfx::Rect(input_image_frame_->coded_size()),
              gfx::Size(kUpScalingOutputWidth, kUpScalingOutputHeight),
              /*num_buffers=*/1, client_task_runner, pick_format_cb, error_cb);
  ASSERT_TRUE(gl_upscaling_image_processor)
      << "Error creating GLImageProcessor";

  const ImageProcessor::PixelLayoutCandidate downscaling_candidate = {
      Fourcc(Fourcc::NV12),
      gfx::Size(kUpScalingOutputWidth, kUpScalingOutputHeight)};
  std::vector<ImageProcessor::PixelLayoutCandidate> downscaling_candidates = {
      downscaling_candidate};

  std::unique_ptr<ImageProcessor> gl_downscaling_image_processor =
      ImageProcessorFactory::
          CreateGLImageProcessorWithInputCandidatesForTesting(
              downscaling_candidates,
              gfx::Rect(kUpScalingOutputWidth, kUpScalingOutputHeight),
              input_image_frame_->coded_size(),
              /*num_buffers=*/1, client_task_runner, pick_format_cb, error_cb);
  ASSERT_TRUE(gl_downscaling_image_processor)
      << "Error creating GLImageProcessor";

  scoped_refptr<VideoFrame> gl_upscaling_output_frame =
      CreateNV12Frame(gfx::Size(kUpScalingOutputWidth, kUpScalingOutputHeight),
                      VideoFrame::STORAGE_GPU_MEMORY_BUFFER);
  ASSERT_TRUE(gl_upscaling_output_frame) << "Error creating GL output frame";

  scoped_refptr<VideoFrame> gl_downscaling_output_frame = CreateNV12Frame(
      input_image_frame_->coded_size(), VideoFrame::STORAGE_GPU_MEMORY_BUFFER);
  ASSERT_TRUE(gl_downscaling_output_frame) << "Error creating GL output frame";

  ImageProcessor::FrameReadyCB gl_callback2 =
      base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> frame) {
        CHECK(client_task_runner->RunsTasksInCurrentSequence());
        gl_downscaling_output_frame = std::move(frame);
        quit_closure.Run();
      });

  ImageProcessor::FrameReadyCB gl_callback1 =
      base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> frame) {
        CHECK(client_task_runner->RunsTasksInCurrentSequence());
        gl_downscaling_image_processor->Process(
            frame, gl_downscaling_output_frame, std::move(gl_callback2));
      });

  gl_upscaling_image_processor->Process(
      input_image_frame_, gl_upscaling_output_frame, std::move(gl_callback1));
  run_loop.Run();

  // The image processor perf tests are currently only available with V4L2, and
  // we should never get Intel media compressed buffers in V4L2 platforms.
  ASSERT_FALSE(IsIntelMediaCompressedModifier(
      gl_downscaling_output_frame->layout().modifier()));
  ASSERT_FALSE(
      IsIntelMediaCompressedModifier(input_image_frame_->layout().modifier()));

  const std::unique_ptr<VideoFrameMapper> output_frame_mapper =
      VideoFrameMapperFactory::CreateMapper(
          PIXEL_FORMAT_NV12, VideoFrame::STORAGE_GPU_MEMORY_BUFFER,
          /*force_linear_buffer_mapper=*/true,
          /*must_support_intel_media_compressed_buffers=*/false);
  ASSERT_TRUE(output_frame_mapper);

  const scoped_refptr<VideoFrame> mapped_gl_output = output_frame_mapper->Map(
      gl_downscaling_output_frame, PROT_READ | PROT_WRITE);
  ASSERT_TRUE(mapped_gl_output);

  const scoped_refptr<VideoFrame> mapped_input_frame =
      output_frame_mapper->Map(input_image_frame_, PROT_READ | PROT_WRITE);
  ASSERT_TRUE(mapped_input_frame);

  const gfx::Size image_size = mapped_gl_output->visible_rect().size();
  const gfx::Size half_image_size((image_size.width() + 1) / 2,
                                  (image_size.height() + 1) / 2);

  uint8_t* out_y = static_cast<uint8_t*>(
      mmap(nullptr, image_size.GetArea(), PROT_READ | PROT_WRITE,
           MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
  uint8_t* out_u = static_cast<uint8_t*>(
      mmap(nullptr, half_image_size.GetArea(), PROT_READ | PROT_WRITE,
           MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
  uint8_t* out_v = static_cast<uint8_t*>(
      mmap(nullptr, half_image_size.GetArea(), PROT_READ | PROT_WRITE,
           MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
  libyuv::NV12ToI420(
      mapped_gl_output->visible_data(0), mapped_gl_output->stride(0),
      mapped_gl_output->visible_data(1), mapped_gl_output->stride(1), out_y,
      image_size.width(), out_u, half_image_size.width(), out_v,
      half_image_size.width(), image_size.width(), image_size.height());

  uint8_t* input_y = static_cast<uint8_t*>(
      mmap(nullptr, image_size.GetArea(), PROT_READ | PROT_WRITE,
           MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
  uint8_t* input_u = static_cast<uint8_t*>(
      mmap(nullptr, half_image_size.GetArea(), PROT_READ | PROT_WRITE,
           MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
  uint8_t* input_v = static_cast<uint8_t*>(
      mmap(nullptr, half_image_size.GetArea(), PROT_READ | PROT_WRITE,
           MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));

  libyuv::NV12ToI420(
      mapped_input_frame->visible_data(0), mapped_input_frame->stride(0),
      mapped_input_frame->visible_data(1), mapped_input_frame->stride(1),
      input_y, image_size.width(), input_u, half_image_size.width(), input_v,
      half_image_size.width(), image_size.width(), image_size.height());

  double psnr_test = libyuv::I420Psnr(
      out_y, image_size.width(), out_u, half_image_size.width(), out_v,
      half_image_size.width(), input_y, image_size.width(), input_u,
      half_image_size.width(), input_v, half_image_size.width(),
      image_size.width(), image_size.height());

  munmap(out_y, image_size.GetArea());
  munmap(out_u, half_image_size.GetArea());
  munmap(out_v, half_image_size.GetArea());
  munmap(input_y, image_size.GetArea());
  munmap(input_u, half_image_size.GetArea());
  munmap(input_v, half_image_size.GetArea());

  perf_test::PerfResultReporter reporter("GLImageProcessor NV12 Scaling",
                                         "PSNR Test");
  reporter.RegisterImportantMetric(".PSNR", "decibels");
  reporter.AddResult(".PSNR", psnr_test);

  WriteJsonResult({{"PSNR", psnr_test}});
}

// Tests GLImageProcessor by feeding in |kNumberOfTestFrames| unique NV12 input
// frames looped over |kNumberOfTestCycles| iterations to the GLImageProcessor
// to be downscaled to 480x270. Will print out elapsed processing time.
TEST_F(ImageProcessorPerfTest, GLImageProcessorNV12DownscalingTest) {
  InitializeImageProcessorTest(/*use_cpu_memory=*/false, kNV12Downscaling);

  scoped_refptr<base::SequencedTaskRunner> client_task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  base::RepeatingClosure quit_closure = run_loop_.QuitClosure();
  std::unique_ptr<ImageProcessor> gl_image_processor = ImageProcessorFactory::
      CreateGLImageProcessorWithInputCandidatesForTesting(
          candidates_, test_image_visible_rect_,
          gfx::Size(kDownScalingOutputWidth, kDownScalingOutputHeight),
          /*num_buffers=*/1, client_task_runner, pick_format_cb_, error_cb_);
  ASSERT_TRUE(gl_image_processor) << "Error creating GLImageProcessor";

  LOG(INFO) << "Running GLImageProcessor NV12 Downscaling Test";
  int outstanding_processors = kNumberOfTestCycles;
  for (int num_cycles = outstanding_processors; num_cycles > 0; num_cycles--) {
    ImageProcessor::FrameReadyCB gl_callback =
        base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> frame) {
          CHECK(client_task_runner->RunsTasksInCurrentSequence());
          if (!(--outstanding_processors)) {
            quit_closure.Run();
          }
        });
    gl_image_processor->Process(input_frames_[num_cycles % kNumberOfTestFrames],
                                output_frame_, std::move(gl_callback));
  }

  auto start_time = base::TimeTicks::Now();
  run_loop_.Run();
  auto end_time = base::TimeTicks::Now();
  base::TimeDelta delta_time = end_time - start_time;
  const double fps = (kNumberOfTestCycles / delta_time.InSecondsF());

  WriteJsonResult({{"FramesDecoded", kNumberOfTestCycles},
                   {"TotalDurationMs", delta_time.InMicrosecondsF()},
                   {"FramesPerSecond", fps}});
}

// Tests GLImageProcessor by feeding in |kNumberOfTestFrames| unique NV12 input
// frames looped over |kNumberOfTestCycles| iterations to the GLImageProcessor
// to be upscaled to 1920x1080. Will print out elapsed processing time.
TEST_F(ImageProcessorPerfTest, GLImageProcessorNV12UpscalingTest) {
  InitializeImageProcessorTest(/*use_cpu_memory=*/false, kNV12Upscaling);

  scoped_refptr<base::SequencedTaskRunner> client_task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  base::RepeatingClosure quit_closure = run_loop_.QuitClosure();
  std::unique_ptr<ImageProcessor> gl_image_processor = ImageProcessorFactory::
      CreateGLImageProcessorWithInputCandidatesForTesting(
          candidates_, test_image_visible_rect_,
          gfx::Size(kUpScalingOutputWidth, kUpScalingOutputHeight),
          /*num_buffers=*/1, client_task_runner, pick_format_cb_, error_cb_);
  ASSERT_TRUE(gl_image_processor) << "Error creating GLImageProcessor";

  LOG(INFO) << "Running GLImageProcessor NV12 Upscaling Test";
  int outstanding_processors = kNumberOfTestCycles;
  for (int num_cycles = outstanding_processors; num_cycles > 0; num_cycles--) {
    ImageProcessor::FrameReadyCB gl_callback =
        base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> frame) {
          CHECK(client_task_runner->RunsTasksInCurrentSequence());
          if (!(--outstanding_processors)) {
            quit_closure.Run();
          }
        });
    gl_image_processor->Process(input_frames_[num_cycles % kNumberOfTestFrames],
                                output_frame_, std::move(gl_callback));
  }

  auto start_time = base::TimeTicks::Now();
  run_loop_.Run();
  auto end_time = base::TimeTicks::Now();
  base::TimeDelta delta_time = end_time - start_time;
  const double fps = (kNumberOfTestCycles / delta_time.InSecondsF());

  WriteJsonResult({{"FramesDecoded", kNumberOfTestCycles},
                   {"TotalDurationMs", delta_time.InMicrosecondsF()},
                   {"FramesPerSecond", fps}});
}

// Tests LibYUV by feeding in |kNumberOfTestFrames| unique NV12 input
// frames looped over |kNumberOfTestCycles| iterations to the LibYUV
// to be downscaled to 480x270. Will print out elapsed processing time.
TEST_F(ImageProcessorPerfTest, LibYUVNV12DownscalingTest) {
  InitializeImageProcessorTest(/*use_cpu_memory=*/false, kNV12Downscaling);

  scoped_refptr<base::SequencedTaskRunner> client_task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  base::RepeatingClosure quit_closure = run_loop_.QuitClosure();

  std::unique_ptr<ImageProcessor> libyuv_image_processor =
      ImageProcessorFactory::
          CreateLibYUVImageProcessorWithInputCandidatesForTesting(
              candidates_, test_image_visible_rect_,
              gfx::Size(kDownScalingOutputWidth, kDownScalingOutputHeight),
              /*num_buffers=*/1, client_task_runner, pick_format_cb_,
              error_cb_);
  ASSERT_TRUE(libyuv_image_processor) << "Error creating LibYUV";

  LOG(INFO) << "Running LibYUV NV12 Downscaling Test";
  int outstanding_processors = kNumberOfTestCycles;
  for (int num_cycles = outstanding_processors; num_cycles > 0; num_cycles--) {
    ImageProcessor::FrameReadyCB libyuv_callback =
        base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> frame) {
          CHECK(client_task_runner->RunsTasksInCurrentSequence());
          if (!(--outstanding_processors)) {
            quit_closure.Run();
          }
        });
    libyuv_image_processor->Process(
        input_frames_[num_cycles % kNumberOfTestFrames], output_frame_,
        std::move(libyuv_callback));
  }

  auto start_time = base::TimeTicks::Now();
  run_loop_.Run();
  auto end_time = base::TimeTicks::Now();
  base::TimeDelta delta_time = end_time - start_time;
  // Preventing integer division inaccuracies with |delta_time|.
  const double fps = (kNumberOfTestCycles / delta_time.InSecondsF());

  WriteJsonResult({{"FramesDecoded", kNumberOfTestCycles},
                   {"TotalDurationMs", delta_time.InMicrosecondsF()},
                   {"FramesPerSecond", fps}});
}

// Tests LibYUV by feeding in |kNumberOfTestFrames| unique NV12 input
// frames looped over |kNumberOfTestCycles| iterations to the LibYUV
// to be upscaled to 1920x1080. Will print out elapsed processing time.
TEST_F(ImageProcessorPerfTest, LibYUVNV12UpscalingTest) {
  InitializeImageProcessorTest(/*use_cpu_memory=*/false, kNV12Upscaling);

  scoped_refptr<base::SequencedTaskRunner> client_task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  base::RepeatingClosure quit_closure = run_loop_.QuitClosure();

  std::unique_ptr<ImageProcessor> libyuv_image_processor =
      ImageProcessorFactory::
          CreateLibYUVImageProcessorWithInputCandidatesForTesting(
              candidates_, test_image_visible_rect_,
              gfx::Size(kUpScalingOutputWidth, kUpScalingOutputHeight),
              /*num_buffers=*/1, client_task_runner, pick_format_cb_,
              error_cb_);
  ASSERT_TRUE(libyuv_image_processor) << "Error creating LibYUV";

  LOG(INFO) << "Running LibYUV NV12 Upscaling Test";
  int outstanding_processors = kNumberOfTestCycles;
  for (int num_cycles = outstanding_processors; num_cycles > 0; num_cycles--) {
    ImageProcessor::FrameReadyCB libyuv_callback =
        base::BindLambdaForTesting([&](scoped_refptr<VideoFrame> frame) {
          CHECK(client_task_runner->RunsTasksInCurrentSequence());
          if (!(--outstanding_processors)) {
            quit_closure.Run();
          }
        });
    libyuv_image_processor->Process(
        input_frames_[num_cycles % kNumberOfTestFrames], output_frame_,
        std::move(libyuv_callback));
  }

  auto start_time = base::TimeTicks::Now();
  run_loop_.Run();
  auto end_time = base::TimeTicks::Now();
  base::TimeDelta delta_time = end_time - start_time;
  // Preventing integer division inaccuracies with |delta_time|.
  const double fps = (kNumberOfTestCycles / delta_time.InSecondsF());

  WriteJsonResult({{"FramesDecoded", kNumberOfTestCycles},
                   {"TotalDurationMs", delta_time.InMicrosecondsF()},
                   {"FramesPerSecond", fps}});
}

}  // namespace
}  // namespace media

int main(int argc, char** argv) {
  base::CommandLine::Init(argc, argv);

  // Print the help message if requested. This needs to be done before
  // initializing gtest, to overwrite the default gtest help message.
  const base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
  LOG_ASSERT(cmd_line);
  if (cmd_line->HasSwitch("help")) {
    std::cout << media::usage_msg << "\n" << media::help_msg;
    return 0;
  }

  base::CommandLine::SwitchMap switches = cmd_line->GetSwitches();
  for (base::CommandLine::SwitchMap::const_iterator it = switches.begin();
       it != switches.end(); ++it) {
    if (it->first.find("gtest_") == 0 ||               // Handled by GoogleTest
        it->first == "v" || it->first == "vmodule") {  // Handled by Chrome
      continue;
    }

    if (it->first == "source_directory") {
      media::g_source_directory = base::FilePath(it->second);
    } else if (it->first == "output_directory") {
      media::g_output_directory = base::FilePath(it->second);
    } else {
      std::cout << "unknown option: --" << it->first << "\n"
                << media::usage_msg;
      return EXIT_FAILURE;
    }
  }
  srand(kRandomFrameSeed);
  testing::InitGoogleTest(&argc, argv);

  auto* const test_environment = new media::test::VideoTestEnvironment;
  media::g_env = reinterpret_cast<media::test::VideoTestEnvironment*>(
      testing::AddGlobalTestEnvironment(test_environment));

// TODO(b/316374371) Try to remove Ozone and replace with EGL and GL.
#if BUILDFLAG(IS_OZONE)
  ui::OzonePlatform::InitParams ozone_param;
  ozone_param.single_process = true;
#if BUILDFLAG(ENABLE_VULKAN) && BUILDFLAG(USE_V4L2_CODEC)
  ui::OzonePlatform::InitializeForUI(ozone_param);
#endif
  ui::OzonePlatform::InitializeForGPU(ozone_param);
#endif

  gl::GLSurfaceTestSupport::InitializeOneOffImplementation(
      gl::GLImplementationParts(gl::kGLImplementationEGLGLES2));

  return RUN_ALL_TESTS();
}