#include "dxgi_factory.h"
#include "dxgi_output.h"
#include "dxgi_swapchain.h"

#include "../util/util_misc.h"

#include <d3d12.h>

namespace dxvk {
  
  DxgiSwapChain::DxgiSwapChain(
          DxgiFactory*                pFactory,
          IDXGIVkSwapChain*           pPresenter,
          HWND                        hWnd,
    const DXGI_SWAP_CHAIN_DESC1*      pDesc,
    const DXGI_SWAP_CHAIN_FULLSCREEN_DESC*  pFullscreenDesc,
          IUnknown*                   pDevice)
  : m_factory   (pFactory),
    m_window    (hWnd),
    m_desc      (*pDesc),
    m_descFs    (*pFullscreenDesc),
    m_presentId (0u),
    m_presenter (pPresenter),
    m_monitor   (wsi::getWindowMonitor(m_window)),
    m_is_d3d12(SUCCEEDED(pDevice->QueryInterface(__uuidof(ID3D12CommandQueue), reinterpret_cast<void**>(&Com<ID3D12CommandQueue>())))) {

    if (FAILED(m_presenter->GetAdapter(__uuidof(IDXGIAdapter), reinterpret_cast<void**>(&m_adapter))))
      throw DxvkError("DXGI: Failed to get adapter for present device");

    // Query updated interface versions from presenter, this
    // may fail e.g. with older vkd3d-proton builds.
    m_presenter->QueryInterface(__uuidof(IDXGIVkSwapChain1), reinterpret_cast<void**>(&m_presenter1));
    m_presenter->QueryInterface(__uuidof(IDXGIVkSwapChain2), reinterpret_cast<void**>(&m_presenter2));

    m_frameRateOption = m_factory->GetOptions()->maxFrameRate;

    // Query monitor info form DXVK's DXGI factory, if available
    m_factory->QueryInterface(__uuidof(IDXGIVkMonitorInfo), reinterpret_cast<void**>(&m_monitorInfo));
    
    // Apply initial window mode and fullscreen state
    if (!m_descFs.Windowed && FAILED(EnterFullscreenMode(nullptr)))
      throw DxvkError("DXGI: Failed to set initial fullscreen state");

    // Ensure that RGBA16 swap chains are scRGB if supported
    UpdateColorSpace(m_desc.Format, m_colorSpace);

    // Somewhat hacky way to determine whether to forward the
    // display refresh rate in windowed mode even with a sync
    // interval of 1.
    if (!m_is_d3d12) {
      auto instance = pFactory->GetDXVKInstance();
      m_hasLatencyControl = instance->options().latencySleep == Tristate::True;
    }
  }
  
  
  DxgiSwapChain::~DxgiSwapChain() {
    if (!m_descFs.Windowed)
      RestoreDisplayMode(m_monitor);

    // Decouple swap chain from monitor if necessary
    DXGI_VK_MONITOR_DATA* monitorInfo = nullptr;
    
    if (SUCCEEDED(AcquireMonitorData(m_monitor, &monitorInfo))) {
      if (monitorInfo->pSwapChain == this)
        monitorInfo->pSwapChain = nullptr;
      
      ReleaseMonitorData();
    }
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::QueryInterface(REFIID riid, void** ppvObject) {
    if (ppvObject == nullptr)
      return E_POINTER;

    *ppvObject = nullptr;
    
    if (riid == __uuidof(IUnknown)
     || riid == __uuidof(IDXGIObject)
     || riid == __uuidof(IDXGIDeviceSubObject)
     || riid == __uuidof(IDXGISwapChain)
     || riid == __uuidof(IDXGISwapChain1)
     || riid == __uuidof(IDXGISwapChain2)
     || riid == __uuidof(IDXGISwapChain3)
     || riid == __uuidof(IDXGISwapChain4)) {
      *ppvObject = ref(this);
      return S_OK;
    }
    
    if (logQueryInterfaceError(__uuidof(IDXGISwapChain), riid)) {
      Logger::warn("DxgiSwapChain::QueryInterface: Unknown interface query");
      Logger::warn(str::format(riid));
    }

    return E_NOINTERFACE;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetParent(REFIID riid, void** ppParent) {
    return m_factory->QueryInterface(riid, ppParent);
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetDevice(REFIID riid, void** ppDevice) {
    return m_presenter->GetDevice(riid, ppDevice);
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetBuffer(UINT Buffer, REFIID riid, void** ppSurface) {
    return m_presenter->GetImage(Buffer, riid, ppSurface);
  }


  UINT STDMETHODCALLTYPE DxgiSwapChain::GetCurrentBackBufferIndex() {
    return m_presenter->GetImageIndex();
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetContainingOutput(IDXGIOutput** ppOutput) {
    InitReturnPtr(ppOutput);
    
    if (!wsi::isWindow(m_window))
      return DXGI_ERROR_INVALID_CALL;
    
    Com<IDXGIOutput1> output;

    if (m_target == nullptr) {
      HRESULT hr = GetOutputFromMonitor(wsi::getWindowMonitor(m_window), &output);

      if (FAILED(hr))
        return hr;
    } else {
      output = m_target;
    }

    *ppOutput = output.ref();
    return S_OK;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetDesc(DXGI_SWAP_CHAIN_DESC* pDesc) {
    if (!pDesc)
      return E_INVALIDARG;
    
    pDesc->BufferDesc.Width     = m_desc.Width;
    pDesc->BufferDesc.Height    = m_desc.Height;
    pDesc->BufferDesc.RefreshRate = m_descFs.RefreshRate;
    pDesc->BufferDesc.Format    = m_desc.Format;
    pDesc->BufferDesc.ScanlineOrdering = m_descFs.ScanlineOrdering;
    pDesc->BufferDesc.Scaling   = m_descFs.Scaling;
    pDesc->SampleDesc           = m_desc.SampleDesc;
    pDesc->BufferUsage          = m_desc.BufferUsage;
    pDesc->BufferCount          = m_desc.BufferCount;
    pDesc->OutputWindow         = m_window;
    pDesc->Windowed             = m_descFs.Windowed;
    pDesc->SwapEffect           = m_desc.SwapEffect;
    pDesc->Flags                = m_desc.Flags;
    return S_OK;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetDesc1(DXGI_SWAP_CHAIN_DESC1* pDesc) {
    if (pDesc == nullptr)
      return E_INVALIDARG;
    
    *pDesc = m_desc;
    return S_OK;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetBackgroundColor(
          DXGI_RGBA*                pColor) {
    Logger::err("DxgiSwapChain::GetBackgroundColor: Not implemented");
    return E_NOTIMPL;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetRotation(
          DXGI_MODE_ROTATION*       pRotation) {
    Logger::err("DxgiSwapChain::GetRotation: Not implemented");
    return E_NOTIMPL;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetRestrictToOutput(
          IDXGIOutput**             ppRestrictToOutput) {
    InitReturnPtr(ppRestrictToOutput);
    
    Logger::err("DxgiSwapChain::GetRestrictToOutput: Not implemented");
    return E_NOTIMPL;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetFrameStatistics(DXGI_FRAME_STATISTICS* pStats) {
    std::lock_guard<dxvk::recursive_mutex> lock(m_lockWindow);

    if (!pStats)
      return E_INVALIDARG;

    static bool s_errorShown = false;

    if (!std::exchange(s_errorShown, true))
      Logger::warn("DxgiSwapChain::GetFrameStatistics: Frame statistics may be inaccurate");

    // Populate frame statistics with local present count and current time
    auto t1Counter = dxvk::high_resolution_clock::get_counter();

    DXGI_VK_FRAME_STATISTICS frameStatistics = { };
    frameStatistics.PresentCount = m_presentId;
    frameStatistics.PresentQPCTime = t1Counter;

    if (m_presenter1 != nullptr)
      m_presenter1->GetFrameStatistics(&frameStatistics);

    // Fill in actual DXGI statistics, using monitor data to help compute
    // vblank counts if possible. This is not fully accurate, especially on
    // displays with variable refresh rates, but it's the best we can do.
    DXGI_VK_MONITOR_DATA* monitorData = nullptr;

    pStats->PresentCount          = frameStatistics.PresentCount;
    pStats->PresentRefreshCount   = 0;
    pStats->SyncRefreshCount      = 0;
    pStats->SyncQPCTime.QuadPart  = frameStatistics.PresentQPCTime;
    pStats->SyncGPUTime.QuadPart  = 0;

    if (SUCCEEDED(AcquireMonitorData(m_monitor, &monitorData))) {
      auto refreshPeriod = computeRefreshPeriod(
        monitorData->LastMode.RefreshRate.Numerator,
        monitorData->LastMode.RefreshRate.Denominator);

      auto t0 = dxvk::high_resolution_clock::get_time_from_counter(monitorData->FrameStats.SyncQPCTime.QuadPart);
      auto t1 = dxvk::high_resolution_clock::get_time_from_counter(t1Counter);
      auto t2 = dxvk::high_resolution_clock::get_time_from_counter(frameStatistics.PresentQPCTime);

      pStats->PresentRefreshCount = m_presenter1 != nullptr
        ? monitorData->FrameStats.SyncRefreshCount + computeRefreshCount(t0, t2, refreshPeriod)
        : monitorData->FrameStats.PresentRefreshCount;
      pStats->SyncRefreshCount = monitorData->FrameStats.SyncRefreshCount + computeRefreshCount(t0, t1, refreshPeriod);

      ReleaseMonitorData();
    }

    // Docs say that DISJOINT is returned on the first call and around
    // mode changes. Just make this swap chain state for now.
    HRESULT hr = S_OK;

    if (std::exchange(m_frameStatisticsDisjoint, false))
      hr = DXGI_ERROR_FRAME_STATISTICS_DISJOINT;

    return hr;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetFullscreenState(
          BOOL*         pFullscreen,
          IDXGIOutput** ppTarget) {
    HRESULT hr = S_OK;

    if (!m_is_d3d12 && !m_descFs.Windowed && wsi::isOccluded(m_window))
      SetFullscreenState(FALSE, nullptr);
    if (pFullscreen != nullptr)
      *pFullscreen = !m_descFs.Windowed;
    
    if (ppTarget != nullptr)
      *ppTarget = m_target.ref();

    return hr;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetFullscreenDesc(
          DXGI_SWAP_CHAIN_FULLSCREEN_DESC* pDesc) {
    if (pDesc == nullptr)
      return E_INVALIDARG;
    
    *pDesc = m_descFs;
    return S_OK;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetHwnd(
          HWND*                     pHwnd) {
    if (pHwnd == nullptr)
      return E_INVALIDARG;
    
    *pHwnd = m_window;
    return S_OK;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetCoreWindow(
          REFIID                    refiid,
          void**                    ppUnk) {
    InitReturnPtr(ppUnk);
    
    Logger::err("DxgiSwapChain::GetCoreWindow: Not implemented");
    return E_NOTIMPL;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetLastPresentCount(UINT* pLastPresentCount) {
    if (pLastPresentCount == nullptr)
      return E_INVALIDARG;

    UINT64 presentId = m_presentId;

    if (m_presenter1 != nullptr)
      m_presenter1->GetLastPresentCount(&presentId);

    *pLastPresentCount = UINT(presentId);
    return S_OK;
  }
  
  
  BOOL STDMETHODCALLTYPE DxgiSwapChain::IsTemporaryMonoSupported() {
    // This seems to be related to stereo 3D display
    // modes, which we don't support at the moment
    return FALSE;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::Present(UINT SyncInterval, UINT Flags) {
    return PresentBase(SyncInterval, Flags, nullptr);
  }

  HRESULT STDMETHODCALLTYPE DxgiSwapChain::Present1(
          UINT                      SyncInterval,
          UINT                      PresentFlags,
    const DXGI_PRESENT_PARAMETERS*  pPresentParameters) {

    return PresentBase(SyncInterval, PresentFlags, pPresentParameters);
  }

  HRESULT STDMETHODCALLTYPE DxgiSwapChain::PresentBase(
          UINT                      SyncInterval,
          UINT                      PresentFlags,
    const DXGI_PRESENT_PARAMETERS*  pPresentParameters) {

    if (SyncInterval > 4)
      return DXGI_ERROR_INVALID_CALL;

    if ((m_desc.SwapEffect == DXGI_SWAP_EFFECT_DISCARD || m_desc.SwapEffect == DXGI_SWAP_EFFECT_SEQUENTIAL) && wsi::isMinimized(m_window))
      return DXGI_STATUS_OCCLUDED;
    bool occluded = !m_descFs.Windowed && wsi::isOccluded(m_window) && !wsi::isMinimized(m_window);

    auto options = m_factory->GetOptions();

    if (options->syncInterval >= 0)
      SyncInterval = options->syncInterval;

    UpdateGlobalHDRState();
    UpdateTargetFrameRate(SyncInterval);

    std::lock_guard<dxvk::recursive_mutex> lockWin(m_lockWindow);
    HRESULT hr = S_OK;

    if (wsi::isWindow(m_window)) {
      std::lock_guard<dxvk::mutex> lockBuf(m_lockBuffer);
      hr = m_presenter->Present(SyncInterval, PresentFlags, nullptr);
    }

    if (PresentFlags & DXGI_PRESENT_TEST)
      return hr == S_OK && occluded ? DXGI_STATUS_OCCLUDED : hr;

    if (hr == S_OK) {

      m_presentId += 1;

      // Update monitor frame statistics. This is not consistent with swap chain
      // frame statistics at all, but we want to ensure that all presents become
      // visible to the IDXGIOutput in case applications rely on that behaviour.
      DXGI_VK_MONITOR_DATA* monitorData = nullptr;

      if (SUCCEEDED(AcquireMonitorData(m_monitor, &monitorData))) {
        auto refreshPeriod = computeRefreshPeriod(
          monitorData->LastMode.RefreshRate.Numerator,
          monitorData->LastMode.RefreshRate.Denominator);

        auto t0 = dxvk::high_resolution_clock::get_time_from_counter(monitorData->FrameStats.SyncQPCTime.QuadPart);
        auto t1 = dxvk::high_resolution_clock::now();

        monitorData->FrameStats.PresentCount += 1;
        monitorData->FrameStats.PresentRefreshCount = monitorData->FrameStats.SyncRefreshCount + computeRefreshCount(t0, t1, refreshPeriod);
        ReleaseMonitorData();
      }
      if (occluded) {
        if (!(PresentFlags & DXGI_PRESENT_TEST))
          SetFullscreenState(FALSE, nullptr);
        hr = DXGI_STATUS_OCCLUDED;
      }
    }

    return hr;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::ResizeBuffers(
          UINT                      BufferCount,
          UINT                      Width,
          UINT                      Height,
          DXGI_FORMAT               NewFormat,
          UINT                      SwapChainFlags) {
    return ResizeBuffers1(BufferCount, Width, Height,
      NewFormat, SwapChainFlags, nullptr, nullptr);
  }


  HRESULT STDMETHODCALLTYPE DxgiSwapChain::ResizeBuffers1(
          UINT                      BufferCount,
          UINT                      Width,
          UINT                      Height,
          DXGI_FORMAT               Format,
          UINT                      SwapChainFlags,
    const UINT*                     pCreationNodeMask,
          IUnknown* const*          ppPresentQueue) {
    if (!wsi::isWindow(m_window))
      return DXGI_ERROR_INVALID_CALL;

    constexpr UINT PreserveFlags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT;

    if ((m_desc.Flags & PreserveFlags) != (SwapChainFlags & PreserveFlags))
      return DXGI_ERROR_INVALID_CALL;
    
    std::lock_guard<dxvk::mutex> lock(m_lockBuffer);
    m_desc.Width  = Width;
    m_desc.Height = Height;
    
    wsi::getWindowSize(m_window,
      m_desc.Width  ? nullptr : &m_desc.Width,
      m_desc.Height ? nullptr : &m_desc.Height);
    
    if (BufferCount != 0)
      m_desc.BufferCount = BufferCount;
    
    if (Format != DXGI_FORMAT_UNKNOWN)
      m_desc.Format = Format;
    
    HRESULT hr = m_presenter->ChangeProperties(&m_desc, pCreationNodeMask, ppPresentQueue);

    if (FAILED(hr))
      return hr;

    UpdateColorSpace(m_desc.Format, m_colorSpace);
    return hr;
  }


  HRESULT STDMETHODCALLTYPE DxgiSwapChain::ResizeTarget(const DXGI_MODE_DESC* pNewTargetParameters) {
    std::lock_guard<dxvk::recursive_mutex> lock(m_lockWindow);

    if (!pNewTargetParameters)
      return DXGI_ERROR_INVALID_CALL;
    
    if (!wsi::isWindow(m_window))
      return DXGI_ERROR_INVALID_CALL;

    // Promote display mode
    DXGI_MODE_DESC1 newDisplayMode = { };
    newDisplayMode.Width = pNewTargetParameters->Width;
    newDisplayMode.Height = pNewTargetParameters->Height;
    newDisplayMode.RefreshRate = pNewTargetParameters->RefreshRate;
    newDisplayMode.Format = pNewTargetParameters->Format;
    newDisplayMode.ScanlineOrdering = pNewTargetParameters->ScanlineOrdering;
    newDisplayMode.Scaling = pNewTargetParameters->Scaling;

    // Update the swap chain description
    if (newDisplayMode.RefreshRate.Numerator != 0)
      m_descFs.RefreshRate = newDisplayMode.RefreshRate;
    
    m_descFs.ScanlineOrdering = newDisplayMode.ScanlineOrdering;
    m_descFs.Scaling          = newDisplayMode.Scaling;
    
    if (m_descFs.Windowed) {
      wsi::resizeWindow(
        m_window, &m_windowState,
        newDisplayMode.Width,
        newDisplayMode.Height);
    } else {
      Com<IDXGIOutput1> output;
      
      if (FAILED(GetOutputFromMonitor(m_monitor, &output))) {
        Logger::err("DXGI: ResizeTarget: Failed to query containing output");
        return E_FAIL;
      }
      
      ChangeDisplayMode(output.ptr(), &newDisplayMode);

      wsi::updateFullscreenWindow(m_monitor, m_window, false);
    }
    
    return S_OK;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::SetFullscreenState(
          BOOL          Fullscreen,
          IDXGIOutput*  pTarget) {
    std::lock_guard<dxvk::recursive_mutex> lock(m_lockWindow);

    if (!Fullscreen && pTarget)
      return DXGI_ERROR_INVALID_CALL;

    Com<IDXGIOutput1> target;

    if (pTarget) {
      DXGI_OUTPUT_DESC desc;

      pTarget->QueryInterface(IID_PPV_ARGS(&target));
      target->GetDesc(&desc);

      if (!m_descFs.Windowed && Fullscreen && m_monitor != desc.Monitor) {
        HRESULT hr = this->LeaveFullscreenMode();
        if (FAILED(hr))
          return hr;
      }
    }

    if (m_descFs.Windowed && Fullscreen)
      return this->EnterFullscreenMode(target.ptr());
    else if (!m_descFs.Windowed && !Fullscreen)
      return this->LeaveFullscreenMode();
    
    return S_OK;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::SetBackgroundColor(
    const DXGI_RGBA*                pColor) {
    Logger::err("DxgiSwapChain::SetBackgroundColor: Not implemented");
    return E_NOTIMPL;
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::SetRotation(
          DXGI_MODE_ROTATION        Rotation) {
    Logger::err("DxgiSwapChain::SetRotation: Not implemented");
    return E_NOTIMPL;
  }
  
  
  HANDLE STDMETHODCALLTYPE DxgiSwapChain::GetFrameLatencyWaitableObject() {
    if (!(m_desc.Flags & DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT))
      return nullptr;

    return m_presenter->GetFrameLatencyEvent();
  }


  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetMatrixTransform(
          DXGI_MATRIX_3X2_F*        pMatrix) {
    // We don't support composition swap chains
    Logger::err("DxgiSwapChain::GetMatrixTransform: Not supported");
    return DXGI_ERROR_INVALID_CALL;
  }

  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetMaximumFrameLatency(
          UINT*                     pMaxLatency) {
    if (!(m_desc.Flags & DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT))
      return DXGI_ERROR_INVALID_CALL;

    std::lock_guard<dxvk::recursive_mutex> lock(m_lockWindow);
    *pMaxLatency = m_presenter->GetFrameLatency();
    return S_OK;
  }

  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::GetSourceSize(
          UINT*                     pWidth,
          UINT*                     pHeight) {
    // TODO implement properly once supported
    if (pWidth)  *pWidth  = m_desc.Width;
    if (pHeight) *pHeight = m_desc.Height;
    return S_OK;
  }

  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::SetMatrixTransform(
    const DXGI_MATRIX_3X2_F*        pMatrix) {
    // We don't support composition swap chains
    Logger::err("DxgiSwapChain::SetMatrixTransform: Not supported");
    return DXGI_ERROR_INVALID_CALL;
  }

  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::SetMaximumFrameLatency(
          UINT                      MaxLatency) {
    if (!(m_desc.Flags & DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT))
      return DXGI_ERROR_INVALID_CALL;

    std::lock_guard<dxvk::recursive_mutex> lock(m_lockWindow);
    return m_presenter->SetFrameLatency(MaxLatency);
  }


  HRESULT STDMETHODCALLTYPE DxgiSwapChain::SetSourceSize(
          UINT                      Width,
          UINT                      Height) {
    if (Width  == 0 || Width  > m_desc.Width
     || Height == 0 || Height > m_desc.Height)
      return E_INVALIDARG;

    std::lock_guard<dxvk::mutex> lock(m_lockBuffer);

    RECT region = { 0, 0, LONG(Width), LONG(Height) };
    return m_presenter->SetPresentRegion(&region);
  }
  

  HRESULT STDMETHODCALLTYPE DxgiSwapChain::CheckColorSpaceSupport(
          DXGI_COLOR_SPACE_TYPE           ColorSpace,
          UINT*                           pColorSpaceSupport) {
    if (!pColorSpaceSupport)
      return E_INVALIDARG;

    std::lock_guard<dxvk::mutex> lock(m_lockBuffer);

    if (ValidateColorSpaceSupport(m_desc.Format, ColorSpace))
      *pColorSpaceSupport = DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT;
    else
      *pColorSpaceSupport = 0;

    return S_OK;
  }


  HRESULT STDMETHODCALLTYPE DxgiSwapChain::SetColorSpace1(DXGI_COLOR_SPACE_TYPE ColorSpace) {
    std::lock_guard<dxvk::mutex> lock(m_lockBuffer);

    if (!ValidateColorSpaceSupport(m_desc.Format, ColorSpace))
      return E_INVALIDARG;

    // Write back color space if setting it up succeeded. This way, we preserve
    // the current color space even if the swap chain temporarily switches to a
    // back buffer format which does not support it.
    HRESULT hr = UpdateColorSpace(m_desc.Format, ColorSpace);

    if (SUCCEEDED(hr))
      m_colorSpace = ColorSpace;

    return hr;
  }

  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::SetHDRMetaData(
          DXGI_HDR_METADATA_TYPE    Type,
          UINT                      Size,
          void*                     pMetaData) {
    if (Size && !pMetaData)
      return E_INVALIDARG;

    DXGI_VK_HDR_METADATA metadata = { Type };

    switch (Type) {
      case DXGI_HDR_METADATA_TYPE_NONE:
        break;

      case DXGI_HDR_METADATA_TYPE_HDR10:
        if (Size != sizeof(DXGI_HDR_METADATA_HDR10))
          return E_INVALIDARG;

        metadata.HDR10 = *static_cast<const DXGI_HDR_METADATA_HDR10*>(pMetaData);
        break;

      default:
        Logger::err(str::format("DXGI: Unsupported HDR metadata type: ", Type));
        return E_INVALIDARG;
    }

    std::lock_guard<dxvk::mutex> lock(m_lockBuffer);
    return m_presenter->SetHDRMetaData(&metadata);
  }
  
  
  HRESULT STDMETHODCALLTYPE DxgiSwapChain::SetGammaControl(
          UINT                      NumPoints,
    const DXGI_RGB*                 pGammaCurve) {
    std::lock_guard<dxvk::mutex> lockBuf(m_lockBuffer);
    return m_presenter->SetGammaControl(NumPoints, pGammaCurve);
  }


  HRESULT DxgiSwapChain::EnterFullscreenMode(IDXGIOutput1* pTarget) {
    if (m_ModeChangeInProgress) {
      Logger::warn("Nested EnterFullscreenMode");
      return DXGI_STATUS_MODE_CHANGE_IN_PROGRESS;
    }
    scoped_bool in_progress(m_ModeChangeInProgress);

    Com<IDXGIOutput1> output = pTarget;

    if (!wsi::isWindow(m_window))
      return DXGI_ERROR_NOT_CURRENTLY_AVAILABLE;
    
    if (output == nullptr) {
      if (FAILED(GetOutputFromMonitor(wsi::getWindowMonitor(m_window), &output))) {
        Logger::err("DXGI: EnterFullscreenMode: Cannot query containing output");
        return E_FAIL;
      }
    }

    DXGI_MODE_DESC1 displayMode = { };
    displayMode.Width            = m_desc.Width;
    displayMode.Height           = m_desc.Height;
    displayMode.RefreshRate      = m_descFs.RefreshRate;
    displayMode.Format           = m_desc.Format;
    // Ignore these two, games usually use them wrong and we don't
    // support any scaling modes except UNSPECIFIED anyway.
    displayMode.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    displayMode.Scaling          = DXGI_MODE_SCALING_UNSPECIFIED;
    
    if (FAILED(ChangeDisplayMode(output.ptr(), &displayMode))) {
      Logger::err("DXGI: EnterFullscreenMode: Failed to change display mode");
      return DXGI_ERROR_NOT_CURRENTLY_AVAILABLE;
    }
    
    // Update swap chain description
    m_descFs.Windowed = FALSE;
    
    // Move the window so that it covers the entire output
    bool modeSwitch = (m_desc.Flags & DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH) != 0u;

    DXGI_OUTPUT_DESC desc;
    output->GetDesc(&desc);

    if (!wsi::enterFullscreenMode(desc.Monitor, m_window, &m_windowState, modeSwitch)) {
      Logger::err("DXGI: EnterFullscreenMode: Failed to enter fullscreen mode");
      return DXGI_ERROR_NOT_CURRENTLY_AVAILABLE;
    }
    
    m_monitor = desc.Monitor;
    m_target  = std::move(output);

    // Apply current gamma curve of the output
    DXGI_VK_MONITOR_DATA* monitorInfo = nullptr;

    if (SUCCEEDED(AcquireMonitorData(m_monitor, &monitorInfo))) {
      if (!monitorInfo->pSwapChain)
        monitorInfo->pSwapChain = this;
      
      SetGammaControl(DXGI_VK_GAMMA_CP_COUNT, monitorInfo->GammaCurve.GammaCurve);
      ReleaseMonitorData();
    }

    return S_OK;
  }
  
  
  HRESULT DxgiSwapChain::LeaveFullscreenMode() {
    if (m_ModeChangeInProgress) {
      Logger::warn("Nested LeaveFullscreenMode");
      return DXGI_STATUS_MODE_CHANGE_IN_PROGRESS;
    }
    scoped_bool in_progress(m_ModeChangeInProgress);

    if (FAILED(RestoreDisplayMode(m_monitor)))
      Logger::warn("DXGI: LeaveFullscreenMode: Failed to restore display mode");
    
    // Reset gamma control and decouple swap chain from monitor
    DXGI_VK_MONITOR_DATA* monitorInfo = nullptr;

    if (SUCCEEDED(AcquireMonitorData(m_monitor, &monitorInfo))) {
      if (monitorInfo->pSwapChain == this)
        monitorInfo->pSwapChain = nullptr;
      
      SetGammaControl(0, nullptr);
      ReleaseMonitorData();
    }

    // Restore internal state
    m_descFs.Windowed = TRUE;
    m_target  = nullptr;
    m_monitor = wsi::getWindowMonitor(m_window);
    
    if (!wsi::isWindow(m_window))
      return S_OK;
    
    if (!wsi::leaveFullscreenMode(m_window, &m_windowState, true)) {
      Logger::err("DXGI: LeaveFullscreenMode: Failed to exit fullscreen mode");
      return DXGI_ERROR_NOT_CURRENTLY_AVAILABLE;
    }
    
    return S_OK;
  }
  
  
  HRESULT DxgiSwapChain::ChangeDisplayMode(
          IDXGIOutput1*           pOutput,
    const DXGI_MODE_DESC1*        pDisplayMode) {
    if (!pOutput)
      return DXGI_ERROR_INVALID_CALL;
    
    // Find a mode that the output supports
    DXGI_OUTPUT_DESC outputDesc;
    pOutput->GetDesc(&outputDesc);
    
    DXGI_MODE_DESC1 preferredMode = *pDisplayMode;
    DXGI_MODE_DESC1 selectedMode = { };

    if (!(m_desc.Flags & DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH)) {
      preferredMode.Width = 0;
      preferredMode.Height = 0;
    }

    if (preferredMode.Format == DXGI_FORMAT_UNKNOWN)
      preferredMode.Format = m_desc.Format;
    
    HRESULT hr = pOutput->FindClosestMatchingMode1(
      &preferredMode, &selectedMode, nullptr);

    if (FAILED(hr)) {
      Logger::err(str::format(
        "DXGI: Failed to query closest mode:",
        "\n  Format: ", preferredMode.Format,
        "\n  Mode:   ", preferredMode.Width, "x", preferredMode.Height,
          "@", preferredMode.RefreshRate.Numerator / std::max(preferredMode.RefreshRate.Denominator, 1u)));
      return hr;
    }

    if (!selectedMode.RefreshRate.Denominator)
      selectedMode.RefreshRate.Denominator = 1;

    if (!wsi::setWindowMode(outputDesc.Monitor, m_window, &m_windowState, ConvertDisplayMode(selectedMode)))
      return DXGI_ERROR_NOT_CURRENTLY_AVAILABLE;

    DXGI_VK_MONITOR_DATA* monitorData = nullptr;

    if (SUCCEEDED(AcquireMonitorData(outputDesc.Monitor, &monitorData))) {
      auto refreshPeriod = computeRefreshPeriod(
        monitorData->LastMode.RefreshRate.Numerator,
        monitorData->LastMode.RefreshRate.Denominator);

      auto t1Counter = dxvk::high_resolution_clock::get_counter();

      auto t0 = dxvk::high_resolution_clock::get_time_from_counter(monitorData->FrameStats.SyncQPCTime.QuadPart);
      auto t1 = dxvk::high_resolution_clock::get_time_from_counter(t1Counter);

      monitorData->FrameStats.SyncRefreshCount += computeRefreshCount(t0, t1, refreshPeriod);
      monitorData->FrameStats.SyncQPCTime.QuadPart = t1Counter;
      monitorData->LastMode = selectedMode;
      ReleaseMonitorData();
    }

    m_frameRateRefresh = double(selectedMode.RefreshRate.Numerator)
                       / double(selectedMode.RefreshRate.Denominator);
    return S_OK;
  }
  
  
  HRESULT DxgiSwapChain::RestoreDisplayMode(HMONITOR hMonitor) {
    if (!hMonitor)
      return DXGI_ERROR_INVALID_CALL;
    
    if (!wsi::restoreDisplayMode())
      return DXGI_ERROR_NOT_CURRENTLY_AVAILABLE;

    m_frameRateRefresh = 0.0;
    return S_OK;
  }
  
  
  HRESULT DxgiSwapChain::GetSampleCount(UINT Count, VkSampleCountFlagBits* pCount) const {
    switch (Count) {
      case  1: *pCount = VK_SAMPLE_COUNT_1_BIT;  return S_OK;
      case  2: *pCount = VK_SAMPLE_COUNT_2_BIT;  return S_OK;
      case  4: *pCount = VK_SAMPLE_COUNT_4_BIT;  return S_OK;
      case  8: *pCount = VK_SAMPLE_COUNT_8_BIT;  return S_OK;
      case 16: *pCount = VK_SAMPLE_COUNT_16_BIT; return S_OK;
    }
    
    return E_INVALIDARG;
  }


  HRESULT DxgiSwapChain::GetOutputFromMonitor(
          HMONITOR                  Monitor,
          IDXGIOutput1**            ppOutput) {
    if (!ppOutput)
      return DXGI_ERROR_INVALID_CALL;

    Com<IDXGIOutput> output;

    for (uint32_t i = 0; SUCCEEDED(m_adapter->EnumOutputs(i, &output)); i++) {
      DXGI_OUTPUT_DESC outputDesc;
      output->GetDesc(&outputDesc);
      
      if (outputDesc.Monitor == Monitor)
        return output->QueryInterface(IID_PPV_ARGS(ppOutput));
      
      output = nullptr;
    }
    
    return DXGI_ERROR_NOT_FOUND;
  }


  HRESULT DxgiSwapChain::AcquireMonitorData(
          HMONITOR                hMonitor,
          DXGI_VK_MONITOR_DATA**  ppData) {
    if (m_monitorInfo == nullptr || !hMonitor)
      return E_NOINTERFACE;

    HRESULT hr = m_monitorInfo->AcquireMonitorData(hMonitor, ppData);

    if (FAILED(hr) && HasLiveReferences()) {
      // We may need to initialize a DXGI output to populate monitor data.
      // If acquiring monitor data has failed previously, do not try again.
      if (hMonitor == m_monitor && !m_monitorHasOutput)
        return E_NOINTERFACE;

      Com<IDXGIOutput1> output;

      if (SUCCEEDED(GetOutputFromMonitor(hMonitor, &output)))
        hr = m_monitorInfo->AcquireMonitorData(hMonitor, ppData);
    }

    if (hMonitor == m_monitor)
      m_monitorHasOutput = SUCCEEDED(hr);

    return hr;
  }

  
  void DxgiSwapChain::ReleaseMonitorData() {
    if (m_monitorInfo != nullptr)
      m_monitorInfo->ReleaseMonitorData();
  }


  void DxgiSwapChain::UpdateGlobalHDRState() {
    // Update the global HDR state if called from the legacy NVAPI
    // interfaces, etc.

    auto state = m_factory->GlobalHDRState();
    if (m_globalHDRStateSerial != state.Serial) {
      SetColorSpace1(state.ColorSpace);

      switch (state.Metadata.Type) {
        case DXGI_HDR_METADATA_TYPE_NONE:
          SetHDRMetaData(DXGI_HDR_METADATA_TYPE_NONE, 0, nullptr);
          break;
        case DXGI_HDR_METADATA_TYPE_HDR10:
          SetHDRMetaData(DXGI_HDR_METADATA_TYPE_HDR10, sizeof(state.Metadata.HDR10), reinterpret_cast<void*>(&state.Metadata.HDR10));
          break;
        default:
          Logger::err(str::format("DXGI: Unsupported HDR metadata type (global): ", state.Metadata.Type));
          break;
      }

      m_globalHDRStateSerial = state.Serial;
    }
  }


  bool DxgiSwapChain::ValidateColorSpaceSupport(
          DXGI_FORMAT             Format,
          DXGI_COLOR_SPACE_TYPE   ColorSpace) {
    // RGBA16 swap chains are treated as scRGB even on SDR displays,
    // and regular sRGB is not exposed when this format is used.
    if (Format == DXGI_FORMAT_R16G16B16A16_FLOAT)
      return ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709;

    // For everything else, we will always expose plain sRGB
    if (ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709)
      return true;

    // Only expose HDR10 color space if HDR option is enabled
    if (ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020)
      return m_factory->GetOptions()->enableHDR && m_presenter->CheckColorSpaceSupport(ColorSpace);

    return false;
  }


  HRESULT DxgiSwapChain::UpdateColorSpace(
          DXGI_FORMAT             Format,
          DXGI_COLOR_SPACE_TYPE   ColorSpace) {
    // Don't do anything if the explicitly sepected color space
    // is compatible with the back buffer format already
    if (!ValidateColorSpaceSupport(Format, ColorSpace)) {
      ColorSpace = Format == DXGI_FORMAT_R16G16B16A16_FLOAT
        ? DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709
        : DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
    }

    // Ensure that we pick a supported color space. This is relevant for
    // mapping scRGB to sRGB on SDR setups, matching Windows behaviour.
    if (!m_presenter->CheckColorSpaceSupport(ColorSpace))
      ColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;

    HRESULT hr = m_presenter->SetColorSpace(ColorSpace);

    // If this was a colorspace other than our current one,
    // punt us into that one on the DXGI output.
    if (SUCCEEDED(hr))
      m_monitorInfo->PuntColorSpace(ColorSpace);

    return hr;
  }


  void DxgiSwapChain::UpdateTargetFrameRate(
          UINT                    SyncInterval) {
    if (m_presenter2 == nullptr)
      return;

    // Engage the frame limiter with large sync intervals even in windowed
    // mode since we want to avoid double-presenting to the swap chain.
    if (SyncInterval != m_frameRateSyncInterval && m_descFs.Windowed) {
      bool engageLimiter = (SyncInterval > 1u) || (SyncInterval && m_hasLatencyControl);

      m_frameRateSyncInterval = SyncInterval;
      m_frameRateRefresh = 0.0f;

      if (engageLimiter && wsi::isWindow(m_window)) {
        wsi::WsiMode mode = { };

        if (wsi::getCurrentDisplayMode(wsi::getWindowMonitor(m_window), &mode)) {
          if (mode.refreshRate.numerator && mode.refreshRate.denominator) {
            m_frameRateRefresh = double(mode.refreshRate.numerator)
                               / double(mode.refreshRate.denominator);
          }
        }
      }
    } else if (!m_descFs.Windowed) {
      // Reset tracking when in fullscreen mode
      m_frameRateSyncInterval = 0;
    }

    // Use a negative number to indicate that the limiter should only
    // be engaged if the target frame rate is actually exceeded
    double frameRate = std::max(m_frameRateOption, 0.0);

    if (SyncInterval && m_frameRateOption == 0.0)
      frameRate = -m_frameRateRefresh / double(SyncInterval);

    if (m_frameRateLimit != frameRate) {
      m_frameRateLimit = frameRate;
      m_presenter2->SetTargetFrameRate(frameRate);
    }
  }

}