// Copyright 2015 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include <algorithm>
#include <queue>
#include <vector>

#include "VideoBackends/D3D12/D3DBase.h"
#include "VideoBackends/D3D12/D3DCommandListManager.h"
#include "VideoBackends/D3D12/D3DDescriptorHeapManager.h"
#include "VideoBackends/D3D12/D3DQueuedCommandList.h"
#include "VideoBackends/D3D12/D3DState.h"
#include "VideoBackends/D3D12/D3DTexture.h"

#include "VideoBackends/D3D12/Render.h"
#include "VideoBackends/D3D12/ShaderConstantsManager.h"
#include "VideoBackends/D3D12/VertexManager.h"

static constexpr unsigned int COMMAND_ALLOCATORS_PER_LIST = 2;

namespace DX12
{
extern StateCache gx_state_cache;

D3DCommandListManager::D3DCommandListManager(D3D12_COMMAND_LIST_TYPE command_list_type,
                                             ID3D12Device* device,
                                             ID3D12CommandQueue* command_queue)
    : m_device(device), m_command_queue(command_queue)
{
  // Create two lists, with two command allocators each. This corresponds to up to two frames in
  // flight at once.
  m_current_command_allocator = 0;
  m_current_command_allocator_list = 0;
  for (UINT i = 0; i < COMMAND_ALLOCATORS_PER_LIST; i++)
  {
    for (UINT j = 0; j < m_command_allocator_lists.size(); j++)
    {
      ID3D12CommandAllocator* command_allocator = nullptr;

      CheckHR(
          m_device->CreateCommandAllocator(command_list_type, IID_PPV_ARGS(&command_allocator)));
      m_command_allocator_lists[j].push_back(command_allocator);
    }
  }

  // Create backing command list.
  CheckHR(m_device->CreateCommandList(
      0, command_list_type, m_command_allocator_lists[m_current_command_allocator_list][0], nullptr,
      IID_PPV_ARGS(&m_backing_command_list)));

#ifdef USE_D3D12_QUEUED_COMMAND_LISTS
  m_queued_command_list = new ID3D12QueuedCommandList(m_backing_command_list, m_command_queue);
#endif

  // Create fence that will be used to measure GPU progress of app rendering requests (e.g. CPU
  // readback of GPU data).
  m_queue_fence_value = 0;
  CheckHR(m_device->CreateFence(m_queue_fence_value, D3D12_FENCE_FLAG_NONE,
                                IID_PPV_ARGS(&m_queue_fence)));

  // Create fence that will be used internally by D3DCommandListManager for frame-level resource
  // tracking.
  m_queue_frame_fence_value = 0;
  CheckHR(m_device->CreateFence(m_queue_frame_fence_value, D3D12_FENCE_FLAG_NONE,
                                IID_PPV_ARGS(&m_queue_frame_fence)));

  // Create event that will be used for waiting on CPU until a fence is signaled by GPU.
  m_wait_on_cpu_fence_event = CreateEvent(nullptr, FALSE, FALSE, nullptr);

  // Pre-size the deferred destruction lists.
  for (UINT i = 0; i < m_deferred_destruction_lists.size(); i++)
  {
    m_deferred_destruction_lists[i].reserve(200);
  }

  m_current_deferred_destruction_list = 0;

  std::fill(m_command_allocator_list_fences.begin(), m_command_allocator_list_fences.end(), 0);
  std::fill(m_deferred_destruction_list_fences.begin(), m_deferred_destruction_list_fences.end(),
            0);
}

void D3DCommandListManager::SetInitialCommandListState()
{
  ID3D12GraphicsCommandList* command_list = nullptr;
  GetCommandList(&command_list);

  command_list->SetDescriptorHeaps(static_cast<unsigned int>(D3D::gpu_descriptor_heaps.size()),
                                   D3D::gpu_descriptor_heaps.data());
  command_list->SetGraphicsRootSignature(D3D::default_root_signature);

  if (g_renderer)
  {
    // It is possible that we change command lists in the middle of the frame. In that case, restore
    // the viewport/scissor to the current console GPU state.
    g_renderer->RestoreAPIState();
  }

  m_command_list_dirty_state = UINT_MAX;

  command_list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
  m_command_list_current_topology = D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP;

  if (g_vertex_manager)
    reinterpret_cast<VertexManager*>(g_vertex_manager.get())->SetIndexBuffer();
}

void D3DCommandListManager::GetCommandList(ID3D12GraphicsCommandList** command_list) const
{
#ifdef USE_D3D12_QUEUED_COMMAND_LISTS
  *command_list = this->m_queued_command_list;
#else
  *command_list = this->m_backing_command_list;
#endif
}

void D3DCommandListManager::ExecuteQueuedWork(bool wait_for_gpu_completion)
{
  m_queue_fence_value++;

#ifdef USE_D3D12_QUEUED_COMMAND_LISTS
  m_queued_command_list->Close();
  m_queued_command_list->QueueExecute();
  m_queued_command_list->QueueFenceGpuSignal(m_queue_fence, m_queue_fence_value);
  m_queued_command_list->ProcessQueuedItems(wait_for_gpu_completion, wait_for_gpu_completion);
#else
  CheckHR(m_backing_command_list->Close());

  ID3D12CommandList* const execute_list[1] = {m_backing_command_list};
  m_command_queue->ExecuteCommandLists(1, execute_list);

  CheckHR(m_command_queue->Signal(m_queue_fence, m_queue_fence_value));
#endif

  // Notify observers of the fence value for the current work to finish.
  for (auto it : m_queue_fence_callbacks)
    it.second(it.first, m_queue_fence_value);

  if (wait_for_gpu_completion)
    WaitForGPUCompletion();

  // Re-open the command list, using the current allocator.
  ResetCommandList();
  SetInitialCommandListState();
}

void D3DCommandListManager::ExecuteQueuedWorkAndPresent(IDXGISwapChain* swap_chain,
                                                        UINT sync_interval, UINT flags)
{
  m_queue_fence_value++;

#ifdef USE_D3D12_QUEUED_COMMAND_LISTS
  m_queued_command_list->Close();
  m_queued_command_list->QueueExecute();
  m_queued_command_list->QueuePresent(swap_chain, sync_interval, flags);
  m_queued_command_list->QueueFenceGpuSignal(m_queue_fence, m_queue_fence_value);
  m_queued_command_list->ProcessQueuedItems(true);
#else
  CheckHR(m_backing_command_list->Close());

  ID3D12CommandList* const execute_list[1] = {m_backing_command_list};
  m_command_queue->ExecuteCommandLists(1, execute_list);

  CheckHR(swap_chain->Present(sync_interval, flags));
  CheckHR(m_command_queue->Signal(m_queue_fence, m_queue_fence_value));
#endif

  // Notify observers of the fence value for the current work to finish.
  for (auto it : m_queue_fence_callbacks)
    it.second(it.first, m_queue_fence_value);

  // Move to the next command allocator, this may mean switching allocator lists.
  MoveToNextCommandAllocator();
  ResetCommandList();
  SetInitialCommandListState();
}

void D3DCommandListManager::DestroyAllPendingResources()
{
  for (auto& destruction_list : m_deferred_destruction_lists)
  {
    for (auto& resource : destruction_list)
      resource->Release();

    destruction_list.clear();
  }
}

void D3DCommandListManager::ResetAllCommandAllocators()
{
  for (auto& allocator_list : m_command_allocator_lists)
  {
    for (auto& allocator : allocator_list)
      allocator->Reset();
  }

  // Move back to the start, using the first allocator of first list.
  m_current_command_allocator = 0;
  m_current_command_allocator_list = 0;
  m_current_deferred_destruction_list = 0;
}

void D3DCommandListManager::WaitForGPUCompletion()
{
  // Wait for GPU to finish all outstanding work.
  // This method assumes that no command lists are open.
  m_queue_frame_fence_value++;

#ifdef USE_D3D12_QUEUED_COMMAND_LISTS
  m_queued_command_list->QueueFenceGpuSignal(m_queue_frame_fence, m_queue_frame_fence_value);
  m_queued_command_list->ProcessQueuedItems(true);
#else
  CheckHR(m_command_queue->Signal(m_queue_frame_fence, m_queue_frame_fence_value));
#endif

  WaitOnCPUForFence(m_queue_frame_fence, m_queue_frame_fence_value);

  // GPU is up to date with us. Therefore, it has finished with any pending resources.
  DestroyAllPendingResources();

  // Command allocators are also up-to-date, so reset these.
  ResetAllCommandAllocators();
}

void D3DCommandListManager::PerformGPURolloverChecks()
{
  m_queue_frame_fence_value++;

#ifdef USE_D3D12_QUEUED_COMMAND_LISTS
  m_queued_command_list->QueueFenceGpuSignal(m_queue_frame_fence, m_queue_frame_fence_value);
#else
  CheckHR(m_command_queue->Signal(m_queue_frame_fence, m_queue_frame_fence_value));
#endif

  // We now know that the previous 'set' of command lists has completed on GPU, and it is safe to
  // release resources / start back at beginning of command allocator list.

  // Begin Deferred Resource Destruction
  UINT safe_to_delete_deferred_destruction_list =
      (m_current_deferred_destruction_list - 1) % m_deferred_destruction_lists.size();
  WaitOnCPUForFence(m_queue_frame_fence,
                    m_deferred_destruction_list_fences[safe_to_delete_deferred_destruction_list]);

  for (UINT i = 0;
       i < m_deferred_destruction_lists[safe_to_delete_deferred_destruction_list].size(); i++)
  {
    CHECK(m_deferred_destruction_lists[safe_to_delete_deferred_destruction_list][i]->Release() == 0,
          "Resource leak.");
  }

  m_deferred_destruction_lists[safe_to_delete_deferred_destruction_list].clear();

  m_deferred_destruction_list_fences[m_current_deferred_destruction_list] =
      m_queue_frame_fence_value;
  m_current_deferred_destruction_list =
      (m_current_deferred_destruction_list + 1) % m_deferred_destruction_lists.size();
  // End Deferred Resource Destruction

  // Begin Command Allocator Resets
  UINT safe_to_reset_command_allocator_list =
      (m_current_command_allocator_list - 1) % m_command_allocator_lists.size();
  WaitOnCPUForFence(m_queue_frame_fence,
                    m_command_allocator_list_fences[safe_to_reset_command_allocator_list]);

  for (UINT i = 0; i < m_command_allocator_lists[safe_to_reset_command_allocator_list].size(); i++)
  {
    CheckHR(m_command_allocator_lists[safe_to_reset_command_allocator_list][i]->Reset());
  }

  m_command_allocator_list_fences[m_current_command_allocator_list] = m_queue_frame_fence_value;
  m_current_command_allocator_list =
      (m_current_command_allocator_list + 1) % m_command_allocator_lists.size();
  m_current_command_allocator = 0;
  // End Command Allocator Resets
}

void D3DCommandListManager::MoveToNextCommandAllocator()
{
  // Move to the next allocator in the current allocator list.
  m_current_command_allocator = (m_current_command_allocator + 1) %
                                m_command_allocator_lists[m_current_command_allocator_list].size();

  // Did we wrap around? Move to the next set of allocators.
  if (m_current_command_allocator == 0)
    PerformGPURolloverChecks();
}

void D3DCommandListManager::ResetCommandList()
{
#ifdef USE_D3D12_QUEUED_COMMAND_LISTS
  ID3D12QueuedCommandList* command_list = m_queued_command_list;
#else
  ID3D12GraphicsCommandList* command_list = m_backing_command_list;
#endif

  CheckHR(command_list->Reset(m_command_allocator_lists[m_current_command_allocator_list]
                                                       [m_current_command_allocator],
                              nullptr));
}

void D3DCommandListManager::DestroyResourceAfterCurrentCommandListExecuted(ID3D12Resource* resource)
{
  CHECK(resource, "Null resource being inserted!");

  m_deferred_destruction_lists[m_current_deferred_destruction_list].push_back(resource);
}

D3DCommandListManager::~D3DCommandListManager()
{
#ifdef USE_D3D12_QUEUED_COMMAND_LISTS
  // Wait for background thread to exit.
  m_queued_command_list->Release();
#endif

  // The command list will still be open, close it before destroying.
  m_backing_command_list->Close();

  DestroyAllPendingResources();

  m_backing_command_list->Release();

  for (auto& allocator_list : m_command_allocator_lists)
  {
    for (auto& resource : allocator_list)
      resource->Release();
  }

  m_queue_fence->Release();
  m_queue_frame_fence->Release();

  CloseHandle(m_wait_on_cpu_fence_event);
}

void D3DCommandListManager::WaitOnCPUForFence(ID3D12Fence* fence, UINT64 fence_value)
{
  if (fence->GetCompletedValue() >= fence_value)
    return;

  CheckHR(fence->SetEventOnCompletion(fence_value, m_wait_on_cpu_fence_event));
  WaitForSingleObject(m_wait_on_cpu_fence_event, INFINITE);
}

void D3DCommandListManager::SetCommandListDirtyState(unsigned int command_list_state, bool dirty)
{
  if (dirty)
    m_command_list_dirty_state |= command_list_state;
  else
    m_command_list_dirty_state &= ~command_list_state;
}

bool D3DCommandListManager::GetCommandListDirtyState(COMMAND_LIST_STATE command_list_state) const
{
  return ((m_command_list_dirty_state & command_list_state) != 0);
}

void D3DCommandListManager::SetCommandListPrimitiveTopology(
    D3D_PRIMITIVE_TOPOLOGY primitive_topology)
{
  m_command_list_current_topology = primitive_topology;
}

D3D_PRIMITIVE_TOPOLOGY D3DCommandListManager::GetCommandListPrimitiveTopology() const
{
  return m_command_list_current_topology;
}

void D3DCommandListManager::CPUAccessNotify()
{
  m_cpu_access_last_frame = true;
  m_cpu_access_this_frame = true;
  m_draws_since_last_execution = 0;
};

ID3D12Fence*
D3DCommandListManager::RegisterQueueFenceCallback(void* owning_object,
                                                  PFN_QUEUE_FENCE_CALLBACK* callback_function)
{
  m_queue_fence_callbacks[owning_object] = callback_function;

  return m_queue_fence;
}

void D3DCommandListManager::RemoveQueueFenceCallback(void* owning_object)
{
  m_queue_fence_callbacks.erase(owning_object);
}

}  // namespace DX12