// Copyright 2024 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/d3d12_video_processor_wrapper.h"

#include "base/compiler_specific.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "media/gpu/windows/d3d12_helpers.h"
#include "ui/gfx/color_space_win.h"

namespace media {

D3D12VideoProcessorWrapper::D3D12VideoProcessorWrapper(
    ComD3D12VideoDevice video_device)
    : video_device_(video_device) {
  CHECK_EQ(video_device.As(&device_), S_OK);
}

D3D12VideoProcessorWrapper::~D3D12VideoProcessorWrapper() {
  // Ensure any in-flight video processing work has completed before releasing
  // resources that may still be referenced by the GPU.
  if (auto status = WaitForInFlightWorkImpl(); !status.is_ok()) {
    DLOG(ERROR) << "Waiting for in-flight video processing during teardown "
                   "failed: "
                << static_cast<int>(status.code());
  }
}

bool D3D12VideoProcessorWrapper::Init() {
  D3D12_COMMAND_QUEUE_DESC command_queue_desc{
      D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS};
  HRESULT hr = device_->CreateCommandQueue(&command_queue_desc,
                                           IID_PPV_ARGS(&command_queue_));
  if (FAILED(hr)) {
    DLOG(ERROR) << "Create video process command queue failed: "
                << logging::SystemErrorCodeToString(hr);
    return false;
  }

  hr = device_->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS,
                                       IID_PPV_ARGS(&command_allocator_));
  if (FAILED(hr)) {
    DLOG(ERROR) << "Create video process command allocator failed: "
                << logging::SystemErrorCodeToString(hr);
    return false;
  }

  hr = device_->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS,
                                  command_allocator_.Get(), nullptr,
                                  IID_PPV_ARGS(&command_list_));
  if (FAILED(hr)) {
    DLOG(ERROR) << "Create video process command list failed: "
                << logging::SystemErrorCodeToString(hr);
    return false;
  }

  hr = command_list_->Close();
  if (FAILED(hr)) {
    DLOG(ERROR) << "Close video process command list failed: "
                << logging::SystemErrorCodeToString(hr);
    return false;
  }

  fence_ = D3D12Fence::Create(device_.Get());
  if (!fence_) {
    DLOG(ERROR) << "Failed to create D3D12Fence";
    return false;
  }

  return true;
}

D3D11Status D3D12VideoProcessorWrapper::WaitForInFlightWork() {
  return WaitForInFlightWorkImpl();
}

D3D11Status D3D12VideoProcessorWrapper::WaitForInFlightWorkImpl() {
  if (!fence_ || fence_->GetCompletedValue() >= fence_->Value()) {
    return D3D11StatusCode::kOk;
  }
  return fence_->WaitCPU(fence_->Value());
}

bool D3D12VideoProcessorWrapper::Wait(D3D12FenceAndValue fence_and_value) {
  auto [fence, value] = fence_and_value;
  CHECK(fence);
  HRESULT hr = command_queue_->Wait(fence.Get(), value);
  if (FAILED(hr)) {
    DLOG(ERROR) << "D3D12 video process command queue wait failed: "
                << logging::SystemErrorCodeToString(hr);
    return false;
  }
  return true;
}

bool D3D12VideoProcessorWrapper::CheckVideoProcessorSupport(
    UINT input_width,
    UINT input_height,
    DXGI_FORMAT input_format,
    const gfx::ColorSpace& input_color_space,
    DXGI_FORMAT output_format,
    const gfx::ColorSpace& output_color_space) {
  D3D12_FEATURE_DATA_VIDEO_PROCESS_SUPPORT support{
      .InputSample = {.Width = input_width,
                      .Height = input_height,
                      .Format = {.Format = input_format,
                                 .ColorSpace =
                                     gfx::ColorSpaceWin::GetDXGIColorSpace(
                                         input_color_space)}},
      .InputFrameRate = {30, 1},
      .OutputFormat = {.Format = output_format,
                       .ColorSpace = gfx::ColorSpaceWin::GetDXGIColorSpace(
                           output_color_space)},
      .OutputFrameRate = {30, 1},
  };
  HRESULT hr = video_device_->CheckFeatureSupport(
      D3D12_FEATURE_VIDEO_PROCESS_SUPPORT, &support, sizeof(support));
  if (FAILED(hr)) {
    DLOG(ERROR) << "CheckFeatureSupport for "
                   "D3D12_FEATURE_VIDEO_PROCESS_SUPPORT failed: "
                << logging::SystemErrorCodeToString(hr);
    return false;
  }
  return support.SupportFlags == D3D12_VIDEO_PROCESS_SUPPORT_FLAG_SUPPORTED;
}

D3D12FenceAndValue D3D12VideoProcessorWrapper::ProcessFrames(
    ID3D12Resource* input_texture,
    UINT input_subresource,
    const gfx::ColorSpace& input_color_space,
    const gfx::Rect& input_rectangle,
    ID3D12Resource* output_texture,
    UINT output_subresource,
    const gfx::ColorSpace& output_color_space,
    const gfx::Rect& output_rectangle) {
  DCHECK(command_queue_ && command_allocator_ && command_list_ && fence_);
  DCHECK(!input_rectangle.IsEmpty());
  D3D12_RESOURCE_DESC input_texture_desc = input_texture->GetDesc();
  D3D12_RESOURCE_DESC output_texture_desc = output_texture->GetDesc();
  D3D12_VIDEO_SIZE_RANGE source_size_range{
      static_cast<UINT>(input_texture_desc.Width), input_texture_desc.Height,
      static_cast<UINT>(input_rectangle.width()),
      static_cast<UINT>(input_rectangle.height())};
  D3D12_VIDEO_SIZE_RANGE destination_size_range{
      static_cast<UINT>(output_texture_desc.Width), output_texture_desc.Height,
      static_cast<UINT>(output_texture_desc.Width), output_texture_desc.Height};
  D3D12_VIDEO_PROCESS_INPUT_STREAM_DESC input_stream_desc{
      .Format = input_texture_desc.Format,
      .ColorSpace = gfx::ColorSpaceWin::GetDXGIColorSpace(input_color_space),
      .FrameRate = {30, 1},
      .SourceSizeRange = source_size_range,
      .DestinationSizeRange = destination_size_range,
  };
  D3D12_VIDEO_PROCESS_OUTPUT_STREAM_DESC output_stream_desc{
      .Format = output_texture_desc.Format,
      .ColorSpace = gfx::ColorSpaceWin::GetDXGIColorSpace(output_color_space),
      .FrameRate = {30, 1},
  };

  HRESULT hr;
  if (UNSAFE_TODO(memcmp(&input_stream_desc, &input_stream_desc_,
                         sizeof(D3D12_VIDEO_PROCESS_INPUT_STREAM_DESC))) != 0 ||
      UNSAFE_TODO(memcmp(&output_stream_desc, &output_stream_desc_,
                         sizeof(D3D12_VIDEO_PROCESS_OUTPUT_STREAM_DESC))) !=
          0) {
    // Query for the source rectangle rather than the whole input texture: that
    // is the region ProcessFrames() below samples, and the driver's answer
    // depends on the size it is asked to read and scale.
    if (!CheckVideoProcessorSupport(
            static_cast<UINT>(input_rectangle.width()),
            static_cast<UINT>(input_rectangle.height()),
            input_texture_desc.Format, input_color_space,
            output_texture_desc.Format, output_color_space)) {
      DLOG(ERROR) << "D3D12 cannot support video processing.";
      return {};
    }

    hr = video_device_->CreateVideoProcessor(0, &output_stream_desc, 1,
                                             &input_stream_desc,
                                             IID_PPV_ARGS(&video_processor_));
    if (FAILED(hr)) {
      DLOG(ERROR) << "Create video processor failed: "
                  << logging::SystemErrorCodeToString(hr);
      return {};
    }
    input_stream_desc_ = input_stream_desc;
    output_stream_desc_ = output_stream_desc;
  }

  // Ensure the GPU has finished executing previous commands before resetting
  // the allocator. Without this guard, a downstream error could leave the
  // allocator in use when Reset() is called.
  // An alternative approach is to reuse the allocator without resetting it.
  // However, this could potentially cause the allocator to grow unboundedly.
  if (auto status = WaitForInFlightWork(); !status.is_ok()) {
    DLOG(ERROR) << "Waiting for previous video processing failed: "
                << static_cast<int>(status.code());
    return {};
  }

  hr = command_allocator_->Reset();
  if (FAILED(hr)) {
    DLOG(ERROR) << "Reset video process command allocator failed:"
                << logging::SystemErrorCodeToString(hr);
    return {};
  }

  hr = command_list_->Reset(command_allocator_.Get());
  if (FAILED(hr)) {
    DLOG(ERROR) << "Reset video process command list failed:"
                << logging::SystemErrorCodeToString(hr);
    return {};
  }

  std::vector<D3D12_RESOURCE_BARRIER> resource_barriers;
  auto barriers1 = CreateD3D12TransitionBarriersForAllPlanes(
      input_texture, input_subresource, D3D12_RESOURCE_STATE_COMMON,
      D3D12_RESOURCE_STATE_VIDEO_PROCESS_READ);
  auto barriers2 = CreateD3D12TransitionBarriersForAllPlanes(
      output_texture, output_subresource, D3D12_RESOURCE_STATE_COMMON,
      D3D12_RESOURCE_STATE_VIDEO_PROCESS_WRITE);
  resource_barriers.insert(resource_barriers.end(), barriers1.begin(),
                           barriers1.end());
  resource_barriers.insert(resource_barriers.end(), barriers2.begin(),
                           barriers2.end());
  command_list_->ResourceBarrier(resource_barriers.size(),
                                 resource_barriers.data());

  D3D12_VIDEO_PROCESS_INPUT_STREAM_ARGUMENTS input_stream_arguments{
      .InputStream = {{.pTexture2D = input_texture,
                       .Subresource = input_subresource}},
      .Transform = {.SourceRectangle = input_rectangle.ToRECT(),
                    .DestinationRectangle = output_rectangle.ToRECT()}};
  D3D12_VIDEO_PROCESS_OUTPUT_STREAM_ARGUMENTS output_stream_arguments{
      .OutputStream = {{.pTexture2D = output_texture,
                        .Subresource = output_subresource}},
      .TargetRectangle = output_rectangle.ToRECT()};
  command_list_->ProcessFrames(video_processor_.Get(), &output_stream_arguments,
                               1, &input_stream_arguments);

  for (auto& barrier : resource_barriers) {
    std::swap(barrier.Transition.StateBefore, barrier.Transition.StateAfter);
  }
  command_list_->ResourceBarrier(resource_barriers.size(),
                                 resource_barriers.data());

  hr = command_list_->Close();
  if (FAILED(hr)) {
    DLOG(ERROR) << "Close video process command list failed:"
                << logging::SystemErrorCodeToString(hr);
    return {};
  }

  ID3D12CommandList* command_list = command_list_.Get();
  command_queue_->ExecuteCommandLists(1, &command_list);

  auto value_or_error = fence_->Signal(*command_queue_.Get());
  if (!value_or_error.has_value()) {
    DLOG(ERROR) << "Failed to call Signal: "
                << std::move(value_or_error).error().message();
    return {};
  }
  return {fence_->Get(), std::move(value_or_error).value()};
}

}  // namespace media
