// Copyright 2019 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_texture_selector.h"

#include <d3d11.h>

#include "base/feature_list.h"
#include "media/base/media_log.h"
#include "media/base/media_switches.h"
#include "media/base/win/mf_helpers.h"
#include "media/gpu/windows/d3d11_copying_texture_wrapper.h"
#include "media/gpu/windows/d3d11_video_device_format_support.h"
#include "media/gpu/windows/format_utils.h"
#include "ui/gfx/color_space.h"
#include "ui/gfx/color_space_win.h"
#include "ui/gfx/geometry/size.h"

namespace media {

TextureSelector::TextureSelector(VideoPixelFormat pixfmt,
                                 viz::SharedImageFormat output_si_format,
                                 ComD3D11VideoDevice1 video_device,
                                 ComD3D11DeviceContext device_context,
                                 bool shared_image_use_shared_handle)
    : pixel_format_(pixfmt),
      output_si_format_(output_si_format),
      video_device_(std::move(video_device)),
      device_context_(std::move(device_context)),
      shared_image_use_shared_handle_(shared_image_use_shared_handle) {}

TextureSelector::~TextureSelector() = default;

bool SupportsZeroCopy(const gpu::GpuPreferences& preferences,
                      const gpu::GpuDriverBugWorkarounds& workarounds) {
  if (!preferences.enable_zero_copy_dxgi_video)
    return false;

  if (workarounds.disable_dxgi_zero_copy_video)
    return false;

  return true;
}

gfx::ColorSpace GetOutputColorSpace(const gfx::ColorSpace& input_color_space,
                                    bool is_rgb_output) {
  // If input & output both are YUV pixel format, there should be no colorspace
  // conversion during the process operations, leave the colorspace unchanged.
  if (!is_rgb_output) {
    return input_color_space;
  }
  // If input is a YUV format, and output is a RGB format, always set output
  // colorspace to a RGB colorspace.
  if (input_color_space.IsHDR()) {
    return gfx::ColorSpace::CreateHDR10();
  }
  return gfx::ColorSpace::CreateSRGB();
}

// static
std::unique_ptr<TextureSelector> TextureSelector::Create(
    const gpu::GpuPreferences& gpu_preferences,
    const gpu::GpuDriverBugWorkarounds& workarounds,
    DXGI_FORMAT decoder_output_format,
    const FormatSupportChecker* format_checker,
    ComD3D11VideoDevice1 video_device,
    ComD3D11DeviceContext device_context,
    MediaLog* media_log,
    gfx::ColorSpace input_color_space,
    bool shared_image_use_shared_handle) {
  VideoPixelFormat output_pixel_format;
  viz::SharedImageFormat output_si_format;

  bool needs_texture_copy = !SupportsZeroCopy(gpu_preferences, workarounds);

  auto supports_fmt = [format_checker](auto fmt) {
    return format_checker->CheckOutputFormatSupport(fmt);
  };

  switch (decoder_output_format) {
    case DXGI_FORMAT_YUY2:
    case DXGI_FORMAT_AYUV: {
      MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder producing "
                                 << DxgiFormatToString(decoder_output_format);
      // YUY2/AYUV output from decoder is always 8-bit 4:2:2/4:4:4 which we
      // prefer to be rendered in ARGB formats to avoid chroma downsampling. For
      // HDR contents, we should not let YUV to RGB conversion happens inside
      // D3DVideoDecoder, the only place for the conversion should be
      // Gfx::ColorTransform or SwapChainPresenter. For color spaces that VP
      // isn't able to handle the correct color conversion, the current
      // workaround is to output a 4:2:0 YUV format and let viz handle the
      // conversion at the expense of losing 4:2:2/4:4:4 chroma sampling. See
      // https://crbug.com/343014700.
      if (!input_color_space.IsHDR() &&
          gfx::ColorSpaceWin::CanConvertToDXGIColorSpace(input_color_space) &&
          supports_fmt(DXGI_FORMAT_B8G8R8A8_UNORM)) {
        output_pixel_format = PIXEL_FORMAT_ARGB;
        output_si_format = viz::SinglePlaneFormat::kBGRA_8888;
        MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder: Selected ARGB";
      } else if (!needs_texture_copy || supports_fmt(DXGI_FORMAT_NV12)) {
        output_pixel_format = PIXEL_FORMAT_NV12;
        output_si_format = viz::MultiPlaneFormat::kNV12;
        MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder: Selected NV12";
      } else {
        MEDIA_LOG(INFO, media_log)
            << DxgiFormatToString(decoder_output_format) << " not supported";
        return nullptr;
      }
      break;
    }
    case DXGI_FORMAT_NV12: {
      MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder producing "
                                 << DxgiFormatToString(decoder_output_format);
      if (!needs_texture_copy || supports_fmt(DXGI_FORMAT_NV12)) {
        output_pixel_format = PIXEL_FORMAT_NV12;
        output_si_format = viz::MultiPlaneFormat::kNV12;
        MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder: Selected NV12";
      } else if (supports_fmt(DXGI_FORMAT_B8G8R8A8_UNORM)) {
        output_pixel_format = PIXEL_FORMAT_ARGB;
        output_si_format = viz::SinglePlaneFormat::kBGRA_8888;
        MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder: Selected ARGB";
      } else {
        MEDIA_LOG(INFO, media_log)
            << DxgiFormatToString(decoder_output_format) << " not supported";
        return nullptr;
      }
      break;
    }
    case DXGI_FORMAT_Y416:
    case DXGI_FORMAT_Y216:
    case DXGI_FORMAT_Y410:
    case DXGI_FORMAT_Y210: {
      MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder producing "
                                 << DxgiFormatToString(decoder_output_format);
      // Y416/Y216/Y410/Y210 output from decoder is always 10/12-bit 4:2:2/4:4:4
      // which we prefer to be rendered in ARGB formats to avoid chroma
      // downsampling. For HDR contents, we should not let YUV to RGB conversion
      // happens inside D3DVideoDecoder, the only place for the conversion
      // should be Gfx::ColorTransform or SwapChainPresenter. For color spaces
      // that VP isn't able to handle the correct color conversion, the current
      // workaround is to output a 4:2:0 YUV format and let viz handle the
      // conversion at the expense of losing 4:2:2/4:4:4 chroma sampling. See
      // https://crbug.com/343014700.
      if (!input_color_space.IsHDR() &&
          gfx::ColorSpaceWin::CanConvertToDXGIColorSpace(input_color_space) &&
          supports_fmt(DXGI_FORMAT_R10G10B10A2_UNORM)) {
        output_si_format = viz::SinglePlaneFormat::kRGBA_1010102;
        output_pixel_format = PIXEL_FORMAT_XB30;
        MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder: Selected XB30";
      } else if (!needs_texture_copy || supports_fmt(DXGI_FORMAT_P010)) {
        output_si_format = viz::MultiPlaneFormat::kP010;
        output_pixel_format = PIXEL_FORMAT_P010LE;
        MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder: Selected P010LE";
      } else if (supports_fmt(DXGI_FORMAT_B8G8R8A8_UNORM)) {
        output_si_format = viz::SinglePlaneFormat::kBGRA_8888;
        output_pixel_format = PIXEL_FORMAT_ARGB;
        MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder: Selected ARGB";
      } else {
        MEDIA_LOG(INFO, media_log)
            << DxgiFormatToString(decoder_output_format) << " not supported";
        return nullptr;
      }
      break;
    }
    case DXGI_FORMAT_P010:
    case DXGI_FORMAT_P016: {
      MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder producing "
                                 << DxgiFormatToString(decoder_output_format);
      // If device support P010 zero copy, then try P010 firstly.
      if (!needs_texture_copy || supports_fmt(DXGI_FORMAT_P010)) {
        output_si_format = viz::MultiPlaneFormat::kP010;
        output_pixel_format = PIXEL_FORMAT_P010LE;
        MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder: Selected P010LE";
      } else if (supports_fmt(DXGI_FORMAT_R10G10B10A2_UNORM)) {
        output_si_format = viz::SinglePlaneFormat::kRGBA_1010102;
        output_pixel_format = PIXEL_FORMAT_XB30;
        MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder: Selected XB30";
      } else if (supports_fmt(DXGI_FORMAT_B8G8R8A8_UNORM)) {
        output_si_format = viz::SinglePlaneFormat::kBGRA_8888;
        output_pixel_format = PIXEL_FORMAT_ARGB;
        MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder: Selected ARGB";
      } else {
        MEDIA_LOG(INFO, media_log)
            << DxgiFormatToString(decoder_output_format) << " not supported";
        return nullptr;
      }
      break;
    }
    default: {
      MEDIA_LOG(INFO, media_log)
          << "D3DVideoDecoder does not support " << decoder_output_format;
      return nullptr;
    }
  }

  // If we're trying to produce an output texture that's different from what
  // the decoder is providing, then we need to copy it. If sharing decoder
  // textures is not allowed, then copy either way.
  needs_texture_copy |= (decoder_output_format !=
                         SharedImageFormatToDXGIFormat(output_si_format));

  if (needs_texture_copy) {
    MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder is copying textures";
    return std::make_unique<CopyTextureSelector>(
        output_pixel_format, output_si_format, std::move(video_device),
        std::move(device_context), shared_image_use_shared_handle);
  } else {
    MEDIA_LOG(INFO, media_log) << "D3DVideoDecoder is binding textures";
    return std::make_unique<TextureSelector>(
        output_pixel_format, output_si_format, std::move(video_device),
        std::move(device_context), shared_image_use_shared_handle);
  }
}

std::unique_ptr<Texture2DWrapper> TextureSelector::CreateTextureWrapper(
    ComD3D11Device device,
    gfx::ColorSpace color_space,
    gfx::Size size) {
  // TODO(liberato): If the output format is rgb, then create a pbuffer wrapper.
  return std::make_unique<DefaultTexture2DWrapper>(
      size, color_space, OutputSharedImageFormat(), device);
}

bool TextureSelector::DoesDecoderOutputUseSharedHandle() const {
  return shared_image_use_shared_handle_;
}

bool TextureSelector::WillCopyForTesting() const {
  return false;
}

CopyTextureSelector::CopyTextureSelector(
    VideoPixelFormat pixfmt,
    viz::SharedImageFormat output_si_format,
    ComD3D11VideoDevice1 video_device,
    ComD3D11DeviceContext device_context,
    bool shared_image_use_shared_handle)
    : TextureSelector(pixfmt,
                      output_si_format,
                      std::move(video_device),
                      std::move(device_context),
                      shared_image_use_shared_handle),
      video_processor_proxy_(
          base::MakeRefCounted<VideoProcessorProxy>(this->video_device(),
                                                    this->device_context())) {}

CopyTextureSelector::~CopyTextureSelector() = default;

std::unique_ptr<Texture2DWrapper> CopyTextureSelector::CreateTextureWrapper(
    ComD3D11Device device,
    gfx::ColorSpace input_color_space,
    gfx::Size size) {
  D3D11_TEXTURE2D_DESC texture_desc = {};
  texture_desc.MipLevels = 1;
  texture_desc.ArraySize = 1;
  texture_desc.CPUAccessFlags = 0;
  texture_desc.Format = SharedImageFormatToDXGIFormat(output_si_format_);
  texture_desc.SampleDesc.Count = 1;
  texture_desc.Usage = D3D11_USAGE_DEFAULT;
  texture_desc.BindFlags =
      D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
  texture_desc.Width = size.width();
  texture_desc.Height = size.height();
  if (DoesSharedImageUseSharedHandle()) {
    texture_desc.MiscFlags =
        D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED;
  }

  ComD3D11Texture2D out_texture;
  if (FAILED(device->CreateTexture2D(&texture_desc, nullptr, &out_texture)))
    return nullptr;

  if (FAILED(
          SetDebugName(out_texture.Get(), "D3D11Decoder_CopyTextureSelector")))
    return nullptr;

  gfx::ColorSpace output_color_space =
      GetOutputColorSpace(input_color_space, IsRGB(pixel_format_));

  return std::make_unique<CopyingTexture2DWrapper>(
      size, input_color_space, output_color_space,
      std::make_unique<DefaultTexture2DWrapper>(
          size, output_color_space, OutputSharedImageFormat(), device),
      video_processor_proxy_, out_texture);
}

bool CopyTextureSelector::DoesDecoderOutputUseSharedHandle() const {
  return base::FeatureList::IsEnabled(kD3D12VideoDecoder);
}

bool CopyTextureSelector::WillCopyForTesting() const {
  return true;
}

}  // namespace media
