chromium/media/gpu/v4l2/v4l2_unittest.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.

// gtest.h has to be included first.
// See http://code.google.com/p/googletest/issues/detail?id=371
#include "testing/gtest/include/gtest/gtest.h"

#include "base/bits.h"
#include "base/containers/contains.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/memory_mapped_file.h"
#include "base/test/launcher/unit_test_launcher.h"
#include "base/test/task_environment.h"
#include "base/test/test_suite.h"
#include "media/base/mock_media_log.h"
#include "media/base/test_helpers.h"
#include "media/base/video_codecs.h"
#include "media/gpu/v4l2/v4l2_device.h"
#include "media/gpu/v4l2/v4l2_stateful_video_decoder.h"
#include "media/gpu/v4l2/v4l2_utils.h"
#include "third_party/libdrm/src/include/drm/drm_fourcc.h"
#include "ui/gfx/linux/gbm_defines.h"

#include <drm.h>
#include <fcntl.h>
#include <gbm.h>
#include <string.h>
#include <xf86drm.h>

namespace media {

namespace {

const base::FilePath kDecoderDevicePrefix("/dev/dri/");

#define TOSTR(enumCase) \
  case enumCase:        \
    return #enumCase

struct DrmVersionDeleter {
  void operator()(drmVersion* version) const { drmFreeVersion(version); }
};
using ScopedDrmVersionPtr = std::unique_ptr<drmVersion, DrmVersionDeleter>;

// Converts v4l2 format to gbm format
uint32_t ToGBMFormat(uint32_t v4l2_format) {
  if (v4l2_format == V4L2_PIX_FMT_NV12 || v4l2_format == V4L2_PIX_FMT_NV12M) {
    return DRM_FORMAT_NV12;
  }
  return DRM_FORMAT_INVALID;
}

const char* VideoCodecProfileToString(VideoCodecProfile profile) {
  switch (profile) {
    TOSTR(H264PROFILE_BASELINE);
    TOSTR(H264PROFILE_MAIN);
    TOSTR(H264PROFILE_EXTENDED);
    TOSTR(H264PROFILE_HIGH);
    TOSTR(VP8PROFILE_ANY);
    TOSTR(VP9PROFILE_PROFILE0);
    TOSTR(AV1PROFILE_PROFILE_MAIN);
    default:
      return "profile_not_enumerated";
  }
}

}  // namespace

class V4L2MinigbmTest
    : public testing::TestWithParam<std::tuple<VideoCodecProfile, gfx::Size>> {
 public:
  V4L2MinigbmTest() = default;
  ~V4L2MinigbmTest() = default;

  struct PrintToStringParamName {
    template <class ParamType>
    std::string operator()(
        const testing::TestParamInfo<ParamType>& info) const {
      return base::StringPrintf(
          "%s__%s", VideoCodecProfileToString(std::get<0>(info.param)),
          std::get<1>(info.param).ToString().c_str());
    }
  };
};

void TestStatefulDecoderAllocations(uint32_t codec_fourcc,
                                    scoped_refptr<V4L2Device> device,
                                    uint32_t chosen_v4l2_pixel_format,
                                    gfx::Size resolution) {
  scoped_refptr<V4L2Queue> OUTPUT_queue =
      device->GetQueue(V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE);
  ASSERT_NE(OUTPUT_queue.get(), nullptr);
  const std::optional<struct v4l2_format> input_v4l2_format =
      OUTPUT_queue->SetFormat(codec_fourcc, gfx::Size(), /*buffer_size=*/1E6);
  ASSERT_TRUE(input_v4l2_format.has_value());

  // Checks that pixel format is set properly as denoted in section 4.5.1.5
  // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/dev-decoder.html#initialization.
  ASSERT_EQ(codec_fourcc, input_v4l2_format.value().fmt.pix_mp.pixelformat)
      << "The driver should never changed the codec :)";
  LOG(INFO) << " Chosen codec: " << FourccToString(codec_fourcc);
  constexpr size_t kNumInputBuffers = 8;
  ASSERT_NE(
      OUTPUT_queue->AllocateBuffers(kNumInputBuffers, V4L2_MEMORY_MMAP, false),
      0u);

  scoped_refptr<V4L2Queue> CAPTURE_queue =
      device->GetQueue(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE);
  ASSERT_NE(CAPTURE_queue.get(), nullptr);
  std::optional<struct v4l2_format> output_v4l2_format =
      CAPTURE_queue->SetFormat(chosen_v4l2_pixel_format, resolution,
                               /*buffer_size=*/0);
  ASSERT_TRUE(output_v4l2_format.has_value());
  const gfx::Size coded_size(output_v4l2_format->fmt.pix_mp.width,
                             output_v4l2_format->fmt.pix_mp.height);
  LOG_IF(INFO, resolution != coded_size)
      << "Device adjusted " << resolution.ToString() << " to "
      << coded_size.ToString();
  constexpr size_t kNumOutputBuffers = VIDEO_MAX_FRAME;
  ASSERT_NE(CAPTURE_queue->AllocateBuffers(kNumOutputBuffers, V4L2_MEMORY_MMAP,
                                           false),
            0u);

  // Determines proper device driver to use for setting up GBM.
  base::FilePath drm_path;
  base::FileEnumerator fe(kDecoderDevicePrefix, true,
                          base::FileEnumerator::FILES);

  for (base::FilePath name = fe.Next(); !name.empty(); name = fe.Next()) {
    base::File fd(name, base::File::FLAG_OPEN | base::File::FLAG_READ);
    ScopedDrmVersionPtr version(drmGetVersion(fd.GetPlatformFile()));

    // VGEM in the version name describes a virtual driver which
    // is not what is desired for the tests.
    if (strncmp(version->name, "vgem", 4)) {
      drm_path = name;
      break;
    }
  }

  ASSERT_TRUE(!drm_path.empty());
  base::File drm_fd(drm_path, base::File::FLAG_OPEN | base::File::FLAG_READ |
                                  base::File::FLAG_WRITE);
  ASSERT_TRUE(drm_fd.IsValid());
  struct gbm_device* gbm = gbm_create_device(drm_fd.GetPlatformFile());
  ASSERT_TRUE(gbm);

  const auto gbm_format = ToGBMFormat(chosen_v4l2_pixel_format);
  ASSERT_NE(gbm_format, static_cast<uint32_t>(DRM_FORMAT_INVALID));

  struct gbm_bo* bo = gbm_bo_create(
      gbm, coded_size.width(), coded_size.height(), gbm_format,
      GBM_BO_USE_SCANOUT | GBM_BO_USE_TEXTURING | GBM_BO_USE_HW_VIDEO_DECODER);
  ASSERT_TRUE(bo);

  EXPECT_EQ(coded_size,
            gfx::Size(base::checked_cast<int>(gbm_bo_get_width(bo)),
                      base::checked_cast<int>(gbm_bo_get_height(bo))));

  // Minigbm currently rounds up the stride to the nearest multiple of 64
  // while the Compute Strides function only rounds to the nearest multiple
  // of 32. Round device value to nearest multiple of 64 and compare
  // stride values.
  const int bo_num_planes = gbm_bo_get_plane_count(bo);
  std::vector<int32_t> strides =
      VideoFrame::ComputeStrides(PIXEL_FORMAT_NV12, coded_size);
  for (int i = 0; i < bo_num_planes; ++i) {
    uint32_t s = base::bits::AlignUpDeprecatedDoNotUse(strides[i], 64);
    EXPECT_EQ(s, gbm_bo_get_stride_for_plane(bo, i));
  }

  gbm_bo_destroy(bo);
  gbm_device_destroy(gbm);

  ASSERT_TRUE(OUTPUT_queue->Streamoff());
  ASSERT_TRUE(CAPTURE_queue->Streamoff());
  ASSERT_TRUE(OUTPUT_queue->DeallocateBuffers());
  ASSERT_TRUE(CAPTURE_queue->DeallocateBuffers());
}

// This test sets up a v4l2 device for the given video codec profiles,
// and resolution  (as per the test parameters). It then verifies that
// said metadata (e.g. width, height, number of planes, pitch) are the
// same as those we would allocate via minigbm.
TEST_P(V4L2MinigbmTest, AllocateAndCompareWithMinigbm) {
  const auto video_codec_profile = std::get<0>(GetParam());
  const gfx::Size resolution = std::get<1>(GetParam());

  scoped_refptr<V4L2Device> device(new V4L2Device());

  const auto fourcc_stateful =
      VideoCodecProfileToV4L2PixFmt(video_codec_profile, /*slice_based=*/false);
  const bool is_stateful =
      device->Open(V4L2Device::Type::kDecoder, fourcc_stateful);

  constexpr auto kCapsRequired = V4L2_CAP_VIDEO_M2M_MPLANE | V4L2_CAP_STREAMING;
  struct v4l2_capability caps;
  if (device->Ioctl(VIDIOC_QUERYCAP, &caps) ||
      (caps.capabilities & kCapsRequired) != kCapsRequired) {
    GTEST_SKIP() << "Device doesn't support expected capabilities";
  }

  constexpr uint32_t desired_v4l2_pixel_formats[] = {V4L2_PIX_FMT_NV12,
                                                     V4L2_PIX_FMT_NV12M};
  std::vector<uint32_t> supported_v4l2_pixel_formats =
      EnumerateSupportedPixFmts(base::BindRepeating(&V4L2Device::Ioctl, device),
                                V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE);
  int32_t chosen_v4l2_pixel_format = 0;
  for (const auto supported_v4l2_pixel_format : supported_v4l2_pixel_formats) {
    if (base::Contains(desired_v4l2_pixel_formats,
                       supported_v4l2_pixel_format)) {
      chosen_v4l2_pixel_format = supported_v4l2_pixel_format;
      break;
    }
  }
  ASSERT_GT(chosen_v4l2_pixel_format, 0);

  if (is_stateful) {
    TestStatefulDecoderAllocations(fourcc_stateful, device,
                                   chosen_v4l2_pixel_format, resolution);
  }
}

constexpr VideoCodecProfile kVideoCodecProfiles[] = {H264PROFILE_BASELINE};
constexpr gfx::Size kResolutions[] = {gfx::Size(127, 128), gfx::Size(128, 128),
                                      gfx::Size(323, 243), gfx::Size(640, 360),
                                      gfx::Size(1280, 720)};

INSTANTIATE_TEST_SUITE_P(
    ,
    V4L2MinigbmTest,
    ::testing::Combine(::testing::ValuesIn(kVideoCodecProfiles),
                       ::testing::ValuesIn(kResolutions)),
    V4L2MinigbmTest::PrintToStringParamName());

class MockVideoDecoderMixinClient : public VideoDecoderMixin::Client {
 public:
  MockVideoDecoderMixinClient() : weak_ptr_factory_(this) {}

  MOCK_METHOD(DmabufVideoFramePool*, GetVideoFramePool, (), (const, override));
  MOCK_METHOD(void, PrepareChangeResolution, (), (override));
  MOCK_METHOD(void, NotifyEstimatedMaxDecodeRequests, (int), (override));
  MOCK_METHOD(CroStatus::Or<ImageProcessor::PixelLayoutCandidate>,
              PickDecoderOutputFormat,
              (const std::vector<ImageProcessor::PixelLayoutCandidate>&,
               const gfx::Rect&,
               const gfx::Size&,
               std::optional<gfx::Size>,
               size_t,
               bool,
               bool,
               std::optional<DmabufVideoFramePool::CreateFrameCB>),
              (override));

  MOCK_METHOD(void, InitCallback, (DecoderStatus), ());

  base::WeakPtrFactory<MockVideoDecoderMixinClient> weak_ptr_factory_;
};

// Test fixture to use with V4L2StatefulVideoDecoder
class V4L2FlatVideoDecoderTest : public ::testing::Test {
 public:
  V4L2FlatVideoDecoderTest() = default;
  void SetUp() override {
    // Only run tests using V4L2StatefulVideoDecoder on platforms supporting the
    // V4L2 stateful decoder API.
    if (!IsV4L2DecoderStateful()) {
      GTEST_SKIP();
    }
  }
};

// Verifies that V4L2StatefulVideoDecoder::Initialize() fails when called with
// an unsupported codec profile.
TEST_F(V4L2FlatVideoDecoderTest, UnsupportedVideoCodec) {
  base::test::TaskEnvironment task_environment;
  MockVideoDecoderMixinClient mock_client;

  auto decoder = V4L2StatefulVideoDecoder::Create(
      std::make_unique<MockMediaLog>(),
      base::SequencedTaskRunner::GetCurrentDefault(),
      mock_client.weak_ptr_factory_.GetWeakPtr());

  const auto unsupported_config = TestVideoConfig::Normal(VideoCodec::kMPEG2);
  EXPECT_CALL(
      mock_client,
      InitCallback(DecoderStatus(DecoderStatus::Codes::kUnsupportedConfig)));
  static_cast<V4L2StatefulVideoDecoder*>(decoder.get())
      ->Initialize(unsupported_config, /*low_delay=*/false,
                   /*cdm_context=*/nullptr,
                   base::BindOnce(&MockVideoDecoderMixinClient::InitCallback,
                                  mock_client.weak_ptr_factory_.GetWeakPtr()),
                   /*output_cb=*/base::DoNothing(),
                   /*waiting_cb*/ base::DoNothing());
}

// Verifies that V4L2StatefulVideoDecoder::Initialize() fails after the limit of
// created instances exceeds the threshold.
TEST_F(V4L2FlatVideoDecoderTest, TooManyDecoderInstances) {
  base::test::TaskEnvironment task_environment;
  ::testing::NiceMock<MockVideoDecoderMixinClient> mock_client;
  const auto supported_config = TestVideoConfig::Normal(VideoCodec::kH264);

  const int kMaxNumOfInstances =
      V4L2StatefulVideoDecoder::GetMaxNumDecoderInstancesForTesting();

  ::testing::InSequence s;
  EXPECT_CALL(mock_client,
              InitCallback(DecoderStatus(DecoderStatus::Codes::kOk)))
      .Times(::testing::Exactly(kMaxNumOfInstances));

  std::vector<std::unique_ptr<VideoDecoderMixin>> decoders(kMaxNumOfInstances);
  for (auto& decoder : decoders) {
    decoder = V4L2StatefulVideoDecoder::Create(
        std::make_unique<MockMediaLog>(),
        base::SequencedTaskRunner::GetCurrentDefault(),
        mock_client.weak_ptr_factory_.GetWeakPtr());

    static_cast<V4L2StatefulVideoDecoder*>(decoder.get())
        ->Initialize(supported_config,
                     /*low_delay=*/false, /*cdm_context=*/nullptr,
                     base::BindOnce(&MockVideoDecoderMixinClient::InitCallback,
                                    mock_client.weak_ptr_factory_.GetWeakPtr()),
                     /*output_cb=*/base::DoNothing(),
                     /*waiting_cb*/ base::DoNothing());
  }
  testing::Mock::VerifyAndClearExpectations(&mock_client);

  // Next one fails:
  EXPECT_CALL(
      mock_client,
      InitCallback(DecoderStatus(DecoderStatus::Codes::kTooManyDecoders)));
  auto decoder = V4L2StatefulVideoDecoder::Create(
      std::make_unique<MockMediaLog>(),
      base::SequencedTaskRunner::GetCurrentDefault(),
      mock_client.weak_ptr_factory_.GetWeakPtr());
  static_cast<V4L2StatefulVideoDecoder*>(decoder.get())
      ->Initialize(supported_config,
                   /*low_delay=*/false, /*cdm_context=*/nullptr,
                   base::BindOnce(&MockVideoDecoderMixinClient::InitCallback,
                                  mock_client.weak_ptr_factory_.GetWeakPtr()),
                   /*output_cb=*/base::DoNothing(),
                   /*waiting_cb*/ base::DoNothing());
}

}  // namespace media

int main(int argc, char** argv) {
  base::TestSuite test_suite(argc, argv);
  {}

  return base::LaunchUnitTests(
      argc, argv,
      base::BindOnce(&base::TestSuite::Run, base::Unretained(&test_suite)));
}