chromium/media/gpu/windows/d3d11_video_decoder_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 "media/gpu/windows/d3d11_video_decoder.h"

#include <d3d11.h>
#include <d3d11_1.h>
#include <initguid.h>

#include <memory>
#include <optional>
#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/win/scoped_com_initializer.h"
#include "media/base/decoder_buffer.h"
#include "media/base/media_log.h"
#include "media/base/media_switches.h"
#include "media/base/media_util.h"
#include "media/base/supported_types.h"
#include "media/base/test_helpers.h"
#include "media/base/win/d3d11_mocks.h"
#include "media/gpu/test/fake_command_buffer_helper.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;
using ::testing::DoAll;
using ::testing::Eq;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::SaveArg;
using ::testing::SetArgPointee;

namespace media {

class D3D11VideoDecoderTest : public ::testing::Test {
 public:
  const std::pair<uint16_t, uint16_t> LegacyIntelGPU = {0x8086, 0x102};
  const std::pair<uint16_t, uint16_t> RecentIntelGPU = {0x8086, 0x100};
  const std::pair<uint16_t, uint16_t> LegacyAMDGPU = {0x1022, 0x130f};
  const std::pair<uint16_t, uint16_t> RecentAMDGPU = {0x1022, 0x130e};

  void SetUp() override {
    // Set up some sane defaults.
    gpu_preferences_.enable_zero_copy_dxgi_video = true;
    gpu_preferences_.use_passthrough_cmd_decoder = false;
    gpu_workarounds_.disable_dxgi_zero_copy_video = false;
    gpu_task_runner_ = task_environment_.GetMainThreadTaskRunner();
    fake_command_buffer_helper_ =
        base::MakeRefCounted<FakeCommandBufferHelper>(gpu_task_runner_);

    // Create a mock D3D11 device that supports 11.0.  Note that if you change
    // this, then you probably also want VideoDevice1 and friends, below.
    mock_d3d11_device_ = MakeComPtr<NiceMock<D3D11DeviceMock>>();
    ON_CALL(*mock_d3d11_device_.Get(), QueryInterface(IID_ID3D11Device, _))
        .WillByDefault(SetComPointeeAndReturnOk<1>(mock_d3d11_device_.Get()));
    ON_CALL(*mock_d3d11_device_.Get(), GetFeatureLevel)
        .WillByDefault(Return(D3D_FEATURE_LEVEL_11_0));

    mock_d3d11_device_context_ = MakeComPtr<D3D11DeviceContextMock>();
    ON_CALL(*mock_d3d11_device_.Get(), GetImmediateContext(_))
        .WillByDefault(SetComPointee<0>(mock_d3d11_device_context_.Get()));

    // Set up an D3D11VideoDevice rather than ...Device1, since Initialize uses
    // Device for checking decoder GUIDs.
    // TODO(liberato): Try to use Device1 more often.
    mock_d3d11_video_device_ = MakeComPtr<NiceMock<D3D11VideoDeviceMock>>();
    ON_CALL(*mock_d3d11_device_.Get(), QueryInterface(IID_ID3D11VideoDevice, _))
        .WillByDefault(
            SetComPointeeAndReturnOk<1>(mock_d3d11_video_device_.Get()));

    mock_multithreaded_ = MakeComPtr<NiceMock<D3D11MultithreadMock>>();
    ON_CALL(*mock_d3d11_device_.Get(), QueryInterface(IID_ID3D11Multithread, _))
        .WillByDefault(SetComPointeeAndReturnOk<1>(mock_multithreaded_.Get()));

    EnableDecoder(D3D11_DECODER_PROFILE_H264_VLD_NOFGT);

    mock_d3d11_video_decoder_ = MakeComPtr<D3D11VideoDecoderMock>();
    ON_CALL(*mock_d3d11_video_device_.Get(), CreateVideoDecoder(_, _, _))
        .WillByDefault(
            SetComPointeeAndReturnOk<2>(mock_d3d11_video_decoder_.Get()));

    mock_d3d11_video_context_ = MakeComPtr<D3D11VideoContextMock>();
    ON_CALL(*mock_d3d11_device_context_.Get(),
            QueryInterface(IID_ID3D11VideoContext, _))
        .WillByDefault(
            SetComPointeeAndReturnOk<1>(mock_d3d11_video_context_.Get()));

    SetUpAdapters();
  }

  void SetGPUProfile(std::pair<uint16_t, uint16_t> vendor_and_gpu) {
    mock_adapter_desc_.DeviceId = static_cast<UINT>(vendor_and_gpu.second);
    mock_adapter_desc_.VendorId = static_cast<UINT>(vendor_and_gpu.first);

    ON_CALL(*mock_dxgi_adapter_.Get(), GetDesc(_))
        .WillByDefault(
            DoAll(SetArgPointee<0>(mock_adapter_desc_), Return(S_OK)));
  }

  void SetUpAdapters() {
    mock_dxgi_device_ = MakeComPtr<NiceMock<DXGIDeviceMock>>();
    ON_CALL(*mock_d3d11_device_.Get(), QueryInterface(IID_IDXGIDevice, _))
        .WillByDefault(SetComPointeeAndReturnOk<1>(mock_dxgi_device_.Get()));

    mock_dxgi_adapter_ = MakeComPtr<NiceMock<DXGIAdapterMock>>();
    ON_CALL(*mock_dxgi_device_.Get(), GetAdapter(_))
        .WillByDefault(SetComPointeeAndReturnOk<0>(mock_dxgi_adapter_.Get()));

    SetGPUProfile(RecentIntelGPU);
  }

  // Enable a decoder for the given GUID.  Only one decoder may be enabled at a
  // time.  GUIDs are things like D3D11_DECODER_PROFILE_H264_VLD_NOFGT or
  // D3D11_DECODER_PROFILE_VP9_VLD_PROFILE0, etc.
  void EnableDecoder(GUID decoder_profile) {
    ON_CALL(*mock_d3d11_video_device_.Get(), GetVideoDecoderProfileCount())
        .WillByDefault(Return(1));

    // Note that we don't check if the guid in the config actually matches
    // |decoder_profile|.  Perhaps we should.
    ON_CALL(*mock_d3d11_video_device_.Get(), GetVideoDecoderProfile(0, _))
        .WillByDefault(DoAll(SetArgPointee<1>(decoder_profile), Return(S_OK)));

    ON_CALL(*mock_d3d11_video_device_.Get(), GetVideoDecoderConfigCount(_, _))
        .WillByDefault(DoAll(
            Invoke(this, &D3D11VideoDecoderTest::GetVideoDecoderConfigCount),
            Return(S_OK)));

    video_decoder_config_.ConfigBitstreamRaw =
        decoder_profile == D3D11_DECODER_PROFILE_H264_VLD_NOFGT ? 2 : 1;

    ON_CALL(*mock_d3d11_video_device_.Get(), GetVideoDecoderConfig(_, 0, _))
        .WillByDefault(
            DoAll(SetArgPointee<2>(video_decoder_config_), Return(S_OK)));
  }

  void GetVideoDecoderConfigCount(const D3D11_VIDEO_DECODER_DESC* desc,
                                  UINT* config_count_out) {
    last_video_decoder_desc_ = *desc;
    *config_count_out = 1;
  }

  scoped_refptr<CommandBufferHelper> GetCommandBufferHelper() {
    return fake_command_buffer_helper_;
  }

  // Most recently provided video decoder desc.
  std::optional<D3D11_VIDEO_DECODER_DESC> last_video_decoder_desc_;
  D3D11_VIDEO_DECODER_CONFIG video_decoder_config_;

  void TearDown() override {
    decoder_.reset();
    base::RunLoop().RunUntilIdle();
  }

  void EnableFeature(const base::Feature& feature) {
    scoped_feature_list_.emplace();
    scoped_feature_list_->InitAndEnableFeature(feature);
  }

  void DisableFeature(const base::Feature& feature) {
    scoped_feature_list_.emplace();
    scoped_feature_list_->InitAndDisableFeature(feature);
  }

  // If provided, |supported_configs| is the list of configs that will be
  // checked before init can succeed.  If one is provided, then we'll
  // use it.  Otherwise, we'll use the list that's autodetected by the
  // decoder based on the current device mock.
  void CreateDecoder(
      std::optional<D3D11VideoDecoder::SupportedConfigs> supported_configs =
          std::optional<D3D11VideoDecoder::SupportedConfigs>()) {
    auto get_device_cb = base::BindRepeating(
        [](ComD3D11Device device,
           D3D11VideoDecoder::D3DVersion version) -> ComUnknown {
          EXPECT_EQ(version, D3D11VideoDecoder::D3DVersion::kD3D11);
          return device;
        },
        mock_d3d11_device_);

    // Autodetect the supported configs, unless it's being overridden.
    if (!supported_configs) {
      supported_configs = D3D11VideoDecoder::GetSupportedVideoDecoderConfigs(
          gpu_preferences_, gpu_workarounds_, get_device_cb);
    }
    task_environment_.RunUntilIdle();

    // We store it in a std::unique_ptr<VideoDecoder> so that the default
    // deleter works.  The dtor is protected.
    decoder_ = base::WrapUnique<VideoDecoder>(
        d3d11_decoder_raw_ = new D3D11VideoDecoder(
            gpu_task_runner_, std::make_unique<NullMediaLog>(),
            gpu_preferences_, gpu_workarounds_,
            base::BindRepeating(&D3D11VideoDecoderTest::GetCommandBufferHelper,
                                base::Unretained(this)),
            get_device_cb, *supported_configs));
  }

  void InitializeDecoder(const VideoDecoderConfig& config,
                         bool expect_success) {
    const bool low_delay = false;
    CdmContext* cdm_context = nullptr;

    decoder_->Initialize(config, low_delay, cdm_context,
                         base::BindOnce(&D3D11VideoDecoderTest::CheckStatus,
                                        base::Unretained(this), expect_success),
                         base::DoNothing(), base::DoNothing());
    task_environment_.RunUntilIdle();
  }

  void CheckStatus(bool expect_success, DecoderStatus actual) {
    ASSERT_EQ(expect_success, actual.is_ok());
  }

  base::test::TaskEnvironment task_environment_;

  scoped_refptr<base::SingleThreadTaskRunner> gpu_task_runner_;

  scoped_refptr<FakeCommandBufferHelper> fake_command_buffer_helper_;

  std::unique_ptr<VideoDecoder> decoder_;
  raw_ptr<D3D11VideoDecoder, DanglingUntriaged> d3d11_decoder_raw_ = nullptr;
  gpu::GpuPreferences gpu_preferences_;
  gpu::GpuDriverBugWorkarounds gpu_workarounds_;

  Microsoft::WRL::ComPtr<D3D11DeviceMock> mock_d3d11_device_;
  Microsoft::WRL::ComPtr<D3D11DeviceContextMock> mock_d3d11_device_context_;
  Microsoft::WRL::ComPtr<D3D11MultithreadMock> mock_multithreaded_;
  Microsoft::WRL::ComPtr<D3D11VideoDeviceMock> mock_d3d11_video_device_;
  Microsoft::WRL::ComPtr<D3D11VideoDecoderMock> mock_d3d11_video_decoder_;
  Microsoft::WRL::ComPtr<D3D11VideoContextMock> mock_d3d11_video_context_;
  Microsoft::WRL::ComPtr<DXGIDeviceMock> mock_dxgi_device_;
  Microsoft::WRL::ComPtr<DXGIAdapterMock> mock_dxgi_adapter_;

  DXGI_ADAPTER_DESC mock_adapter_desc_;

  std::optional<base::test::ScopedFeatureList> scoped_feature_list_;
  base::win::ScopedCOMInitializer com_initializer_;
};

TEST_F(D3D11VideoDecoderTest, SupportsVP9Profile0WithDecoderEnabled) {
  VideoDecoderConfig configuration = TestVideoConfig::NormalCodecProfile(
      VideoCodec::kVP9, VP9PROFILE_PROFILE0);

  EnableDecoder(D3D11_DECODER_PROFILE_VP9_VLD_PROFILE0);
  CreateDecoder();
  InitializeDecoder(configuration, /*expect_success=*/true);
}

TEST_F(D3D11VideoDecoderTest, DoesNotSupportVP9WithGPUWorkaroundDisableVPX) {
  gpu_workarounds_.disable_accelerated_vp9_decode = true;
  VideoDecoderConfig configuration = TestVideoConfig::NormalCodecProfile(
      VideoCodec::kVP9, VP9PROFILE_PROFILE0);

  EnableDecoder(D3D11_DECODER_PROFILE_VP9_VLD_PROFILE0);
  CreateDecoder();
  InitializeDecoder(configuration, /*expect_success=*/false);
}

TEST_F(D3D11VideoDecoderTest, DoesNotSupportVP9WithoutDecoderEnabled) {
  VideoDecoderConfig configuration = TestVideoConfig::NormalCodecProfile(
      VideoCodec::kVP9, VP9PROFILE_PROFILE0);

  // Enable a non-VP9 decoder.
  EnableDecoder(D3D11_DECODER_PROFILE_H264_VLD_NOFGT);  // Paranoia, not VP9.
  CreateDecoder();
  InitializeDecoder(configuration, false);
}

TEST_F(D3D11VideoDecoderTest, DoesNotSupportsH264HIGH10Profile) {
  CreateDecoder();

  VideoDecoderConfig high10 = TestVideoConfig::NormalCodecProfile(
      VideoCodec::kH264, H264PROFILE_HIGH10PROFILE);

  // When the codec is built in this should fail without H264 decoding being
  // attempted. If H264 isn't built-in, we should at least attempt initialize.
  const bool expect_success = !IsBuiltInVideoCodec(VideoCodec::kH264);
  InitializeDecoder(high10, expect_success);
}

TEST_F(D3D11VideoDecoderTest, SupportsH264WithAutodetectedConfig) {
  CreateDecoder();

  VideoDecoderConfig normal =
      TestVideoConfig::NormalCodecProfile(VideoCodec::kH264, H264PROFILE_MAIN);

  InitializeDecoder(normal, true);
  // TODO(liberato): Check |last_video_decoder_desc_| for sanity.
}

TEST_F(D3D11VideoDecoderTest, DoesNotSupportH264IfNoSupportedConfig) {
  // This is identical to SupportsH264, except that we initialize with an empty
  // list of supported configs.  This should match nothing.  Assuming that
  // SupportsH264WithSupportedConfig passes, then this checks that the supported
  // config check kinda works.
  // For whatever reason, Optional<SupportedConfigs>({}) results in one that
  // doesn't have a value, rather than one that has an empty vector.
  std::optional<D3D11VideoDecoder::SupportedConfigs> empty_configs;
  empty_configs.emplace(std::vector<SupportedVideoDecoderConfig>());
  CreateDecoder(empty_configs);

  VideoDecoderConfig normal =
      TestVideoConfig::NormalCodecProfile(VideoCodec::kH264, H264PROFILE_MAIN);

  // When the codec is built in this should fail without H264 decoding being
  // attempted. If H264 isn't built-in, we should at least attempt initialize.
  const bool expect_success = !IsBuiltInVideoCodec(VideoCodec::kH264);
  InitializeDecoder(normal, expect_success);
}

TEST_F(D3D11VideoDecoderTest, DoesNotSupportEncryptedConfig) {
  CreateDecoder();
  VideoDecoderConfig encrypted_config =
      TestVideoConfig::NormalCodecProfile(VideoCodec::kH264, H264PROFILE_MAIN);
  encrypted_config.SetIsEncrypted(true);
  InitializeDecoder(encrypted_config, false);
}

TEST_F(D3D11VideoDecoderTest, WorkaroundTurnsOffDecoder) {
  //  We shouldn't be able to decode if the decoder is off via gpu workaround.
  gpu_workarounds_.disable_d3d11_video_decoder = true;
  CreateDecoder();
  InitializeDecoder(
      TestVideoConfig::NormalCodecProfile(VideoCodec::kH264, H264PROFILE_MAIN),
      false);
}

TEST_F(D3D11VideoDecoderTest, CanReadWithoutStalling) {
  CreateDecoder();

  VideoDecoderConfig normal =
      TestVideoConfig::NormalCodecProfile(VideoCodec::kH264, H264PROFILE_MAIN);

  InitializeDecoder(normal, true);

  // Should be true prior to picture buffers being assigned.
  EXPECT_TRUE(decoder_->CanReadWithoutStalling());
}

}  // namespace media