chromium/components/chromeos_camera/jpeg_encode_accelerator_unittest.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stddef.h>
#include <stdint.h>
#include <string.h>

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

#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/read_only_shared_memory_region.h"
#include "base/memory/unsafe_shared_memory_region.h"
#include "base/path_service.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/chromeos_camera/gpu_jpeg_encode_accelerator_factory.h"
#include "components/chromeos_camera/jpeg_encode_accelerator.h"
#include "media/base/color_plane_layout.h"
#include "media/base/test_data_util.h"
#include "media/gpu/buildflags.h"
#include "media/gpu/chromeos/generic_dmabuf_video_frame_mapper.h"
#include "media/gpu/test/local_gpu_memory_buffer_manager.h"
#include "media/gpu/test/video_test_helpers.h"
#include "media/parsers/jpeg_parser.h"
#include "mojo/core/embedder/embedder.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/libyuv/include/libyuv.h"
#include "ui/gfx/codec/jpeg_codec.h"

#if BUILDFLAG(USE_VAAPI)
#include "media/gpu/vaapi/vaapi_wrapper.h"
#endif

namespace chromeos_camera {
namespace {

// Default test image file.
const base::FilePath::CharType kDefaultYuvFilename[] =
    FILE_PATH_LITERAL("bali_640x360_P420.yuv:640x360");
// Whether to save encode results to files. Output files will be saved in the
// same directory as the input files, with the '.jpg' extension appended to
// their names. The encode result of generated images is written to the current
// folder using HxW_[black|white].jpg as output file name.
bool g_save_to_file = false;

const double kMeanDiffThreshold = 10.0;
const int kJpegDefaultQuality = 90;
const int kJpegMaxSize = 1024 * 1024 * 13;

// Environment to create test data for all test cases.
class JpegEncodeAcceleratorTestEnvironment;
JpegEncodeAcceleratorTestEnvironment* g_env;

struct TestImage {
  TestImage(std::vector<uint8_t> image_data,
            const gfx::Size& visible_size,
            const base::FilePath output_filename)
      : image_data(std::move(image_data)),
        visible_size(visible_size),
        output_filename(output_filename) {}

  // Test image data.
  std::vector<uint8_t> image_data;
  gfx::Size visible_size;

  // Output filename, only used when '--save_to_file' is specified.
  base::FilePath output_filename;
  size_t output_size = 0;
};

enum class ClientState {
  CREATED,
  INITIALIZED,
  ENCODE_PASS,
  ERROR,
};

scoped_refptr<media::VideoFrame> GetVideoFrameFromGpuMemoryBuffer(
    gfx::GpuMemoryBuffer* buffer,
    gfx::Size size,
    media::VideoPixelFormat format) {
  auto buffer_handle = buffer->CloneHandle().native_pixmap_handle;

  size_t num_planes = media::VideoFrame::NumPlanes(format);
  std::vector<media::ColorPlaneLayout> planes(num_planes);
  std::vector<base::ScopedFD> fds(num_planes);
  for (size_t i = 0; i < num_planes; i++) {
    auto& plane = buffer_handle.planes[i];
    fds[i] = std::move(plane.fd);
    planes[i].stride = plane.stride;
    planes[i].offset = plane.offset;
    planes[i].size = plane.size;
  }

  gfx::Rect visible_rect(size);
  auto layout = media::VideoFrameLayout::CreateWithPlanes(format, size,
                                                          std::move(planes));
  return media::VideoFrame::WrapExternalDmabufs(
      *layout, visible_rect, size, std::move(fds), base::TimeDelta());
}

class JpegEncodeAcceleratorTestEnvironment : public ::testing::Environment {
 public:
  JpegEncodeAcceleratorTestEnvironment(
      const base::FilePath::CharType* yuv_filenames,
      const base::FilePath log_path,
      const int repeat)
      : repeat_(repeat), log_path_(log_path) {
    user_yuv_files_ = yuv_filenames ? yuv_filenames : kDefaultYuvFilename;
  }
  void SetUp() override;
  void TearDown() override;

  void LogToFile(const std::string& key, const std::string& value);

  // Read image from |filename| to |image_data|.
  std::unique_ptr<TestImage> ReadTestYuvImage(const base::FilePath& filename,
                                              const gfx::Size& image_size);

  // Returns a file path for a file in what name specified or media/test/data
  // directory.  If the original file path is existed, returns it first.
  base::FilePath GetOriginalOrTestDataFilePath(const std::string& name);

  // Parsed data from command line.
  std::vector<std::unique_ptr<TestImage>> image_data_user_;

  // Generated 2560x1920 white I420 image.
  std::unique_ptr<TestImage> image_data_2560x1920_white_;
  // Scarlet doesn't support 1080 width, it only suports 1088 width.
  // Generated 1280x720 white I420 image.
  std::unique_ptr<TestImage> image_data_1280x720_white_;
  // Generated 640x480 black I420 image.
  std::unique_ptr<TestImage> image_data_640x480_black_;
  // Generated 640x368 black I420 image.
  std::unique_ptr<TestImage> image_data_640x368_black_;
  // Generated 640x360 black I420 image.
  std::unique_ptr<TestImage> image_data_640x360_black_;

  // Number of times SimpleEncodeTest should repeat for an image.
  const size_t repeat_;

 private:
  // Create black or white test image with specified |size|.
  std::unique_ptr<TestImage> CreateTestYuvImage(const gfx::Size& image_size,
                                                bool is_black);

  const base::FilePath::CharType* user_yuv_files_;
  const base::FilePath log_path_;
  std::unique_ptr<base::File> log_file_;
};

void JpegEncodeAcceleratorTestEnvironment::SetUp() {
  // Since base::test::TaskEnvironment will call
  // TestTimeouts::action_max_timeout(), TestTimeouts::Initialize() needs to be
  // called in advance.
  TestTimeouts::Initialize();

  if (!log_path_.empty()) {
    log_file_.reset(new base::File(
        log_path_, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE));
    LOG_ASSERT(log_file_->IsValid());
  }

  image_data_2560x1920_white_ =
      CreateTestYuvImage(gfx::Size(2560, 1920), false);
  image_data_1280x720_white_ = CreateTestYuvImage(gfx::Size(1280, 720), false);
  image_data_640x480_black_ = CreateTestYuvImage(gfx::Size(640, 480), true);
  image_data_640x368_black_ = CreateTestYuvImage(gfx::Size(640, 368), true);
  image_data_640x360_black_ = CreateTestYuvImage(gfx::Size(640, 360), true);

  // |user_yuv_files_| may include many files and use ';' as delimiter.
  std::vector<base::FilePath::StringType> files =
      base::SplitString(user_yuv_files_, base::FilePath::StringType(1, ';'),
                        base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
  for (const auto& file : files) {
    std::vector<base::FilePath::StringType> filename_and_size =
        base::SplitString(file, base::FilePath::StringType(1, ':'),
                          base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
    ASSERT_EQ(2u, filename_and_size.size());
    base::FilePath::StringType filename(filename_and_size[0]);

    std::vector<base::FilePath::StringType> image_resolution =
        base::SplitString(filename_and_size[1],
                          base::FilePath::StringType(1, 'x'),
                          base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
    ASSERT_EQ(2u, image_resolution.size());
    int width = 0, height = 0;
    ASSERT_TRUE(base::StringToInt(image_resolution[0], &width));
    ASSERT_TRUE(base::StringToInt(image_resolution[1], &height));

    gfx::Size image_size(width, height);
    ASSERT_TRUE(!image_size.IsEmpty());

    base::FilePath input_file = GetOriginalOrTestDataFilePath(filename);
    auto image_data = ReadTestYuvImage(input_file, image_size);
    image_data_user_.push_back(std::move(image_data));
  }
}

void JpegEncodeAcceleratorTestEnvironment::TearDown() {
  log_file_.reset();
}

void JpegEncodeAcceleratorTestEnvironment::LogToFile(const std::string& key,
                                                     const std::string& value) {
  std::string s = base::StringPrintf("%s: %s\n", key.c_str(), value.c_str());
  LOG(INFO) << s;
  if (log_file_) {
    log_file_->WriteAtCurrentPos(s.data(), static_cast<int>(s.length()));
  }
}

std::unique_ptr<TestImage>
JpegEncodeAcceleratorTestEnvironment::CreateTestYuvImage(
    const gfx::Size& image_size,
    bool is_black) {
  const size_t num_pixels = image_size.width() * image_size.height();
  std::vector<uint8_t> image_data(num_pixels * 3 / 2);

  // Fill in Y values.
  std::fill(image_data.begin(), image_data.begin() + num_pixels,
            is_black ? 0 : 255);
  // Fill in U and V values.
  std::fill(image_data.begin() + num_pixels, image_data.end(), 128);

  base::FilePath output_filename(base::NumberToString(image_size.width()) +
                                 "x" +
                                 base::NumberToString(image_size.height()) +
                                 (is_black ? "_black.jpg" : "_white.jpg"));
  return std::make_unique<TestImage>(std::move(image_data), image_size,
                                     output_filename);
}

std::unique_ptr<TestImage>
JpegEncodeAcceleratorTestEnvironment::ReadTestYuvImage(
    const base::FilePath& input_file,
    const gfx::Size& image_size) {
  int64_t file_size = 0;
  LOG_ASSERT(GetFileSize(input_file, &file_size));
  std::vector<uint8_t> image_data(file_size);
  LOG_ASSERT(ReadFile(input_file, reinterpret_cast<char*>(image_data.data()),
                      file_size) == file_size);

  base::FilePath output_filename = input_file.AddExtension(".jpg");
  return std::make_unique<TestImage>(std::move(image_data), image_size,
                                     output_filename);
}

base::FilePath
JpegEncodeAcceleratorTestEnvironment::GetOriginalOrTestDataFilePath(
    const std::string& name) {
  base::FilePath file_path = base::FilePath(name);
  if (!PathExists(file_path)) {
    file_path = media::GetTestDataFilePath(name);
  }
  VLOG(3) << "Using file path " << file_path.value();
  return file_path;
}

class JpegClient : public JpegEncodeAccelerator::Client {
 public:
  JpegClient(const std::vector<TestImage*>& test_aligned_images,
             const std::vector<TestImage*>& test_images,
             media::test::ClientStateNotification<ClientState>* note,
             size_t exif_size);

  JpegClient(const JpegClient&) = delete;
  JpegClient& operator=(const JpegClient&) = delete;

  ~JpegClient() override;
  void CreateJpegEncoder();
  void DestroyJpegEncoder();
  void StartEncode(int32_t bitstream_buffer_id);
  void StartEncodeDmaBuf(int32_t bitstream_buffer_id);

  // JpegEncodeAccelerator::Client implementation.
  void VideoFrameReady(int32_t buffer_id, size_t encoded_picture_size) override;
  void NotifyError(int32_t buffer_id,
                   JpegEncodeAccelerator::Status status) override;

 private:
  // Get the related test image file.
  TestImage* GetTestImage(int32_t bitstream_buffer_id);
  void PrepareMemory(int32_t bitstream_buffer_id);
  void SetState(ClientState new_state);
  void OnInitialize(
      chromeos_camera::JpegEncodeAccelerator::Status initialize_result);
  void SaveToFile(TestImage* test_image, size_t hw_size, size_t sw_size);
  bool CompareHardwareAndSoftwareResults(int width,
                                         int height,
                                         size_t hw_encoded_size,
                                         size_t sw_encoded_size);

  // Calculate mean absolute difference of hardware and software encode results
  // for verifying the similarity.
  double GetMeanAbsoluteDifference(uint8_t* hw_yuv_result,
                                   uint8_t* sw_yuv_result,
                                   size_t yuv_size);

  // Generate software encode result and populate it into |sw_out_shm_|.
  bool GetSoftwareEncodeResult(int width,
                               int height,
                               size_t* sw_encoded_size,
                               base::TimeDelta* sw_encode_time);

  // JpegClient doesn't own |test_aligned_images_|.
  // The resolutions of these images are all aligned. HW Accelerator must
  // support them.
  const std::vector<TestImage*>& test_aligned_images_;

  // JpegClient doesn't own |test_unaligned_images_|.
  // The resolutions of these images may be unaligned.
  const std::vector<TestImage*>& test_unaligned_images_;

  // A map that stores HW encoding start timestamp for each output buffer id.
  std::map<int, base::TimeTicks> buffer_id_to_start_time_;

  std::unique_ptr<JpegEncodeAccelerator> encoder_;
  ClientState state_;

  // Used to notify another thread about the state. JpegClient does not own
  // this.
  media::test::ClientStateNotification<ClientState>* note_;

  // EXIF data size for testing.
  size_t exif_size_;
  // Input buffer for EXIF data.
  media::BitstreamBuffer exif_buffer_;
  // Output buffer prepared for JpegEncodeAccelerator.
  media::BitstreamBuffer encoded_buffer_;

  // Mapped memory of input file.
  std::unique_ptr<base::MappedReadOnlyRegion> in_shm_;
  // Mapped memory of output buffer from hardware encoder.
  base::UnsafeSharedMemoryRegion hw_out_shm_;
  base::WritableSharedMemoryMapping hw_out_mapping_;
  // Mapped memory of output buffer from software encoder.
  base::UnsafeSharedMemoryRegion sw_out_shm_;
  base::WritableSharedMemoryMapping sw_out_mapping_;
  // Output for DMA-buf based encoding.
  scoped_refptr<media::VideoFrame> hw_out_frame_;

  // Used to create Gpu memory buffer for DMA-buf encoding tests.
  std::unique_ptr<gpu::GpuMemoryBufferManager> gpu_memory_buffer_manager_;

  base::WeakPtrFactory<JpegClient> weak_factory_{this};
};

JpegClient::JpegClient(const std::vector<TestImage*>& test_aligned_images,
                       const std::vector<TestImage*>& test_images,
                       media::test::ClientStateNotification<ClientState>* note,
                       size_t exif_size)
    : test_aligned_images_(test_aligned_images),
      test_unaligned_images_(test_images),
      state_(ClientState::CREATED),
      note_(note),
      exif_size_(exif_size),
      gpu_memory_buffer_manager_(new media::LocalGpuMemoryBufferManager()) {}

JpegClient::~JpegClient() {}

void JpegClient::CreateJpegEncoder() {
  auto jea_factories =
      GpuJpegEncodeAcceleratorFactory::GetAcceleratorFactories();
  if (jea_factories.size() == 0) {
    LOG(ERROR) << "JpegEncodeAccelerator is not supported on this platform.";
    SetState(ClientState::ERROR);
    return;
  }

  for (const auto& create_jea_func : jea_factories) {
    encoder_ =
        create_jea_func.Run(base::SingleThreadTaskRunner::GetCurrentDefault());
    if (encoder_)
      break;
  }

  if (!encoder_) {
    LOG(ERROR) << "Failed to create JpegEncodeAccelerator.";
    SetState(ClientState::ERROR);
    return;
  }
  encoder_->InitializeAsync(this, base::BindOnce(&JpegClient::OnInitialize,
                                                 weak_factory_.GetWeakPtr()));
}

void JpegClient::OnInitialize(
    chromeos_camera::JpegEncodeAccelerator::Status initialize_result) {
  if (initialize_result ==
      ::chromeos_camera::JpegEncodeAccelerator::ENCODE_OK) {
    SetState(ClientState::INITIALIZED);
    return;
  }

  LOG(ERROR) << "JpegEncodeAccelerator::InitializeAsync() failed";
  SetState(ClientState::ERROR);
}

void JpegClient::DestroyJpegEncoder() {
  encoder_.reset();
}

void JpegClient::VideoFrameReady(int32_t buffer_id, size_t hw_encoded_size) {
  base::TimeTicks hw_encode_end = base::TimeTicks::Now();
  base::TimeDelta elapsed_hw =
      hw_encode_end - buffer_id_to_start_time_[buffer_id];

  TestImage* test_image;
  if (buffer_id < static_cast<int32_t>(test_aligned_images_.size())) {
    test_image = test_aligned_images_[buffer_id];
  } else {
    test_image =
        test_unaligned_images_[buffer_id - test_aligned_images_.size()];
  }

  if (hw_out_frame_ && !hw_out_frame_->IsMappable()) {
    // |hw_out_frame_| should only be mapped once.
    auto mapper =
        media::GenericDmaBufVideoFrameMapper::Create(hw_out_frame_->format());
    hw_out_frame_ = mapper->Map(hw_out_frame_, PROT_READ | PROT_WRITE);
  }

  size_t sw_encoded_size = 0;
  base::TimeDelta elapsed_sw;
  LOG_ASSERT(GetSoftwareEncodeResult(test_image->visible_size.width(),
                                     test_image->visible_size.height(),
                                     &sw_encoded_size, &elapsed_sw));

  g_env->LogToFile("hw_encode_time",
                   base::NumberToString(elapsed_hw.InMicroseconds()));
  g_env->LogToFile("sw_encode_time",
                   base::NumberToString(elapsed_sw.InMicroseconds()));

  if (g_save_to_file) {
    SaveToFile(test_image, hw_encoded_size, sw_encoded_size);
  }

  if (!CompareHardwareAndSoftwareResults(test_image->visible_size.width(),
                                         test_image->visible_size.height(),
                                         hw_encoded_size, sw_encoded_size)) {
    SetState(ClientState::ERROR);
  } else {
    SetState(ClientState::ENCODE_PASS);
  }

  encoded_buffer_ = {};
}

bool JpegClient::GetSoftwareEncodeResult(int width,
                                         int height,
                                         size_t* sw_encoded_size,
                                         base::TimeDelta* sw_encode_time) {
  base::TimeTicks sw_encode_start = base::TimeTicks::Now();
  int y_stride = width;
  int u_stride = width / 2;
  int v_stride = u_stride;
  const uint8_t* yuv_src = static_cast<uint8_t*>(in_shm_->mapping.memory());
  const int kBytesPerPixel = 4;
  std::vector<uint8_t> rgba_buffer(width * height * kBytesPerPixel);
  std::vector<uint8_t> encoded;
  libyuv::I420ToABGR(yuv_src, y_stride, yuv_src + y_stride * height, u_stride,
                     yuv_src + y_stride * height + u_stride * height / 2,
                     v_stride, rgba_buffer.data(), width * kBytesPerPixel,
                     width, height);

  SkImageInfo info = SkImageInfo::Make(width, height, kRGBA_8888_SkColorType,
                                       kOpaque_SkAlphaType);
  SkPixmap src(info, &rgba_buffer[0], width * kBytesPerPixel);
  if (!gfx::JPEGCodec::Encode(src, kJpegDefaultQuality, &encoded)) {
    return false;
  }

  memcpy(sw_out_mapping_.memory(), encoded.data(), encoded.size());
  *sw_encoded_size = encoded.size();
  *sw_encode_time = base::TimeTicks::Now() - sw_encode_start;
  return true;
}

bool JpegClient::CompareHardwareAndSoftwareResults(int width,
                                                   int height,
                                                   size_t hw_encoded_size,
                                                   size_t sw_encoded_size) {
  size_t yuv_size = width * height * 3 / 2;
  uint8_t* hw_yuv_result = new uint8_t[yuv_size];
  int y_stride = width;
  int u_stride = width / 2;
  int v_stride = u_stride;

  const uint8_t* out_mem = static_cast<const uint8_t*>(
      hw_out_frame_ ? hw_out_frame_->data(0) : hw_out_mapping_.memory());
  if (libyuv::ConvertToI420(
          out_mem, hw_encoded_size, hw_yuv_result, y_stride,
          hw_yuv_result + y_stride * height, u_stride,
          hw_yuv_result + y_stride * height + u_stride * height / 2, v_stride,
          0, 0, width, height, width, height, libyuv::kRotate0,
          libyuv::FOURCC_MJPG)) {
    LOG(ERROR) << "Convert HW encoded result to YUV failed";
  }

  uint8_t* sw_yuv_result = new uint8_t[yuv_size];
  if (libyuv::ConvertToI420(
          static_cast<const uint8_t*>(sw_out_mapping_.memory()),
          sw_encoded_size, sw_yuv_result, y_stride,
          sw_yuv_result + y_stride * height, u_stride,
          sw_yuv_result + y_stride * height + u_stride * height / 2, v_stride,
          0, 0, width, height, width, height, libyuv::kRotate0,
          libyuv::FOURCC_MJPG)) {
    LOG(ERROR) << "Convert SW encoded result to YUV failed";
  }

  double difference =
      GetMeanAbsoluteDifference(hw_yuv_result, sw_yuv_result, yuv_size);
  delete[] hw_yuv_result;
  delete[] sw_yuv_result;

  if (difference > kMeanDiffThreshold) {
    LOG(ERROR) << "HW and SW encode results are not similar enough. diff = "
               << difference;
    return false;
  } else {
    return true;
  }
}

double JpegClient::GetMeanAbsoluteDifference(uint8_t* hw_yuv_result,
                                             uint8_t* sw_yuv_result,
                                             size_t yuv_size) {
  double total_difference = 0;
  for (size_t i = 0; i < yuv_size; i++)
    total_difference += std::abs(hw_yuv_result[i] - sw_yuv_result[i]);
  return total_difference / yuv_size;
}

void JpegClient::NotifyError(int32_t buffer_id,
                             JpegEncodeAccelerator::Status status) {
  LOG(ERROR) << "Notifying of error " << status << " for output buffer id "
             << buffer_id;
  SetState(ClientState::ERROR);
  encoded_buffer_ = {};
}

TestImage* JpegClient::GetTestImage(int32_t bitstream_buffer_id) {
  DCHECK_LT(static_cast<size_t>(bitstream_buffer_id),
            test_aligned_images_.size() + test_unaligned_images_.size());
  TestImage* image_file;
  if (bitstream_buffer_id < static_cast<int32_t>(test_aligned_images_.size())) {
    image_file = test_aligned_images_[bitstream_buffer_id];
  } else {
    image_file = test_unaligned_images_[bitstream_buffer_id -
                                        test_aligned_images_.size()];
  }

  return image_file;
}

void JpegClient::PrepareMemory(int32_t bitstream_buffer_id) {
  TestImage* test_image = GetTestImage(bitstream_buffer_id);

  if (exif_size_ > 0) {
    auto shm = base::UnsafeSharedMemoryRegion::Create(exif_size_);
    auto shm_mapping = shm.Map();
    base::RandBytes(shm_mapping.GetMemoryAsSpan<uint8_t>());
    exif_buffer_ =
        media::BitstreamBuffer(bitstream_buffer_id, std::move(shm), exif_size_);
  }

  size_t input_size = test_image->image_data.size();
  if (!in_shm_ || input_size > in_shm_->mapping.size()) {
    in_shm_ = std::make_unique<base::MappedReadOnlyRegion>(
        base::ReadOnlySharedMemoryRegion::Create(input_size));
    LOG_ASSERT(in_shm_->IsValid());
  }
  memcpy(in_shm_->mapping.memory(), test_image->image_data.data(), input_size);

  if (!hw_out_shm_.IsValid() || !hw_out_mapping_.IsValid() ||
      test_image->output_size > hw_out_mapping_.size()) {
    hw_out_shm_ =
        base::UnsafeSharedMemoryRegion::Create(test_image->output_size);
    LOG_ASSERT(hw_out_shm_.IsValid());
    hw_out_mapping_ = hw_out_shm_.Map();
    LOG_ASSERT(hw_out_mapping_.IsValid());
  }
  memset(hw_out_mapping_.memory(), 0, test_image->output_size);

  if (!sw_out_shm_.IsValid() || !sw_out_mapping_.IsValid() ||
      test_image->output_size > sw_out_mapping_.size()) {
    sw_out_shm_ =
        base::UnsafeSharedMemoryRegion::Create(test_image->output_size);
    LOG_ASSERT(sw_out_shm_.IsValid());
    sw_out_mapping_ = sw_out_shm_.Map();
    LOG_ASSERT(sw_out_mapping_.IsValid());
  }
  memset(sw_out_mapping_.memory(), 0, test_image->output_size);

  hw_out_frame_ = nullptr;
}

void JpegClient::SetState(ClientState new_state) {
  DVLOG(2) << "Changing state "
           << static_cast<std::underlying_type<ClientState>::type>(state_)
           << "->"
           << static_cast<std::underlying_type<ClientState>::type>(new_state);
  note_->Notify(new_state);
  state_ = new_state;
}

void JpegClient::SaveToFile(TestImage* test_image,
                            size_t hw_size,
                            size_t sw_size) {
  DCHECK_NE(nullptr, test_image);

  base::FilePath out_filename_hw = test_image->output_filename;
  LOG(INFO) << "Writing HW encode results to "
            << out_filename_hw.MaybeAsASCII();

  ASSERT_TRUE(base::WriteFile(
      out_filename_hw,
      base::make_span(hw_out_frame_
                          ? hw_out_frame_->data(0)
                          : static_cast<uint8_t*>(hw_out_mapping_.memory()),
                      hw_size)));

  base::FilePath out_filename_sw = out_filename_hw.InsertBeforeExtension("_sw");
  LOG(INFO) << "Writing SW encode results to "
            << out_filename_sw.MaybeAsASCII();
  ASSERT_TRUE(base::WriteFile(
      out_filename_sw,
      base::make_span(static_cast<uint8_t*>(sw_out_mapping_.memory()),
                      sw_size)));
}

void JpegClient::StartEncode(int32_t bitstream_buffer_id) {
  TestImage* test_image = GetTestImage(bitstream_buffer_id);

  test_image->output_size =
      encoder_->GetMaxCodedBufferSize(test_image->visible_size);
  PrepareMemory(bitstream_buffer_id);

  encoded_buffer_ = media::BitstreamBuffer(
      bitstream_buffer_id, std::move(hw_out_shm_), test_image->output_size);
  scoped_refptr<media::VideoFrame> input_frame_ =
      media::VideoFrame::WrapExternalData(
          media::PIXEL_FORMAT_I420, test_image->visible_size,
          gfx::Rect(test_image->visible_size), test_image->visible_size,
          static_cast<uint8_t*>(in_shm_->mapping.memory()),
          test_image->image_data.size(), base::TimeDelta());
  LOG_ASSERT(input_frame_.get());
  input_frame_->BackWithSharedMemory(&in_shm_->region);

  buffer_id_to_start_time_[bitstream_buffer_id] = base::TimeTicks::Now();
  encoder_->Encode(input_frame_, kJpegDefaultQuality,
                   exif_size_ > 0 ? &exif_buffer_ : nullptr,
                   std::move(encoded_buffer_));
}

void JpegClient::StartEncodeDmaBuf(int32_t bitstream_buffer_id) {
  TestImage* test_image = GetTestImage(bitstream_buffer_id);
  test_image->output_size =
      encoder_->GetMaxCodedBufferSize(test_image->visible_size);
  PrepareMemory(bitstream_buffer_id);

  auto input_buffer = gpu_memory_buffer_manager_->CreateGpuMemoryBuffer(
      test_image->visible_size, gfx::BufferFormat::YUV_420_BIPLANAR,
      gfx::BufferUsage::VEA_READ_CAMERA_AND_CPU_READ_WRITE,
      gpu::kNullSurfaceHandle, nullptr);
  ASSERT_EQ(input_buffer->Map(), true);

  uint8_t* plane_buf[2] = {static_cast<uint8_t*>(input_buffer->memory(0)),
                           static_cast<uint8_t*>(input_buffer->memory(1))};
  uint8_t* src = test_image->image_data.data();
  int width = test_image->visible_size.width();
  int height = test_image->visible_size.height();

  libyuv::I420ToNV12(src, width, src + width * height, width / 2,
                     src + width * height * 5 / 4, width / 2, plane_buf[0],
                     input_buffer->stride(0), plane_buf[1],
                     input_buffer->stride(1), width, height);

  auto input_frame = GetVideoFrameFromGpuMemoryBuffer(
      input_buffer.get(), test_image->visible_size, media::PIXEL_FORMAT_NV12);
  LOG_ASSERT(input_frame.get());

  auto output_buffer = gpu_memory_buffer_manager_->CreateGpuMemoryBuffer(
      gfx::Size(kJpegMaxSize, 1), gfx::BufferFormat::R_8,
      gfx::BufferUsage::CAMERA_AND_CPU_READ_WRITE, gpu::kNullSurfaceHandle,
      nullptr);
  ASSERT_EQ(output_buffer->Map(), true);
  hw_out_frame_ = GetVideoFrameFromGpuMemoryBuffer(
      output_buffer.get(), test_image->visible_size, media::PIXEL_FORMAT_MJPEG);
  LOG_ASSERT(hw_out_frame_.get());

  buffer_id_to_start_time_[bitstream_buffer_id] = base::TimeTicks::Now();
  encoder_->EncodeWithDmaBuf(input_frame, hw_out_frame_, kJpegDefaultQuality,
                             bitstream_buffer_id,
                             exif_size_ > 0 ? &exif_buffer_ : nullptr);
}

class JpegEncodeAcceleratorTest : public ::testing::Test {
 public:
  JpegEncodeAcceleratorTest(const JpegEncodeAcceleratorTest&) = delete;
  JpegEncodeAcceleratorTest& operator=(const JpegEncodeAcceleratorTest&) =
      delete;

 protected:
  JpegEncodeAcceleratorTest() {}

  void TestEncode(size_t num_concurrent_encoders,
                  bool is_dma,
                  size_t exif_size);

  // This is needed to allow the usage of methods in post_task.h in
  // JpegEncodeAccelerator implementations.
  base::test::TaskEnvironment task_environment_;

  // The elements of |test_aligned_images_| and |test_unaligned_images_| are
  // owned by JpegEncodeAcceleratorTestEnvironment.
  std::vector<TestImage*> test_aligned_images_;
  std::vector<TestImage*> test_unaligned_images_;
};

void JpegEncodeAcceleratorTest::TestEncode(size_t num_concurrent_encoders,
                                           bool is_dma,
                                           size_t exif_size) {
  base::Thread encoder_thread("EncoderThread");
  ASSERT_TRUE(encoder_thread.Start());

  std::vector<
      std::unique_ptr<media::test::ClientStateNotification<ClientState>>>
      notes;
  std::vector<std::unique_ptr<JpegClient>> clients;
  std::vector<ClientState> results;

  for (size_t i = 0; i < num_concurrent_encoders; i++) {
    notes.push_back(
        std::make_unique<media::test::ClientStateNotification<ClientState>>());
    clients.push_back(std::make_unique<JpegClient>(
        test_aligned_images_, test_unaligned_images_, notes.back().get(),
        exif_size));
    encoder_thread.task_runner()->PostTask(
        FROM_HERE, base::BindOnce(&JpegClient::CreateJpegEncoder,
                                  base::Unretained(clients.back().get())));
    results.push_back(notes[i]->Wait());
  }

  for (size_t i = 0; i < num_concurrent_encoders; i++) {
    ASSERT_EQ(results[i], ClientState::INITIALIZED);
  }

  for (size_t index = 0; index < test_aligned_images_.size(); index++) {
    VLOG(3) << index
            << ",width:" << test_aligned_images_[index]->visible_size.width();
    VLOG(3) << index
            << ",height:" << test_aligned_images_[index]->visible_size.height();
    if (!is_dma) {
      for (size_t i = 0; i < num_concurrent_encoders; i++) {
        encoder_thread.task_runner()->PostTask(
            FROM_HERE,
            base::BindOnce(&JpegClient::StartEncode,
                           base::Unretained(clients[i].get()), index));
      }
    } else {
      for (size_t i = 0; i < num_concurrent_encoders; i++) {
        encoder_thread.task_runner()->PostTask(
            FROM_HERE,
            base::BindOnce(&JpegClient::StartEncodeDmaBuf,
                           base::Unretained(clients[i].get()), index));
      }
    }
    for (size_t i = 0; i < num_concurrent_encoders; i++) {
      results[i] = notes[i]->Wait();
    }

    for (size_t i = 0; i < num_concurrent_encoders; i++) {
      ASSERT_EQ(results[i], ClientState::ENCODE_PASS);
    }
  }

#if BUILDFLAG(USE_V4L2_CODEC) && defined(ARCH_CPU_ARM_FAMILY)
  // For unaligned images, V4L2 may not be able to encode them so skip for V4L2
  // cases.
#else
  for (size_t index = 0; index < test_unaligned_images_.size(); index++) {
    int buffer_id = index + test_aligned_images_.size();
    VLOG(3) << buffer_id
            << ",width:" << test_unaligned_images_[index]->visible_size.width();
    VLOG(3) << buffer_id << ",height:"
            << test_unaligned_images_[index]->visible_size.height();

    if (!is_dma) {
      for (size_t i = 0; i < num_concurrent_encoders; i++) {
        encoder_thread.task_runner()->PostTask(
            FROM_HERE,
            base::BindOnce(&JpegClient::StartEncode,
                           base::Unretained(clients[i].get()), buffer_id));
      }
    } else {
      for (size_t i = 0; i < num_concurrent_encoders; i++) {
        encoder_thread.task_runner()->PostTask(
            FROM_HERE,
            base::BindOnce(&JpegClient::StartEncodeDmaBuf,
                           base::Unretained(clients[i].get()), buffer_id));
      }
    }
    for (size_t i = 0; i < num_concurrent_encoders; i++) {
      results[i] = notes[i]->Wait();
    }

    for (size_t i = 0; i < num_concurrent_encoders; i++) {
      ASSERT_EQ(results[i], ClientState::ENCODE_PASS);
    }
  }
#endif

  for (size_t i = 0; i < num_concurrent_encoders; i++) {
    encoder_thread.task_runner()->PostTask(
        FROM_HERE, base::BindOnce(&JpegClient::DestroyJpegEncoder,
                                  base::Unretained(clients[i].get())));
  }
  auto destroy_clients_task = base::BindOnce(
      [](std::vector<std::unique_ptr<JpegClient>> clients) { clients.clear(); },
      std::move(clients));
  encoder_thread.task_runner()->PostTask(FROM_HERE,
                                         std::move(destroy_clients_task));
  encoder_thread.Stop();
  VLOG(1) << "Exit TestEncode";
}

// We may need to keep the VAAPI shared memory path for Linux-based Chrome VCD.
// Some of our older boards are still running on the Linux VCD.
#if BUILDFLAG(USE_VAAPI)
TEST_F(JpegEncodeAcceleratorTest, SimpleEncode) {
  for (size_t i = 0; i < g_env->repeat_; i++) {
    for (auto& image : g_env->image_data_user_) {
      test_aligned_images_.push_back(image.get());
    }
  }
  TestEncode(/*num_concurrent_encoders=*/1u, /*is_dma=*/false,
             /*exif_size=*/0u);
}

TEST_F(JpegEncodeAcceleratorTest, MultipleEncoders) {
  for (auto& image : g_env->image_data_user_) {
    test_aligned_images_.push_back(image.get());
  }
  TestEncode(/*num_concurrent_encoders=*/3u, /*is_dma=*/false,
             /*exif_size=*/0u);
}

TEST_F(JpegEncodeAcceleratorTest, ResolutionChange) {
  test_aligned_images_.push_back(g_env->image_data_640x368_black_.get());
  test_aligned_images_.push_back(g_env->image_data_1280x720_white_.get());
  TestEncode(/*num_concurrent_encoders=*/1u, /*is_dma=*/false,
             /*exif_size=*/0u);
}

TEST_F(JpegEncodeAcceleratorTest, AlignedSizes) {
  test_aligned_images_.push_back(g_env->image_data_2560x1920_white_.get());
  test_aligned_images_.push_back(g_env->image_data_1280x720_white_.get());
  test_aligned_images_.push_back(g_env->image_data_640x480_black_.get());
  TestEncode(/*num_concurrent_encoders=*/1u, /*is_dma=*/false,
             /*exif_size=*/0u);
}

TEST_F(JpegEncodeAcceleratorTest, CodedSizeAlignment) {
  test_unaligned_images_.push_back(g_env->image_data_640x360_black_.get());
  TestEncode(/*num_concurrent_encoders=*/1u, /*is_dma=*/false,
             /*exif_size=*/0u);
}
#endif

TEST_F(JpegEncodeAcceleratorTest, SimpleDmaEncode) {
  for (size_t i = 0; i < g_env->repeat_; i++) {
    for (auto& image : g_env->image_data_user_) {
      test_aligned_images_.push_back(image.get());
    }
  }
  TestEncode(/*num_concurrent_encoders=*/1u, /*is_dma=*/true, /*exif_size=*/0u);
}

TEST_F(JpegEncodeAcceleratorTest, MultipleDmaEncoders) {
  for (auto& image : g_env->image_data_user_) {
    test_aligned_images_.push_back(image.get());
  }
  TestEncode(/*num_concurrent_encoders=*/3u, /*is_dma=*/true, /*exif_size=*/0u);
}

TEST_F(JpegEncodeAcceleratorTest, ResolutionChangeDma) {
  test_aligned_images_.push_back(g_env->image_data_640x368_black_.get());
  test_aligned_images_.push_back(g_env->image_data_1280x720_white_.get());
  TestEncode(/*num_concurrent_encoders=*/1u, /*is_dma=*/true, /*exif_size=*/0u);
}

TEST_F(JpegEncodeAcceleratorTest, AlignedSizesDma) {
  test_aligned_images_.push_back(g_env->image_data_2560x1920_white_.get());
  test_aligned_images_.push_back(g_env->image_data_1280x720_white_.get());
  test_aligned_images_.push_back(g_env->image_data_640x480_black_.get());
  TestEncode(/*num_concurrent_encoders=*/1u, /*is_dma=*/true, /*exif_size=*/0u);
}

TEST_F(JpegEncodeAcceleratorTest, CodedSizeAlignmentDma) {
  test_unaligned_images_.push_back(g_env->image_data_640x360_black_.get());
  TestEncode(/*num_concurrent_encoders=*/1u, /*is_dma=*/true, /*exif_size=*/0u);
}

TEST_F(JpegEncodeAcceleratorTest, ExifSizesDma) {
  for (size_t i = 0; i < g_env->repeat_; i++) {
    for (auto& image : g_env->image_data_user_) {
      test_aligned_images_.push_back(image.get());
    }
  }
  // Intel iHD driver is known to fail when |exif_size| % 1020 == 411, so we
  // sample more around these numbers.
  constexpr size_t kTestExifSizes[] = {
      8000u,  8571u,  9000u,  9591u,  10000u,
      10609u, 10610u, 10611u, 10612u, 11111u,
  };
  for (size_t exif_size : kTestExifSizes) {
    TestEncode(/*num_concurrent_encoders=*/1u, /*is_dma=*/true, exif_size);
  }
}

}  // namespace
}  // namespace chromeos_camera

int main(int argc, char** argv) {
  testing::InitGoogleTest(&argc, argv);
  base::CommandLine::Init(argc, argv);
  mojo::core::Init();
  base::ShadowingAtExitManager at_exit_manager;

  // Needed to enable DVLOG through --vmodule.
  logging::LoggingSettings settings;
  settings.logging_dest = logging::LOG_TO_SYSTEM_DEBUG_LOG;
  LOG_ASSERT(logging::InitLogging(settings));

  const base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
  DCHECK(cmd_line);

  const base::FilePath::CharType* yuv_filenames = nullptr;
  base::FilePath log_path;
  size_t repeat = 1;
  base::CommandLine::SwitchMap switches = cmd_line->GetSwitches();
  for (base::CommandLine::SwitchMap::const_iterator it = switches.begin();
       it != switches.end(); ++it) {
    // yuv_filenames can include one or many files and use ';' as delimiter.
    // For each file, it should follow the format "[filename]:[width]x[height]".
    // For example, "lake.yuv:4160x3120".
    if (it->first == "yuv_filenames") {
      yuv_filenames = it->second.c_str();
      continue;
    }
    if (it->first == "output_log") {
      log_path = base::FilePath(
          base::FilePath::StringType(it->second.begin(), it->second.end()));
      continue;
    }
    if (it->first == "repeat") {
      if (!base::StringToSizeT(it->second, &repeat)) {
        LOG(INFO) << "Can't parse parameter |repeat|: " << it->second;
        repeat = 1;
      }
      continue;
    }
    if (it->first == "save_to_file") {
      chromeos_camera::g_save_to_file = true;
      continue;
    }
    if (it->first == "v" || it->first == "vmodule")
      continue;
    if (it->first == "h" || it->first == "help")
      continue;
    LOG(ERROR) << "Unexpected switch: " << it->first << ":" << it->second;
    return -EINVAL;
  }
#if BUILDFLAG(USE_VAAPI)
  media::VaapiWrapper::PreSandboxInitialization();
#endif

  chromeos_camera::g_env =
      reinterpret_cast<chromeos_camera::JpegEncodeAcceleratorTestEnvironment*>(
          testing::AddGlobalTestEnvironment(
              new chromeos_camera::JpegEncodeAcceleratorTestEnvironment(
                  yuv_filenames, log_path, repeat)));

  return RUN_ALL_TESTS();
}