From 43838d3df8c1e5e1d3ec1f69413598b7ff296c6e Mon Sep 17 00:00:00 2001
From: Philip Rebohle <philip.rebohle@tu-dortmund.de>
Date: Sun, 12 Jan 2025 23:26:56 +0100
Subject: [PATCH] [dxvk] Move Vulkan swapchain management to backend

Massive cleanup reduce code duplication between D3D11 and D3D9,
and introduce a sane path to pass data around. Implicit swap
chain recreation is now entirely transparent to the frontends.
---
 src/d3d11/d3d11_swapchain.cpp | 152 ++++--------
 src/d3d11/d3d11_swapchain.h   |  13 +-
 src/d3d9/d3d9_swapchain.cpp   | 165 +++++--------
 src/d3d9/d3d9_swapchain.h     |  16 +-
 src/dxvk/dxvk_device.cpp      |   2 -
 src/dxvk/dxvk_device.h        |   2 -
 src/dxvk/dxvk_presenter.cpp   | 445 +++++++++++++++++++++++-----------
 src/dxvk/dxvk_presenter.h     | 165 +++++++------
 src/dxvk/dxvk_queue.cpp       |   5 +-
 src/dxvk/dxvk_queue.h         |   1 -
 10 files changed, 514 insertions(+), 452 deletions(-)

diff --git a/src/d3d11/d3d11_swapchain.cpp b/src/d3d11/d3d11_swapchain.cpp
index 2fc84ab49..df3e99879 100644
--- a/src/d3d11/d3d11_swapchain.cpp
+++ b/src/d3d11/d3d11_swapchain.cpp
@@ -174,11 +174,11 @@ namespace dxvk {
     const DXGI_SWAP_CHAIN_DESC1*    pDesc,
     const UINT*                     pNodeMasks,
           IUnknown* const*          ppPresentQueues) {
-    m_dirty |= m_desc.Format      != pDesc->Format
-            || m_desc.Width       != pDesc->Width
-            || m_desc.Height      != pDesc->Height
-            || m_desc.BufferCount != pDesc->BufferCount
-            || m_desc.Flags       != pDesc->Flags;
+    if (m_desc.Format != pDesc->Format)
+      m_presenter->setSurfaceFormat(GetSurfaceFormat(pDesc->Format));
+
+    if (m_desc.Width != pDesc->Width || m_desc.Height != pDesc->Height)
+      m_presenter->setSurfaceExtent({ m_desc.Width, m_desc.Height });
 
     m_desc = *pDesc;
     CreateBackBuffers();
@@ -251,33 +251,32 @@ namespace dxvk {
           UINT                      SyncInterval,
           UINT                      PresentFlags,
     const DXGI_PRESENT_PARAMETERS*  pPresentParameters) {
-    if (!(PresentFlags & DXGI_PRESENT_TEST))
-      m_dirty |= m_presenter->setSyncInterval(SyncInterval) != VK_SUCCESS;
-
     HRESULT hr = S_OK;
 
-    if (!m_presenter->hasSwapChain()) {
-      RecreateSwapChain();
-      m_dirty = false;
-    }
-
-    if (!m_presenter->hasSwapChain())
-      hr = DXGI_STATUS_OCCLUDED;
-
     if (m_device->getDeviceStatus() != VK_SUCCESS)
       hr = DXGI_ERROR_DEVICE_RESET;
 
-    if (PresentFlags & DXGI_PRESENT_TEST)
-      return hr;
+    if (PresentFlags & DXGI_PRESENT_TEST) {
+      if (hr != S_OK)
+        return hr;
+
+      // If the current present status is NOT_READY, we have a present
+      // in flight, which means that we can most likely present again.
+      // This avoids an expensive sync point.
+      VkResult status = m_presentStatus.result.load();
+
+      if (status == VK_NOT_READY)
+        return S_OK;
+
+      status = m_presenter->checkSwapChainStatus();
+      return status == VK_SUCCESS ? S_OK : DXGI_STATUS_OCCLUDED;
+    }
 
     if (hr != S_OK) {
       SyncFrameLatency();
       return hr;
     }
 
-    if (std::exchange(m_dirty, false))
-      RecreateSwapChain();
-
     try {
       hr = PresentImage(SyncInterval);
     } catch (const DxvkError& e) {
@@ -298,7 +297,8 @@ namespace dxvk {
           DXGI_COLOR_SPACE_TYPE     ColorSpace) {
     UINT supportFlags = 0;
 
-    const VkColorSpaceKHR vkColorSpace = ConvertColorSpace(ColorSpace);
+    VkColorSpaceKHR vkColorSpace = ConvertColorSpace(ColorSpace);
+
     if (m_presenter->supportsColorSpace(vkColorSpace))
       supportFlags |= DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT;
 
@@ -308,13 +308,14 @@ namespace dxvk {
 
   HRESULT STDMETHODCALLTYPE D3D11SwapChain::SetColorSpace(
           DXGI_COLOR_SPACE_TYPE     ColorSpace) {
-    if (!(CheckColorSpaceSupport(ColorSpace) & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT))
+    VkColorSpaceKHR colorSpace = ConvertColorSpace(ColorSpace);
+
+    if (!m_presenter->supportsColorSpace(colorSpace))
       return E_INVALIDARG;
 
-    const VkColorSpaceKHR vkColorSpace = ConvertColorSpace(ColorSpace);
-    m_dirty |= vkColorSpace != m_colorspace;
-    m_colorspace = vkColorSpace;
+    m_colorSpace = colorSpace;
 
+    m_presenter->setSurfaceFormat(GetSurfaceFormat(m_desc.Format));
     return S_OK;
   }
 
@@ -378,27 +379,19 @@ namespace dxvk {
 
     SynchronizePresent();
 
-    if (!m_presenter->hasSwapChain())
-      return DXGI_STATUS_OCCLUDED;
+    m_presenter->setSyncInterval(SyncInterval);
 
     // Presentation semaphores and WSI swap chain image
     PresenterSync sync;
-
     Rc<DxvkImage> backBuffer;
 
     VkResult status = m_presenter->acquireNextImage(sync, backBuffer);
 
-    while (status != VK_SUCCESS) {
-      RecreateSwapChain();
+    if (status < 0)
+      return E_FAIL;
 
-      if (!m_presenter->hasSwapChain())
-        return DXGI_STATUS_OCCLUDED;
-      
-      status = m_presenter->acquireNextImage(sync, backBuffer);
-
-      if (status == VK_SUBOPTIMAL_KHR)
-        break;
-    }
+    if (status == VK_NOT_READY)
+      return DXGI_STATUS_OCCLUDED;
 
     m_frameId += 1;
 
@@ -425,7 +418,7 @@ namespace dxvk {
       cSync           = sync,
       cHud            = m_hud,
       cPresenter      = m_presenter,
-      cColorSpace     = m_colorspace,
+      cColorSpace     = m_colorSpace,
       cFrameId        = m_frameId
     ] (DxvkContext* ctx) {
       // Blit the D3D back buffer onto the actual Vulkan
@@ -448,7 +441,6 @@ namespace dxvk {
       ctx->flushCommandList(nullptr);
 
       cDevice->presentImage(cPresenter,
-        cPresenter->info().presentMode,
         cFrameId, cPresentStatus);
     });
 
@@ -480,30 +472,7 @@ namespace dxvk {
 
 
   void D3D11SwapChain::SynchronizePresent() {
-    // Recreate swap chain if the previous present call failed
-    VkResult status = m_device->waitForSubmission(&m_presentStatus);
-    
-    if (status != VK_SUCCESS)
-      RecreateSwapChain();
-  }
-
-
-  void D3D11SwapChain::RecreateSwapChain() {
-    // Ensure that we can safely destroy the swap chain
     m_device->waitForSubmission(&m_presentStatus);
-    m_device->waitForIdle();
-
-    m_presentStatus.result = VK_SUCCESS;
-
-    PresenterDesc presenterDesc;
-    presenterDesc.imageExtent     = { m_desc.Width, m_desc.Height };
-    presenterDesc.imageCount      = PickImageCount(m_desc.BufferCount + 1);
-    presenterDesc.numFormats      = PickFormats(m_desc.Format, presenterDesc.formats);
-
-    VkResult vr = m_presenter->recreateSwapChain(presenterDesc);
-
-    if (vr)
-      throw DxvkError(str::format("D3D11SwapChain: Failed to recreate swap chain: ", vr));
   }
 
 
@@ -516,10 +485,7 @@ namespace dxvk {
 
 
   void D3D11SwapChain::CreatePresenter() {
-    PresenterDesc presenterDesc;
-    presenterDesc.imageExtent     = { m_desc.Width, m_desc.Height };
-    presenterDesc.imageCount      = PickImageCount(m_desc.BufferCount + 1);
-    presenterDesc.numFormats      = PickFormats(m_desc.Format, presenterDesc.formats);
+    PresenterDesc presenterDesc = { };
     presenterDesc.deferSurfaceCreation = m_parent->GetOptions()->deferSurfaceCreation;
 
     m_presenter = new Presenter(m_device, m_frameLatencySignal, presenterDesc, [
@@ -531,6 +497,8 @@ namespace dxvk {
         cAdapter->handle(), surface);
     });
 
+    m_presenter->setSurfaceFormat(GetSurfaceFormat(m_desc.Format));
+    m_presenter->setSurfaceExtent({ m_desc.Width, m_desc.Height });
     m_presenter->setFrameRateLimit(m_targetFrameRate, GetActualFrameLatency());
   }
 
@@ -658,46 +626,26 @@ namespace dxvk {
   }
 
 
-  uint32_t D3D11SwapChain::PickFormats(
-          DXGI_FORMAT               Format,
-          VkSurfaceFormatKHR*       pDstFormats) {
-    uint32_t n = 0;
-
+  VkSurfaceFormatKHR D3D11SwapChain::GetSurfaceFormat(DXGI_FORMAT Format) {
     switch (Format) {
       default:
         Logger::warn(str::format("D3D11SwapChain: Unexpected format: ", m_desc.Format));
-      [[fallthrough]];
-      
+        [[fallthrough]];
+
       case DXGI_FORMAT_R8G8B8A8_UNORM:
-      case DXGI_FORMAT_B8G8R8A8_UNORM: {
-        pDstFormats[n++] = { VK_FORMAT_R8G8B8A8_UNORM, m_colorspace };
-        pDstFormats[n++] = { VK_FORMAT_B8G8R8A8_UNORM, m_colorspace };
-      } break;
-      
+      case DXGI_FORMAT_B8G8R8A8_UNORM:
+        return { VK_FORMAT_R8G8B8A8_UNORM, m_colorSpace };
+
       case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
-      case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: {
-        pDstFormats[n++] = { VK_FORMAT_R8G8B8A8_SRGB, m_colorspace };
-        pDstFormats[n++] = { VK_FORMAT_B8G8R8A8_SRGB, m_colorspace };
-      } break;
-      
-      case DXGI_FORMAT_R10G10B10A2_UNORM: {
-        pDstFormats[n++] = { VK_FORMAT_A2B10G10R10_UNORM_PACK32, m_colorspace };
-        pDstFormats[n++] = { VK_FORMAT_A2R10G10B10_UNORM_PACK32, m_colorspace };
-      } break;
-      
-      case DXGI_FORMAT_R16G16B16A16_FLOAT: {
-        pDstFormats[n++] = { VK_FORMAT_R16G16B16A16_SFLOAT, m_colorspace };
-      } break;
+      case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
+        return { VK_FORMAT_R8G8B8A8_SRGB, m_colorSpace };
+
+      case DXGI_FORMAT_R10G10B10A2_UNORM:
+        return { VK_FORMAT_A2B10G10R10_UNORM_PACK32, m_colorSpace };
+
+      case DXGI_FORMAT_R16G16B16A16_FLOAT:
+        return { VK_FORMAT_R16G16B16A16_SFLOAT, m_colorSpace };
     }
-
-    return n;
-  }
-
-
-  uint32_t D3D11SwapChain::PickImageCount(
-          UINT                      Preferred) {
-    int32_t option = m_parent->GetOptions()->numBackBuffers;
-    return option > 0 ? uint32_t(option) : uint32_t(Preferred);
   }
 
 
diff --git a/src/d3d11/d3d11_swapchain.h b/src/d3d11/d3d11_swapchain.h
index 80e8dfb81..74149f9bf 100644
--- a/src/d3d11/d3d11_swapchain.h
+++ b/src/d3d11/d3d11_swapchain.h
@@ -119,9 +119,7 @@ namespace dxvk {
     HANDLE                    m_frameLatencyEvent = nullptr;
     Rc<sync::CallbackFence>   m_frameLatencySignal;
 
-    bool                      m_dirty = true;
-
-    VkColorSpaceKHR           m_colorspace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
+    VkColorSpaceKHR           m_colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
 
     double                    m_targetFrameRate = 0.0;
 
@@ -136,8 +134,6 @@ namespace dxvk {
 
     void SynchronizePresent();
 
-    void RecreateSwapChain();
-
     void CreateFrameLatencyEvent();
 
     void CreatePresenter();
@@ -154,12 +150,7 @@ namespace dxvk {
 
     uint32_t GetActualFrameLatency();
     
-    uint32_t PickFormats(
-            DXGI_FORMAT               Format,
-            VkSurfaceFormatKHR*       pDstFormats);
-    
-    uint32_t PickImageCount(
-            UINT                      Preferred);
+    VkSurfaceFormatKHR GetSurfaceFormat(DXGI_FORMAT Format);
     
     std::string GetApiName() const;
 
diff --git a/src/d3d9/d3d9_swapchain.cpp b/src/d3d9/d3d9_swapchain.cpp
index c302c493e..e0a0218ab 100644
--- a/src/d3d9/d3d9_swapchain.cpp
+++ b/src/d3d9/d3d9_swapchain.cpp
@@ -153,18 +153,16 @@ namespace dxvk {
 
     UpdateWindowCtx();
 
-    bool recreate = false;
-    recreate   |= m_wctx->presenter == nullptr;
+    bool recreate = !m_wctx->presenter;
+
     if (options->deferSurfaceCreation)
       recreate |= m_parent->IsDeviceReset();
 
-    if (m_wctx->presenter != nullptr) {
-      m_dirty  |= m_wctx->presenter->setSyncInterval(presentInterval) != VK_SUCCESS;
-      m_dirty  |= !m_wctx->presenter->hasSwapChain();
-    }
+    if (m_wctx->presenter)
+      m_wctx->presenter->setSyncInterval(presentInterval);
 
-    m_dirty    |= UpdatePresentRegion(pSourceRect, pDestRect);
-    m_dirty    |= recreate;
+    UpdatePresentRegion(pSourceRect, pDestRect);
+    UpdatePresentParameters();
 
 #ifdef _WIN32
     const bool useGDIFallback = m_partialCopy && !HasFrontBuffer();
@@ -174,17 +172,11 @@ namespace dxvk {
 
     try {
       if (recreate)
-        CreatePresenter();
-
-      if (std::exchange(m_dirty, false))
-        RecreateSwapChain();
+        RecreateSurface();
 
       // We aren't going to device loss simply because
       // 99% of D3D9 games don't handle this properly and
       // just end up crashing (like with alt-tab loss)
-      if (!m_wctx->presenter->hasSwapChain())
-        return D3D_OK;
-
       UpdateTargetFrameRate(presentInterval);
       PresentImage(presentInterval);
       return D3D_OK;
@@ -606,9 +598,6 @@ namespace dxvk {
     this->SynchronizePresent();
     this->NormalizePresentParameters(pPresentParams);
 
-    m_dirty    |= m_presentParams.BackBufferFormat   != pPresentParams->BackBufferFormat
-               || m_presentParams.BackBufferCount    != pPresentParams->BackBufferCount;
-
     bool changeFullscreen = m_presentParams.Windowed != pPresentParams->Windowed;
 
     if (pPresentParams->Windowed) {
@@ -640,6 +629,8 @@ namespace dxvk {
     if (changeFullscreen)
       SetGammaRamp(0, &m_ramp);
 
+    UpdatePresentParameters();
+
     hr = CreateBackBuffers(m_presentParams.BackBufferCount, m_presentParams.Flags);
     if (FAILED(hr))
       return hr;
@@ -826,27 +817,13 @@ namespace dxvk {
       SynchronizePresent();
 
       // Presentation semaphores and WSI swap chain image
-      PresenterInfo info = m_wctx->presenter->info();
       PresenterSync sync = { };
-
       Rc<DxvkImage> backBuffer;
 
       VkResult status = m_wctx->presenter->acquireNextImage(sync, backBuffer);
 
-      while (status != VK_SUCCESS) {
-        RecreateSwapChain();
-        
-        info = m_wctx->presenter->info();
-        status = m_wctx->presenter->acquireNextImage(sync, backBuffer);
-
-        if (status == VK_SUBOPTIMAL_KHR)
-          break;
-      }
-
-      if (m_hdrMetadata && m_dirtyHdrMetadata) {
-        m_wctx->presenter->setHdrMetadata(*m_hdrMetadata);
-        m_dirtyHdrMetadata = false;
-      }
+      if (status < 0 || status == VK_NOT_READY)
+        break;
 
       VkRect2D srcRect = {
         {  int32_t(m_srcRect.left),                    int32_t(m_srcRect.top)                    },
@@ -912,7 +889,6 @@ namespace dxvk {
         uint64_t frameId = cRepeat ? 0 : cFrameId;
 
         cDevice->presentImage(cPresenter,
-          cPresenter->info().presentMode,
           frameId, cPresentStatus);
       });
 
@@ -931,29 +907,15 @@ namespace dxvk {
 
 
   void D3D9SwapChainEx::SynchronizePresent() {
-    // Recreate swap chain if the previous present call failed
-    VkResult status = m_device->waitForSubmission(&m_presentStatus);
-
-    if (status != VK_SUCCESS)
-      RecreateSwapChain();
+    m_device->waitForSubmission(&m_presentStatus);
   }
 
-  void D3D9SwapChainEx::RecreateSwapChain() {
-    // Ensure that we can safely destroy the swap chain
-    m_device->waitForSubmission(&m_presentStatus);
-    m_device->waitForIdle();
 
-    m_presentStatus.result = VK_SUCCESS;
-
-    PresenterDesc presenterDesc;
-    presenterDesc.imageExtent     = GetPresentExtent();
-    presenterDesc.imageCount      = PickImageCount(m_presentParams.BackBufferCount + 1);
-    presenterDesc.numFormats      = PickFormats(EnumerateFormat(m_presentParams.BackBufferFormat), presenterDesc.formats);
-
-    VkResult vr = m_wctx->presenter->recreateSwapChain(presenterDesc);
-
-    if (vr)
-      throw DxvkError(str::format("D3D9SwapChainEx: Failed to recreate swap chain: ", vr));
+  void D3D9SwapChainEx::RecreateSurface() {
+    if (m_wctx->presenter)
+      m_wctx->presenter->invalidateSurface();
+    else
+      CreatePresenter();
   }
 
 
@@ -965,9 +927,6 @@ namespace dxvk {
     m_presentStatus.result = VK_SUCCESS;
 
     PresenterDesc presenterDesc;
-    presenterDesc.imageExtent     = GetPresentExtent();
-    presenterDesc.imageCount      = PickImageCount(m_presentParams.BackBufferCount + 1);
-    presenterDesc.numFormats      = PickFormats(EnumerateFormat(m_presentParams.BackBufferFormat), presenterDesc.formats);
     presenterDesc.deferSurfaceCreation = m_parent->GetOptions()->deferSurfaceCreation;
 
     m_wctx->presenter = new Presenter(m_device,
@@ -982,6 +941,12 @@ namespace dxvk {
         vki->instance(),
         surface);
     });
+
+    m_wctx->presenter->setSurfaceExtent(m_swapchainExtent);
+    m_wctx->presenter->setSurfaceFormat(GetSurfaceFormat());
+
+    if (m_hdrMetadata)
+      m_wctx->presenter->setHdrMetadata(*m_hdrMetadata);
   }
 
 
@@ -1138,60 +1103,44 @@ namespace dxvk {
   }
 
 
-  uint32_t D3D9SwapChainEx::PickFormats(
-          D3D9Format                Format,
-          VkSurfaceFormatKHR*       pDstFormats) {
-    uint32_t n = 0;
+  VkSurfaceFormatKHR D3D9SwapChainEx::GetSurfaceFormat() {
+    D3D9Format format = EnumerateFormat(m_presentParams.BackBufferFormat);
 
-    switch (Format) {
+    switch (format) {
       default:
-        Logger::warn(str::format("D3D9SwapChainEx: Unexpected format: ", Format));      
-     [[fallthrough]];
+        Logger::warn(str::format("D3D9SwapChainEx: Unexpected format: ", format));
+        [[fallthrough]];
 
       case D3D9Format::A8R8G8B8:
       case D3D9Format::X8R8G8B8:
+        return { VK_FORMAT_B8G8R8A8_UNORM, m_colorspace };
+
       case D3D9Format::A8B8G8R8:
-      case D3D9Format::X8B8G8R8: {
-        pDstFormats[n++] = { VK_FORMAT_R8G8B8A8_UNORM, m_colorspace };
-        pDstFormats[n++] = { VK_FORMAT_B8G8R8A8_UNORM, m_colorspace };
-      } break;
+      case D3D9Format::X8B8G8R8:
+        return { VK_FORMAT_R8G8B8A8_UNORM, m_colorspace };
 
       case D3D9Format::A2R10G10B10:
-      case D3D9Format::A2B10G10R10: {
-        pDstFormats[n++] = { VK_FORMAT_A2B10G10R10_UNORM_PACK32, m_colorspace };
-        pDstFormats[n++] = { VK_FORMAT_A2R10G10B10_UNORM_PACK32, m_colorspace };
-      } break;
+        return { VK_FORMAT_A2R10G10B10_UNORM_PACK32, m_colorspace };
+
+      case D3D9Format::A2B10G10R10:
+        return { VK_FORMAT_A2B10G10R10_UNORM_PACK32, m_colorspace };
 
       case D3D9Format::X1R5G5B5:
-      case D3D9Format::A1R5G5B5: {
-        pDstFormats[n++] = { VK_FORMAT_B5G5R5A1_UNORM_PACK16, m_colorspace };
-        pDstFormats[n++] = { VK_FORMAT_R5G5B5A1_UNORM_PACK16, m_colorspace };
-        pDstFormats[n++] = { VK_FORMAT_A1R5G5B5_UNORM_PACK16, m_colorspace };
-      } break;
+      case D3D9Format::A1R5G5B5:
+        return { VK_FORMAT_B5G5R5A1_UNORM_PACK16, m_colorspace };
 
-      case D3D9Format::R5G6B5: {
-        pDstFormats[n++] = { VK_FORMAT_B5G6R5_UNORM_PACK16, m_colorspace };
-        pDstFormats[n++] = { VK_FORMAT_R5G6B5_UNORM_PACK16, m_colorspace };
-      } break;
+      case D3D9Format::R5G6B5:
+        return { VK_FORMAT_B5G6R5_UNORM_PACK16, m_colorspace };
 
       case D3D9Format::A16B16G16R16F: {
-        if (m_unlockAdditionalFormats) {
-          pDstFormats[n++] = { VK_FORMAT_R16G16B16A16_SFLOAT, m_colorspace };
-        } else {
-          Logger::warn(str::format("D3D9SwapChainEx: Unexpected format: ", Format));      
+        if (!m_unlockAdditionalFormats) {
+          Logger::warn(str::format("D3D9SwapChainEx: Unexpected format: ", format));
+          return VkSurfaceFormatKHR { };
         }
-        break;
+
+        return { VK_FORMAT_R16G16B16A16_SFLOAT, m_colorspace };
       }
     }
-
-    return n;
-  }
-
-
-  uint32_t D3D9SwapChainEx::PickImageCount(
-          UINT                      Preferred) {
-    int32_t option = m_parent->GetOptions()->numBackBuffers;
-    return option > 0 ? uint32_t(option) : uint32_t(Preferred);
   }
 
 
@@ -1297,7 +1246,7 @@ namespace dxvk {
     return D3D_OK;
   }
 
-  bool    D3D9SwapChainEx::UpdatePresentRegion(const RECT* pSourceRect, const RECT* pDestRect) {
+  void D3D9SwapChainEx::UpdatePresentRegion(const RECT* pSourceRect, const RECT* pDestRect) {
     const bool isWindowed = m_presentParams.Windowed;
 
     // Tests show that present regions are ignored in fullscreen
@@ -1333,15 +1282,15 @@ namespace dxvk {
     || dstRect.right  - dstRect.left != LONG(width)
     || dstRect.bottom - dstRect.top  != LONG(height);
 
-    bool recreate = m_wctx != nullptr
-      && (m_wctx->presenter == nullptr
-      || m_wctx->presenter->info().imageExtent.width  != width
-      || m_wctx->presenter->info().imageExtent.height != height);
-
     m_swapchainExtent = { width, height };
     m_dstRect = dstRect;
+  }
 
-    return recreate;
+  void D3D9SwapChainEx::UpdatePresentParameters() {
+    if (m_wctx && m_wctx->presenter) {
+      m_wctx->presenter->setSurfaceExtent(m_swapchainExtent);
+      m_wctx->presenter->setSurfaceFormat(GetSurfaceFormat());
+    }
   }
 
   VkExtent2D D3D9SwapChainEx::GetPresentExtent() {
@@ -1386,9 +1335,11 @@ namespace dxvk {
     if (!CheckColorSpaceSupport(ColorSpace))
       return D3DERR_INVALIDCALL;
     
-    m_swapchain->m_dirty |= ColorSpace != m_swapchain->m_colorspace;
     m_swapchain->m_colorspace = ColorSpace;
 
+    if (m_swapchain->m_wctx && m_swapchain->m_wctx->presenter)
+      m_swapchain->m_wctx->presenter->setSurfaceFormat(m_swapchain->GetSurfaceFormat());
+
     return S_OK;
   }
 
@@ -1397,8 +1348,10 @@ namespace dxvk {
     if (!pHDRMetadata)
       return D3DERR_INVALIDCALL;
 
-    m_swapchain->m_hdrMetadata      = *pHDRMetadata;
-    m_swapchain->m_dirtyHdrMetadata = true;
+    m_swapchain->m_hdrMetadata = *pHDRMetadata;
+
+    if (m_swapchain->m_wctx && m_swapchain->m_wctx->presenter)
+      m_swapchain->m_wctx->presenter->setHdrMetadata(*pHDRMetadata);
 
     return S_OK;
   }
diff --git a/src/d3d9/d3d9_swapchain.h b/src/d3d9/d3d9_swapchain.h
index 1ae3c6b8a..c413a73c0 100644
--- a/src/d3d9/d3d9_swapchain.h
+++ b/src/d3d9/d3d9_swapchain.h
@@ -169,8 +169,6 @@ namespace dxvk {
 
     uint32_t                  m_frameLatencyCap = 0;
 
-    bool                      m_dirty    = true;
-
     HWND                      m_window   = nullptr;
     HMONITOR                  m_monitor  = nullptr;
 
@@ -185,7 +183,6 @@ namespace dxvk {
     VkColorSpaceKHR           m_colorspace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
 
     std::optional<VkHdrMetadataEXT> m_hdrMetadata;
-    bool m_dirtyHdrMetadata = true;
     bool m_unlockAdditionalFormats = false;
 
     D3D9VkExtSwapchain m_swapchainExt;
@@ -194,7 +191,7 @@ namespace dxvk {
 
     void SynchronizePresent();
 
-    void RecreateSwapChain();
+    void RecreateSurface();
 
     void CreatePresenter();
 
@@ -212,13 +209,8 @@ namespace dxvk {
 
     uint32_t GetActualFrameLatency();
 
-    uint32_t PickFormats(
-            D3D9Format                Format,
-            VkSurfaceFormatKHR*       pDstFormats);
+    VkSurfaceFormatKHR GetSurfaceFormat();
     
-    uint32_t PickImageCount(
-            UINT                      Preferred);
-
     void NormalizePresentParameters(D3DPRESENT_PARAMETERS* pPresentParams);
 
     void NotifyDisplayRefreshRate(
@@ -236,7 +228,9 @@ namespace dxvk {
     
     HRESULT RestoreDisplayMode(HMONITOR hMonitor);
 
-    bool    UpdatePresentRegion(const RECT* pSourceRect, const RECT* pDestRect);
+    void UpdatePresentRegion(const RECT* pSourceRect, const RECT* pDestRect);
+
+    void UpdatePresentParameters();
 
     VkExtent2D GetPresentExtent();
 
diff --git a/src/dxvk/dxvk_device.cpp b/src/dxvk/dxvk_device.cpp
index 86c789dd8..36275e2a3 100644
--- a/src/dxvk/dxvk_device.cpp
+++ b/src/dxvk/dxvk_device.cpp
@@ -307,14 +307,12 @@ namespace dxvk {
 
   void DxvkDevice::presentImage(
     const Rc<Presenter>&            presenter,
-          VkPresentModeKHR          presentMode,
           uint64_t                  frameId,
           DxvkSubmitStatus*         status) {
     status->result = VK_NOT_READY;
 
     DxvkPresentInfo presentInfo = { };
     presentInfo.presenter = presenter;
-    presentInfo.presentMode = presentMode;
     presentInfo.frameId = frameId;
     m_submissionQueue.present(presentInfo, status);
     
diff --git a/src/dxvk/dxvk_device.h b/src/dxvk/dxvk_device.h
index 219655fa9..a40a25fec 100644
--- a/src/dxvk/dxvk_device.h
+++ b/src/dxvk/dxvk_device.h
@@ -485,13 +485,11 @@ namespace dxvk {
      * the submission thread. The status of this operation
      * can be retrieved with \ref waitForSubmission.
      * \param [in] presenter The presenter
-     * \param [in] presenteMode Present mode
      * \param [in] frameId Optional frame ID
      * \param [out] status Present status
      */
     void presentImage(
       const Rc<Presenter>&            presenter,
-            VkPresentModeKHR          presentMode,
             uint64_t                  frameId,
             DxvkSubmitStatus*         status);
     
diff --git a/src/dxvk/dxvk_presenter.cpp b/src/dxvk/dxvk_presenter.cpp
index a97c60200..e5016ebcc 100644
--- a/src/dxvk/dxvk_presenter.cpp
+++ b/src/dxvk/dxvk_presenter.cpp
@@ -52,26 +52,51 @@ namespace dxvk {
   }
 
 
-  PresenterInfo Presenter::info() const {
-    return m_info;
+  VkResult Presenter::checkSwapChainStatus() {
+    std::lock_guard lock(m_surfaceMutex);
+
+    if (!m_swapchain)
+      return recreateSwapChain();
+
+    return VK_SUCCESS;
   }
 
 
   VkResult Presenter::acquireNextImage(PresenterSync& sync, Rc<DxvkImage>& image) {
-    PresenterSync& semaphores = m_semaphores.at(m_frameIndex);
-    sync = semaphores;
+    std::lock_guard lock(m_surfaceMutex);
+
+    // Ensure that the swap chain gets recreated if it is dirty
+    updateSwapChain();
 
     // Don't acquire more than one image at a time
-    if (m_acquireStatus == VK_NOT_READY) {
-      waitForSwapchainFence(semaphores);
+    if (m_acquireStatus == VK_NOT_READY && m_swapchain) {
+      PresenterSync sync = m_semaphores.at(m_frameIndex);
+
+      waitForSwapchainFence(sync);
 
       m_acquireStatus = m_vkd->vkAcquireNextImageKHR(m_vkd->device(),
         m_swapchain, std::numeric_limits<uint64_t>::max(),
         sync.acquire, VK_NULL_HANDLE, &m_imageIndex);
     }
 
-    if (m_acquireStatus != VK_SUCCESS && m_acquireStatus != VK_SUBOPTIMAL_KHR)
-      return m_acquireStatus;
+    // If the swap chain is out of date, recreate it and retry. It
+    // is possible that we do not get a new swap chain here, e.g.
+    // because the window is minimized.
+    if (m_acquireStatus != VK_SUCCESS || !m_swapchain) {
+      VkResult vr = recreateSwapChain();
+
+      if (vr != VK_SUCCESS)
+        return vr;
+
+      PresenterSync sync = m_semaphores.at(m_frameIndex);
+
+      m_acquireStatus = m_vkd->vkAcquireNextImageKHR(m_vkd->device(),
+        m_swapchain, std::numeric_limits<uint64_t>::max(),
+        sync.acquire, VK_NULL_HANDLE, &m_imageIndex);
+
+      if (m_acquireStatus < 0)
+        return m_acquireStatus;
+    }
 
     // Update HDR metadata after a successful acquire. We know
     // that there won't be a present in flight at this point.
@@ -84,14 +109,19 @@ namespace dxvk {
       }
     }
 
+    // Set dynamic present mode for the next frame if possible
+    if (!m_dynamicModes.empty())
+      m_presentMode = m_dynamicModes.at(m_preferredSyncInterval ? 1u : 0u); 
+
+    // Return relevant Vulkan objects for the acquired image
+    sync = m_semaphores.at(m_frameIndex);
     image = m_images.at(m_imageIndex);
+
     return m_acquireStatus;
   }
 
 
-  VkResult Presenter::presentImage(
-          VkPresentModeKHR  mode,
-          uint64_t          frameId) {
+  VkResult Presenter::presentImage(uint64_t frameId) {
     PresenterSync& currSync = m_semaphores.at(m_frameIndex);
 
     VkPresentIdKHR presentId = { VK_STRUCTURE_TYPE_PRESENT_ID_KHR };
@@ -104,7 +134,7 @@ namespace dxvk {
 
     VkSwapchainPresentModeInfoEXT modeInfo = { VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_MODE_INFO_EXT };
     modeInfo.swapchainCount = 1;
-    modeInfo.pPresentModes  = &mode;
+    modeInfo.pPresentModes  = &m_presentMode;
 
     VkPresentInfoKHR info = { VK_STRUCTURE_TYPE_PRESENT_INFO_KHR };
     info.waitSemaphoreCount = 1;
@@ -146,10 +176,7 @@ namespace dxvk {
   }
 
 
-  void Presenter::signalFrame(
-          VkResult          result,
-          VkPresentModeKHR  mode,
-          uint64_t          frameId) {
+  void Presenter::signalFrame(VkResult result, uint64_t frameId) {
     if (m_signal == nullptr || !frameId)
       return;
 
@@ -158,7 +185,7 @@ namespace dxvk {
 
       PresenterFrame frame = { };
       frame.result = result;
-      frame.mode = mode;
+      frame.mode = m_presentMode;
       frame.frameId = frameId;
 
       m_frameQueue.push(frame);
@@ -172,14 +199,102 @@ namespace dxvk {
   }
 
 
-  VkResult Presenter::recreateSwapChain(const PresenterDesc& desc) {
+  bool Presenter::supportsColorSpace(VkColorSpaceKHR colorspace) {
+    std::lock_guard lock(m_surfaceMutex);
+
+    if (!m_surface) {
+      VkResult vr = createSurface();
+
+      if (vr != VK_SUCCESS)
+        return false;
+    }
+
+    std::vector<VkSurfaceFormatKHR> surfaceFormats;
+    getSupportedFormats(surfaceFormats);
+
+    for (const auto& surfaceFormat : surfaceFormats) {
+      if (surfaceFormat.colorSpace == colorspace)
+        return true;
+    }
+
+    return false;
+  }
+
+
+  void Presenter::invalidateSurface() {
+    std::lock_guard lock(m_surfaceMutex);
+
+    m_dirtySurface = true;
+  }
+
+
+  void Presenter::setSyncInterval(uint32_t syncInterval) {
+    std::lock_guard lock(m_surfaceMutex);
+
+    // Normalize sync interval for present modes. We currently
+    // cannot support anything other than 1 natively anyway.
+    syncInterval = std::min(syncInterval, 1u);
+
+    if (m_preferredSyncInterval != syncInterval) {
+      m_preferredSyncInterval = syncInterval;
+
+      if (m_dynamicModes.empty())
+        m_dirtySwapchain = true;
+    }
+  }
+
+
+  void Presenter::setFrameRateLimit(double frameRate, uint32_t maxLatency) {
+    m_fpsLimiter.setTargetFrameRate(frameRate, maxLatency);
+  }
+
+
+  void Presenter::setSurfaceFormat(VkSurfaceFormatKHR format) {
+    std::lock_guard lock(m_surfaceMutex);
+
+    if (m_preferredFormat.format != format.format || m_preferredFormat.colorSpace != format.colorSpace) {
+      m_preferredFormat = format;
+      m_dirtySwapchain = true;
+    }
+  }
+
+
+  void Presenter::setSurfaceExtent(VkExtent2D extent) {
+    std::lock_guard lock(m_surfaceMutex);
+
+    if (m_preferredExtent != extent) {
+      m_preferredExtent = extent;
+      m_dirtySwapchain = true;
+    }
+  }
+
+
+  void Presenter::setHdrMetadata(VkHdrMetadataEXT hdrMetadata) {
+    std::lock_guard lock(m_surfaceMutex);
+
+    if (m_hdrMetadata->sType != VK_STRUCTURE_TYPE_HDR_METADATA_EXT) {
+      m_hdrMetadata = std::nullopt;
+      return;
+    }
+
+    if (hdrMetadata.pNext)
+      Logger::warn("HDR metadata extensions not currently supported.");
+
+    m_hdrMetadata = hdrMetadata;
+    m_hdrMetadata->pNext = nullptr;
+
+    m_hdrMetadataDirty = true;
+  }
+
+
+  VkResult Presenter::recreateSwapChain() {
     VkResult vr;
 
     if (m_swapchain)
       destroySwapchain();
 
     if (m_surface) {
-      vr = createSwapChain(desc);
+      vr = createSwapChain();
 
       if (vr == VK_ERROR_SURFACE_LOST_KHR)
         destroySurface();
@@ -189,14 +304,27 @@ namespace dxvk {
       vr = createSurface();
 
       if (vr == VK_SUCCESS)
-        vr = createSwapChain(desc);
+        vr = createSwapChain();
     }
 
     return vr;
   }
 
 
-  VkResult Presenter::createSwapChain(const PresenterDesc& desc) {
+  void Presenter::updateSwapChain() {
+    if (m_dirtySurface || m_dirtySwapchain) {
+      destroySwapchain();
+      m_dirtySwapchain = false;
+    }
+
+    if (m_dirtySurface) {
+      destroySurface();
+      m_dirtySurface = false;
+    }
+  }
+
+
+  VkResult Presenter::createSwapChain() {
     VkSurfaceFullScreenExclusiveInfoEXT fullScreenExclusiveInfo = { VK_STRUCTURE_TYPE_SURFACE_FULL_SCREEN_EXCLUSIVE_INFO_EXT };
     fullScreenExclusiveInfo.fullScreenExclusive = m_fullscreenMode;
 
@@ -229,25 +357,22 @@ namespace dxvk {
 
     // Select image extent based on current surface capabilities, and return
     // immediately if we cannot create an actual swap chain.
-    m_info.imageExtent = pickImageExtent(caps.surfaceCapabilities, desc.imageExtent);
+    VkExtent2D imageExtent = pickImageExtent(caps.surfaceCapabilities, m_preferredExtent);
 
-    if (!m_info.imageExtent.width || !m_info.imageExtent.height) {
-      m_info.imageCount = 0;
-      m_info.format     = { VK_FORMAT_UNDEFINED, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR };
-      return VK_SUCCESS;
-    }
+    if (!imageExtent.width || !imageExtent.height)
+      return VK_NOT_READY;
 
     // Select format based on swap chain properties
     if ((status = getSupportedFormats(formats)))
       return status;
 
-    m_info.format = pickFormat(formats.size(), formats.data(), desc.numFormats, desc.formats);
+    VkSurfaceFormatKHR surfaceFormat = pickSurfaceFormat(formats.size(), formats.data(), m_preferredFormat);
 
     // Select a present mode for the current sync interval
     if ((status = getSupportedPresentModes(modes)))
       return status;
 
-    m_info.presentMode = pickPresentMode(modes.size(), modes.data(), m_info.syncInterval);
+    m_presentMode = pickPresentMode(modes.size(), modes.data(), m_preferredSyncInterval);
 
     // Check whether we can change present modes dynamically. This may
     // influence the image count as well as further swap chain creation.
@@ -268,7 +393,7 @@ namespace dxvk {
 
       VkSurfacePresentModeEXT presentModeInfo = { VK_STRUCTURE_TYPE_SURFACE_PRESENT_MODE_EXT };
       presentModeInfo.pNext = const_cast<void*>(std::exchange(surfaceInfo.pNext, &presentModeInfo));
-      presentModeInfo.presentMode = m_info.presentMode;
+      presentModeInfo.presentMode = m_presentMode;
 
       caps.pNext = &compatibleModeInfo;
 
@@ -324,8 +449,6 @@ namespace dxvk {
     }
 
     // Compute swap chain image count based on available info
-    m_info.imageCount = pickImageCount(minImageCount, maxImageCount, desc.imageCount);
-
     VkSurfaceFullScreenExclusiveInfoEXT fullScreenInfo = { VK_STRUCTURE_TYPE_SURFACE_FULL_SCREEN_EXCLUSIVE_INFO_EXT };
     fullScreenInfo.fullScreenExclusive = m_fullscreenMode;
 
@@ -335,17 +458,17 @@ namespace dxvk {
 
     VkSwapchainCreateInfoKHR swapInfo = { VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR };
     swapInfo.surface                = m_surface;
-    swapInfo.minImageCount          = m_info.imageCount;
-    swapInfo.imageFormat            = m_info.format.format;
-    swapInfo.imageColorSpace        = m_info.format.colorSpace;
-    swapInfo.imageExtent            = m_info.imageExtent;
+    swapInfo.minImageCount          = pickImageCount(minImageCount, maxImageCount);
+    swapInfo.imageFormat            = surfaceFormat.format;
+    swapInfo.imageColorSpace        = surfaceFormat.colorSpace;
+    swapInfo.imageExtent            = imageExtent;
     swapInfo.imageArrayLayers       = 1;
     swapInfo.imageUsage             = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
                                     | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
     swapInfo.imageSharingMode       = VK_SHARING_MODE_EXCLUSIVE;
     swapInfo.preTransform           = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
     swapInfo.compositeAlpha         = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
-    swapInfo.presentMode            = m_info.presentMode;
+    swapInfo.presentMode            = m_presentMode;
     swapInfo.clipped                = VK_TRUE;
 
     if (m_device->features().extFullScreenExclusive)
@@ -356,26 +479,23 @@ namespace dxvk {
 
     Logger::info(str::format(
       "Presenter: Actual swap chain properties:"
-      "\n  Format:       ", m_info.format.format,
-      "\n  Color space:  ", m_info.format.colorSpace,
-      "\n  Present mode: ", m_info.presentMode, " (dynamic: ", (dynamicModes.empty() ? "no)" : "yes)"),
-      "\n  Buffer size:  ", m_info.imageExtent.width, "x", m_info.imageExtent.height,
-      "\n  Image count:  ", m_info.imageCount));
+      "\n  Format:       ", swapInfo.imageFormat,
+      "\n  Color space:  ", swapInfo.imageColorSpace,
+      "\n  Present mode: ", swapInfo.presentMode, " (dynamic: ", (dynamicModes.empty() ? "no)" : "yes)"),
+      "\n  Buffer size:  ", swapInfo.imageExtent.width, "x", swapInfo.imageExtent.height,
+      "\n  Image count:  ", swapInfo.minImageCount));
     
     if ((status = m_vkd->vkCreateSwapchainKHR(m_vkd->device(),
         &swapInfo, nullptr, &m_swapchain)))
       return status;
     
-    // Acquire images and create views
+    // Import actual swap chain images
     std::vector<VkImage> images;
 
     if ((status = getSwapImages(images)))
       return status;
     
-    // Update actual image count
-    m_info.imageCount = images.size();
-
-    for (uint32_t i = 0; i < m_info.imageCount; i++) {
+    for (uint32_t i = 0; i < images.size(); i++) {
       std::string debugName = str::format("Vulkan swap image ", i);
 
       DxvkImageCreateInfo imageInfo = { };
@@ -397,7 +517,7 @@ namespace dxvk {
 
     // Create one set of semaphores per swap image, as well as a fence
     // that we use to ensure that semaphores are safe to access.
-    uint32_t semaphoreCount = m_info.imageCount;
+    uint32_t semaphoreCount = images.size();
 
     if (!m_device->features().extSwapchainMaintenance1.swapchainMaintenance1) {
       // Without support for present fences, just give up and allocate extra
@@ -437,61 +557,6 @@ namespace dxvk {
   }
 
 
-  bool Presenter::supportsColorSpace(VkColorSpaceKHR colorspace) {
-    if (!m_surface)
-      return false;
-
-    std::vector<VkSurfaceFormatKHR> surfaceFormats;
-    getSupportedFormats(surfaceFormats);
-
-    for (const auto& surfaceFormat : surfaceFormats) {
-      if (surfaceFormat.colorSpace == colorspace)
-        return true;
-    }
-
-    return false;
-  }
-
-
-  VkResult Presenter::setSyncInterval(uint32_t syncInterval) {
-    // Normalize sync interval for present modes. We currently
-    // cannot support anything other than 1 natively anyway.
-    syncInterval = std::min(syncInterval, 1u);
-
-    if (syncInterval == m_info.syncInterval)
-      return VK_SUCCESS;
-
-    m_info.syncInterval = syncInterval;
-
-    if (syncInterval >= m_dynamicModes.size())
-      return VK_ERROR_OUT_OF_DATE_KHR;
-
-    m_info.presentMode = m_dynamicModes[syncInterval];
-    return VK_SUCCESS;
-  }
-
-
-  void Presenter::setFrameRateLimit(double frameRate, uint32_t maxLatency) {
-    m_fpsLimiter.setTargetFrameRate(frameRate, maxLatency);
-  }
-
-
-  void Presenter::setHdrMetadata(const VkHdrMetadataEXT& hdrMetadata) {
-    if (m_hdrMetadata->sType != VK_STRUCTURE_TYPE_HDR_METADATA_EXT) {
-      m_hdrMetadata = std::nullopt;
-      return;
-    }
-
-    if (hdrMetadata.pNext)
-      Logger::warn("HDR metadata extensions not currently supported.");
-
-    m_hdrMetadata = hdrMetadata;
-    m_hdrMetadata->pNext = nullptr;
-
-    m_hdrMetadataDirty = true;
-  }
-
-
   VkResult Presenter::getSupportedFormats(std::vector<VkSurfaceFormatKHR>& formats) const {
     uint32_t numFormats = 0;
 
@@ -586,42 +651,148 @@ namespace dxvk {
   }
 
 
-  VkSurfaceFormatKHR Presenter::pickFormat(
+  VkSurfaceFormatKHR Presenter::pickSurfaceFormat(
           uint32_t                  numSupported,
     const VkSurfaceFormatKHR*       pSupported,
-          uint32_t                  numDesired,
-    const VkSurfaceFormatKHR*       pDesired) {
-    if (numDesired > 0) {
-      // If the implementation allows us to freely choose
-      // the format, we'll just use the preferred format.
-      if (numSupported == 1 && pSupported[0].format == VK_FORMAT_UNDEFINED)
-        return pDesired[0];
-      
-      // If the preferred format is explicitly listed in
-      // the array of supported surface formats, use it
-      for (uint32_t i = 0; i < numDesired; i++) {
-        for (uint32_t j = 0; j < numSupported; j++) {
-          if (pSupported[j].format     == pDesired[i].format
-           && pSupported[j].colorSpace == pDesired[i].colorSpace)
-            return pSupported[j];
-        }
-      }
+    const VkSurfaceFormatKHR&       desired) {
+    VkSurfaceFormatKHR result = { };
+    result.colorSpace = pickColorSpace(numSupported, pSupported, desired.colorSpace);
+    result.format = pickFormat(numSupported, pSupported, result.colorSpace, desired.format);
+    return result;
+  }
 
-      // If that didn't work, we'll fall back to a format
-      // which has similar properties to the preferred one
-      DxvkFormatFlags prefFlags = lookupFormatInfo(pDesired[0].format)->flags;
 
-      for (uint32_t j = 0; j < numSupported; j++) {
-        auto currFlags = lookupFormatInfo(pSupported[j].format)->flags;
+  VkColorSpaceKHR Presenter::pickColorSpace(
+          uint32_t                  numSupported,
+    const VkSurfaceFormatKHR*       pSupported,
+          VkColorSpaceKHR           desired) {
+    static const std::array<std::pair<VkColorSpaceKHR, VkColorSpaceKHR>, 2> fallbacks = {{
+      { VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT, VK_COLOR_SPACE_HDR10_ST2084_EXT },
 
-        if ((currFlags & DxvkFormatFlag::ColorSpaceSrgb)
-         == (prefFlags & DxvkFormatFlag::ColorSpaceSrgb))
-          return pSupported[j];
+      { VK_COLOR_SPACE_HDR10_ST2084_EXT, VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT },
+    }};
+
+    for (uint32_t i = 0; i < numSupported; i++) {
+      if (pSupported[i].colorSpace == desired)
+        return desired;
+    }
+
+    for (const auto& f : fallbacks) {
+      if (f.first != desired)
+        continue;
+
+      for (uint32_t i = 0; i < numSupported; i++) {
+        if (pSupported[i].colorSpace == f.second)
+          return f.second;
       }
     }
-    
-    // Otherwise, fall back to the first supported format
-    return pSupported[0];
+
+    Logger::warn(str::format("No fallback color space found for ", desired, ", using ", pSupported[0].colorSpace));
+    return pSupported[0].colorSpace;
+  }
+
+
+  VkFormat Presenter::pickFormat(
+          uint32_t                  numSupported,
+    const VkSurfaceFormatKHR*       pSupported,
+          VkColorSpaceKHR           colorSpace,
+          VkFormat                  format) {
+    static const std::array<VkFormat, 15> srgbFormatList = {
+      VK_FORMAT_B5G5R5A1_UNORM_PACK16,
+      VK_FORMAT_R5G5B5A1_UNORM_PACK16,
+      VK_FORMAT_A1B5G5R5_UNORM_PACK16_KHR,
+      VK_FORMAT_R5G6B5_UNORM_PACK16,
+      VK_FORMAT_B5G6R5_UNORM_PACK16,
+      VK_FORMAT_R8G8B8A8_SRGB,
+      VK_FORMAT_B8G8R8A8_SRGB,
+      VK_FORMAT_A8B8G8R8_SRGB_PACK32,
+      VK_FORMAT_R8G8B8A8_UNORM,
+      VK_FORMAT_B8G8R8A8_UNORM,
+      VK_FORMAT_A8B8G8R8_UNORM_PACK32,
+      VK_FORMAT_A2R10G10B10_UNORM_PACK32,
+      VK_FORMAT_A2B10G10R10_UNORM_PACK32,
+      VK_FORMAT_R16G16B16A16_UNORM,
+      VK_FORMAT_R16G16B16A16_SFLOAT,
+    };
+
+    static const std::array<VkFormat, 5> hdr10FormatList = {
+      VK_FORMAT_A2R10G10B10_UNORM_PACK32,
+      VK_FORMAT_A2B10G10R10_UNORM_PACK32,
+      VK_FORMAT_R16G16B16A16_UNORM,
+      VK_FORMAT_R16G16B16A16_SFLOAT,
+      VK_FORMAT_E5B9G9R9_UFLOAT_PACK32,
+    };
+
+    static const std::array<VkFormat, 1> scRGBFormatList = {
+      VK_FORMAT_R16G16B16A16_SFLOAT,
+    };
+
+    static const std::array<PresenterFormatList, 3> compatLists = {{
+      { VK_COLOR_SPACE_SRGB_NONLINEAR_KHR,
+        srgbFormatList.size(), srgbFormatList.data() },
+      { VK_COLOR_SPACE_HDR10_ST2084_EXT,
+        hdr10FormatList.size(), hdr10FormatList.data() },
+      { VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT,
+        scRGBFormatList.size(), scRGBFormatList.data() },
+    }};
+
+    // If the desired format is supported natively, use it
+    VkFormat fallback = VK_FORMAT_UNDEFINED;
+
+    for (uint32_t i = 0; i < numSupported; i++) {
+      if (pSupported[i].colorSpace == colorSpace) {
+        if (pSupported[i].format == format)
+          return pSupported[i].format;
+
+        if (!fallback)
+          fallback = pSupported[i].format;
+      }
+    }
+
+    // Otherwise, find a supported format for the color space
+    const PresenterFormatList* compatList = nullptr;
+
+    for (const auto& l : compatLists) {
+      if (l.colorSpace == colorSpace)
+        compatList = &l;
+    }
+
+    if (!compatList)
+      return fallback;
+
+    // If the desired format is linear, ignore sRGB formats. We can do
+    // this because sRGB and linear formats must be supported in pairs.
+    // sRGB to linear fallbacks need to be allowed though in order to
+    // be able to select a format with a higher bit depth than requested.
+    bool desiredIsSrgb = lookupFormatInfo(format)->flags.test(DxvkFormatFlag::ColorSpaceSrgb);
+    bool desiredFound = false;
+
+    for (uint32_t i = 0; i < compatList->formatCount; i++) {
+      bool formatIsSrgb = lookupFormatInfo(compatList->formats[i])->flags.test(DxvkFormatFlag::ColorSpaceSrgb);
+
+      if (!desiredIsSrgb && formatIsSrgb)
+        continue;
+
+      bool isSupported = false;
+
+      if (compatList->formats[i] == format)
+        desiredFound = true;
+
+      for (uint32_t j = 0; j < numSupported && !isSupported; j++)
+        isSupported = pSupported[j].colorSpace == colorSpace && pSupported[j].format == compatList->formats[i];
+
+      if (isSupported) {
+        fallback = compatList->formats[i];
+
+        if (desiredFound)
+          break;
+      }
+    }
+
+    if (!desiredFound)
+      Logger::warn(str::format("Desired format ", format, " not in compatibility list for ", colorSpace, ", using ", fallback));
+
+    return fallback;
   }
 
 
@@ -671,16 +842,12 @@ namespace dxvk {
 
   uint32_t Presenter::pickImageCount(
           uint32_t                  minImageCount,
-          uint32_t                  maxImageCount,
-          uint32_t                  desired) {
+          uint32_t                  maxImageCount) {
     uint32_t count = minImageCount + 1;
-    
-    if (count < desired)
-      count = desired;
-    
+
     if (count > maxImageCount && maxImageCount != 0)
       count = maxImageCount;
-    
+
     return count;
   }
 
diff --git a/src/dxvk/dxvk_presenter.h b/src/dxvk/dxvk_presenter.h
index 2da3a78ca..3222634ed 100644
--- a/src/dxvk/dxvk_presenter.h
+++ b/src/dxvk/dxvk_presenter.h
@@ -33,25 +33,7 @@ namespace dxvk {
    * an input during swap chain creation.
    */
   struct PresenterDesc {
-    VkExtent2D          imageExtent = { };
-    uint32_t            imageCount = 0u;
-    uint32_t            numFormats = 0u;
-    VkSurfaceFormatKHR  formats[4] = { };
-    bool                deferSurfaceCreation = false;
-  };
-
-  /**
-   * \brief Presenter properties
-   * 
-   * Contains the actual properties
-   * of the underlying swap chain.
-   */
-  struct PresenterInfo {
-    VkSurfaceFormatKHR  format;
-    VkPresentModeKHR    presentMode;
-    VkExtent2D          imageExtent;
-    uint32_t            imageCount;
-    uint32_t            syncInterval;
+    bool deferSurfaceCreation = false;
   };
 
   /**
@@ -78,6 +60,15 @@ namespace dxvk {
     VkResult          result  = VK_NOT_READY;
   };
 
+  /**
+   * \brief Format compatibility list
+   */
+  struct PresenterFormatList {
+    VkColorSpaceKHR colorSpace;
+    size_t formatCount;
+    const VkFormat* formats;
+  };
+
   /**
    * \brief Vulkan presenter
    * 
@@ -98,21 +89,29 @@ namespace dxvk {
     ~Presenter();
 
     /**
-     * \brief Actual presenter info
-     * \returns Swap chain properties
+     * \brief Tests swap chain status
+     *
+     * If no swapchain currently exists, this method may create
+     * one so that presentation can subsequently be performed.
+     * \returns One of the following return codes:
+     *  - \c VK_SUCCESS if a valid swapchain exists
+     *  - \c VK_NOT_READY if no swap chain can be created
+     *  - Any other error code if swap chain creation failed.
      */
-    PresenterInfo info() const;
+    VkResult checkSwapChainStatus();
 
     /**
      * \brief Acquires next image
-     * 
-     * Potentially blocks the calling thread.
-     * If this returns an error, the swap chain
-     * must be recreated and a new image must
-     * be acquired before proceeding.
+     *
+     * Tries to acquire an image from the underlying Vulkan
+     * swapchain. May recreate the swapchain if any surface
+     * properties or user-specified parameters have changed.
+     * Potentially blocks the calling thread, and must not be
+     * called if any present call is currently in flight.
      * \param [out] sync Synchronization semaphores
      * \param [out] image Acquired swap chain image
-     * \returns Status of the operation
+     * \returns Status of the operation. May return
+     *    \c VK_NOT_READY if no swap chain exists.
      */
     VkResult acquireNextImage(
             PresenterSync&  sync,
@@ -121,17 +120,12 @@ namespace dxvk {
     /**
      * \brief Presents current image
      * 
-     * Presents the current image. If this returns
-     * an error, the swap chain must be recreated,
-     * but do not present before acquiring an image.
-     * \param [in] mode Present mode
+     * Presents the last successfuly acquired image.
      * \param [in] frameId Frame number.
      *    Must increase monotonically.
      * \returns Status of the operation
      */
-    VkResult presentImage(
-            VkPresentModeKHR  mode,
-            uint64_t          frameId);
+    VkResult presentImage(uint64_t frameId);
 
     /**
      * \brief Signals a given frame
@@ -141,34 +135,17 @@ namespace dxvk {
      * called before GPU work prior to the present submission has
      * completed in order to maintain consistency.
      * \param [in] result Presentation result
-     * \param [in] mode Present mode
      * \param [in] frameId Frame number
      */
-    void signalFrame(
-            VkResult          result,
-            VkPresentModeKHR  mode,
-            uint64_t          frameId);
-
-    /**
-     * \brief Changes presenter properties
-     * 
-     * Recreates the swap chain immediately. Note that
-     * no swap chain resources must be in use by the
-     * GPU at the time this is called.
-     * \param [in] desc Swap chain description
-     * \param [in] surface New Vulkan surface
-     */
-    VkResult recreateSwapChain(
-      const PresenterDesc&  desc);
+    void signalFrame(VkResult result, uint64_t frameId);
 
     /**
      * \brief Changes sync interval
      *
-     * If this returns an error, the swap chain must
-     * be recreated.
+     * Changes the Vulkan present mode as necessary.
      * \param [in] syncInterval New sync interval
      */
-    VkResult setSyncInterval(uint32_t syncInterval);
+    void setSyncInterval(uint32_t syncInterval);
 
     /**
      * \brief Changes maximum frame rate
@@ -179,16 +156,32 @@ namespace dxvk {
     void setFrameRateLimit(double frameRate, uint32_t maxLatency);
 
     /**
-     * \brief Checks whether a Vulkan swap chain exists
+     * \brief Sets preferred color space and format
      *
-     * On Windows, there are situations where we cannot create
-     * a swap chain as the surface size can reach zero, and no
-     * presentation can be performed.
-     * \returns \c true if the presenter has a swap chain.
+     * If the Vulkan surface does not natively support the given
+     * parameter combo, it will try to select a format and color
+     * space with similar properties.
+     * \param [in] format Preferred surface format
      */
-    bool hasSwapChain() const {
-      return m_swapchain;
-    }
+    void setSurfaceFormat(VkSurfaceFormatKHR format);
+
+    /**
+     * \brief Sets preferred surface extent
+     *
+     * The preferred surface extent is only relevant if the Vulkan
+     * surface itself does not have a fixed size. Should match the
+     * back buffer size of the application.
+     * \param [in] extent Preferred surface extent
+     */
+    void setSurfaceExtent(VkExtent2D extent);
+
+    /**
+     * \brief Sets HDR metadata
+     *
+     * Updated HDR metadata will be applied on the next \c acquire.
+     * \param [in] hdrMetadata HDR Metadata
+     */
+    void setHdrMetadata(VkHdrMetadataEXT hdrMetadata);
 
     /**
      * \brief Checks support for a Vulkan color space
@@ -199,12 +192,13 @@ namespace dxvk {
     bool supportsColorSpace(VkColorSpaceKHR colorspace);
 
     /**
-     * \brief Sets HDR metadata
+     * \brief Invalidates Vulkan surface
      *
-     * Updated HDR metadata will be applied on the next \c acquire.
-     * \param [in] hdrMetadata HDR Metadata
+     * This will cause the Vulkan surface to be destroyed and
+     * recreated on the next \c acquire call. This is a hacky
+     * workaround to support windows with multiple surfaces.
      */
-    void setHdrMetadata(const VkHdrMetadataEXT& hdrMetadata);
+    void invalidateSurface();
 
   private:
 
@@ -214,7 +208,7 @@ namespace dxvk {
     Rc<vk::InstanceFn>          m_vki;
     Rc<vk::DeviceFn>            m_vkd;
 
-    PresenterInfo               m_info = { };
+    dxvk::mutex                 m_surfaceMutex;
     PresenterSurfaceProc        m_surfaceProc;
 
     VkSurfaceKHR                m_surface     = VK_NULL_HANDLE;
@@ -227,6 +221,15 @@ namespace dxvk {
 
     std::vector<VkPresentModeKHR> m_dynamicModes;
 
+    VkExtent2D                  m_preferredExtent = { };
+    VkSurfaceFormatKHR          m_preferredFormat = { };
+    uint32_t                    m_preferredSyncInterval = 1u;
+
+    bool                        m_dirtySwapchain = false;
+    bool                        m_dirtySurface = false;
+
+    VkPresentModeKHR            m_presentMode = VK_PRESENT_MODE_FIFO_KHR;
+
     uint32_t                    m_imageIndex = 0;
     uint32_t                    m_frameIndex = 0;
 
@@ -246,8 +249,11 @@ namespace dxvk {
     alignas(CACHE_LINE_SIZE)
     FpsLimiter                  m_fpsLimiter;
 
-    VkResult createSwapChain(
-      const PresenterDesc&  desc);
+    void updateSwapChain();
+
+    VkResult recreateSwapChain();
+
+    VkResult createSwapChain();
 
     VkResult getSupportedFormats(
             std::vector<VkSurfaceFormatKHR>& formats) const;
@@ -258,11 +264,21 @@ namespace dxvk {
     VkResult getSwapImages(
             std::vector<VkImage>&     images);
     
-    VkSurfaceFormatKHR pickFormat(
+    VkSurfaceFormatKHR pickSurfaceFormat(
             uint32_t                  numSupported,
       const VkSurfaceFormatKHR*       pSupported,
-            uint32_t                  numDesired,
-      const VkSurfaceFormatKHR*       pDesired);
+      const VkSurfaceFormatKHR&       desired);
+
+    VkColorSpaceKHR pickColorSpace(
+            uint32_t                  numSupported,
+      const VkSurfaceFormatKHR*       pSupported,
+            VkColorSpaceKHR           desired);
+
+    VkFormat pickFormat(
+            uint32_t                  numSupported,
+      const VkSurfaceFormatKHR*       pSupported,
+            VkColorSpaceKHR           colorSpace,
+            VkFormat                  format);
 
     VkPresentModeKHR pickPresentMode(
             uint32_t                  numSupported,
@@ -275,8 +291,7 @@ namespace dxvk {
 
     uint32_t pickImageCount(
             uint32_t                  minImageCount,
-            uint32_t                  maxImageCount,
-            uint32_t                  desired);
+            uint32_t                  maxImageCount);
 
     VkResult createSurface();
 
diff --git a/src/dxvk/dxvk_queue.cpp b/src/dxvk/dxvk_queue.cpp
index 533bd30fd..d9d8b6c30 100644
--- a/src/dxvk/dxvk_queue.cpp
+++ b/src/dxvk/dxvk_queue.cpp
@@ -145,7 +145,7 @@ namespace dxvk {
           entry.result = entry.submit.cmdList->submit(m_semaphores, m_timelines);
           entry.timelines = m_timelines;
         } else if (entry.present.presenter != nullptr) {
-          entry.result = entry.present.presenter->presentImage(entry.present.presentMode, entry.present.frameId);
+          entry.result = entry.present.presenter->presentImage(entry.present.frameId);
         }
 
         if (m_callback)
@@ -235,8 +235,7 @@ namespace dxvk {
         // Signal the frame and then immediately destroy the reference.
         // This is necessary since the front-end may want to explicitly
         // destroy the presenter object. 
-        entry.present.presenter->signalFrame(entry.result,
-          entry.present.presentMode, entry.present.frameId);
+        entry.present.presenter->signalFrame(entry.result, entry.present.frameId);
         entry.present.presenter = nullptr;
       }
 
diff --git a/src/dxvk/dxvk_queue.h b/src/dxvk/dxvk_queue.h
index b7b9114b9..8ef6a7148 100644
--- a/src/dxvk/dxvk_queue.h
+++ b/src/dxvk/dxvk_queue.h
@@ -43,7 +43,6 @@ namespace dxvk {
    */
   struct DxvkPresentInfo {
     Rc<Presenter>       presenter;
-    VkPresentModeKHR    presentMode;
     uint64_t            frameId;
   };