| // 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 <drm.h> |
| #include <fcntl.h> |
| #include <gbm.h> |
| #include <string.h> |
| #include <xf86drm.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/memory/scoped_refptr.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 "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/libdrm/src/include/drm/drm_fourcc.h" |
| #include "ui/gfx/linux/gbm_defines.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<size_t> strides = |
| VideoFrame::ComputeStrides(PIXEL_FORMAT_NV12, coded_size); |
| for (int i = 0; i < bo_num_planes; ++i) { |
| size_t s = base::bits::AlignUp(strides[i], static_cast<size_t>(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()); |
| |
| auto device = base::MakeRefCounted<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))); |
| } |