diff --git a/src/d3d9/d3d9_device.cpp b/src/d3d9/d3d9_device.cpp
index 80bdcf88f..46f9061bf 100644
--- a/src/d3d9/d3d9_device.cpp
+++ b/src/d3d9/d3d9_device.cpp
@@ -4270,7 +4270,7 @@ namespace dxvk {
     m_implicitSwapchain->Invalidate(pPresentationParameters->hDeviceWindow);
 
     try {
-      auto* swapchain = new D3D9SwapChainEx(this, pPresentationParameters, pFullscreenDisplayMode);
+      auto* swapchain = new D3D9SwapChainEx(this, pPresentationParameters, pFullscreenDisplayMode, false);
       *ppSwapChain = ref(swapchain);
       m_losableResourceCounter++;
     }
@@ -6098,11 +6098,29 @@ namespace dxvk {
   }
 
 
-  void D3D9DeviceEx::EndFrame() {
+  void D3D9DeviceEx::BeginFrame(Rc<DxvkLatencyTracker> LatencyTracker, uint64_t FrameId) {
     D3D9DeviceLock lock = LockDevice();
 
-    EmitCs<false>([] (DxvkContext* ctx) {
+    EmitCs<false>([
+      cTracker = std::move(LatencyTracker),
+      cFrameId = FrameId
+    ] (DxvkContext* ctx) {
+      if (cTracker && cTracker->needsAutoMarkers())
+        ctx->beginLatencyTracking(cTracker, cFrameId);
+    });
+  }
+
+
+  void D3D9DeviceEx::EndFrame(Rc<DxvkLatencyTracker> LatencyTracker) {
+    D3D9DeviceLock lock = LockDevice();
+
+    EmitCs<false>([
+      cTracker = std::move(LatencyTracker)
+    ] (DxvkContext* ctx) {
       ctx->endFrame();
+
+      if (cTracker && cTracker->needsAutoMarkers())
+        ctx->endLatencyTracking(cTracker);
     });
   }
 
@@ -8445,7 +8463,7 @@ namespace dxvk {
         return hr;
     }
     else {
-      m_implicitSwapchain = new D3D9SwapChainEx(this, pPresentationParameters, pFullscreenDisplayMode);
+      m_implicitSwapchain = new D3D9SwapChainEx(this, pPresentationParameters, pFullscreenDisplayMode, true);
       m_mostRecentlyUsedSwapchain = m_implicitSwapchain.ptr();
     }
 
diff --git a/src/d3d9/d3d9_device.h b/src/d3d9/d3d9_device.h
index 7fd4ab70a..ed86bb85e 100644
--- a/src/d3d9/d3d9_device.h
+++ b/src/d3d9/d3d9_device.h
@@ -800,7 +800,8 @@ namespace dxvk {
     void Flush();
     void FlushAndSync9On12();
 
-    void EndFrame();
+    void BeginFrame(Rc<DxvkLatencyTracker> LatencyTracker, uint64_t FrameId);
+    void EndFrame(Rc<DxvkLatencyTracker> LatencyTracker);
 
     void UpdateActiveRTs(uint32_t index);
 
diff --git a/src/d3d9/d3d9_swapchain.cpp b/src/d3d9/d3d9_swapchain.cpp
index de6604904..c268fa5dc 100644
--- a/src/d3d9/d3d9_swapchain.cpp
+++ b/src/d3d9/d3d9_swapchain.cpp
@@ -23,10 +23,12 @@ namespace dxvk {
   D3D9SwapChainEx::D3D9SwapChainEx(
           D3D9DeviceEx*          pDevice,
           D3DPRESENT_PARAMETERS* pPresentParams,
-    const D3DDISPLAYMODEEX*      pFullscreenDisplayMode)
+    const D3DDISPLAYMODEEX*      pFullscreenDisplayMode,
+          bool                   EnableLatencyTracking)
     : D3D9SwapChainExBase(pDevice)
     , m_device           (pDevice->GetDXVKDevice())
     , m_frameLatencyCap  (pDevice->GetOptions()->maxFrameLatency)
+    , m_latencyTracking  (EnableLatencyTracking)
     , m_swapchainExt     (this) {
     this->NormalizePresentParameters(pPresentParams);
     m_presentParams = *pPresentParams;
@@ -186,7 +188,7 @@ namespace dxvk {
   #define DCX_USESTYLE 0x00010000
 
   HRESULT D3D9SwapChainEx::PresentImageGDI(HWND Window) {
-    m_parent->EndFrame();
+    m_parent->EndFrame(nullptr);
     m_parent->Flush();
 
     if (!std::exchange(m_warnedAboutGDIFallback, true))
@@ -717,6 +719,9 @@ namespace dxvk {
       if (entry->second.presenter) {
         entry->second.presenter->destroyResources();
         entry->second.presenter = nullptr;
+
+        if (m_presentParams.hDeviceWindow == hWindow)
+          DestroyLatencyTracker();
       }
 
       if (m_wctx == &entry->second)
@@ -802,10 +807,15 @@ namespace dxvk {
 
 
   void D3D9SwapChainEx::PresentImage(UINT SyncInterval) {
-    m_parent->EndFrame();
+    m_parent->EndFrame(m_latencyTracker);
     m_parent->Flush();
 
+    if (m_latencyTracker)
+      m_latencyTracker->notifyCpuPresentBegin(m_wctx->frameId + 1u);
+
     // Retrieve the image and image view to present
+    VkResult status = VK_SUCCESS;
+
     Rc<DxvkImage> swapImage = m_backBuffers[0]->GetCommonTexture()->GetImage();
     Rc<DxvkImageView> swapImageView = m_backBuffers[0]->GetImageView(false);
 
@@ -814,10 +824,12 @@ namespace dxvk {
       PresenterSync sync = { };
       Rc<DxvkImage> backBuffer;
 
-      VkResult status = m_wctx->presenter->acquireNextImage(sync, backBuffer);
+      status = m_wctx->presenter->acquireNextImage(sync, backBuffer);
 
-      if (status < 0 || status == VK_NOT_READY)
+      if (status < 0 || status == VK_NOT_READY) {
+        status = i ? VK_SUCCESS : status;
         break;
+      }
 
       VkRect2D srcRect = {
         {  int32_t(m_srcRect.left),                    int32_t(m_srcRect.top)                    },
@@ -854,7 +866,8 @@ namespace dxvk {
         cDstRect        = dstRect,
         cRepeat         = i,
         cSync           = sync,
-        cFrameId        = m_wctx->frameId
+        cFrameId        = m_wctx->frameId,
+        cLatency        = m_latencyTracker
       ] (DxvkContext* ctx) {
         // Update back buffer color space as necessary
         if (cSrcView->image()->info().colorSpace != cColorSpace) {
@@ -876,14 +889,33 @@ namespace dxvk {
 
         uint64_t frameId = cRepeat ? 0 : cFrameId;
 
-        cDevice->presentImage(cPresenter, nullptr, frameId, nullptr);
+        cDevice->presentImage(cPresenter, cLatency, frameId, nullptr);
       });
 
       m_parent->FlushCsChunk();
     }
 
+    if (m_latencyTracker) {
+      if (status == VK_SUCCESS)
+        m_latencyTracker->notifyCpuPresentEnd(m_wctx->frameId);
+      else
+        m_latencyTracker->discardTimings();
+    }
+
     SyncFrameLatency();
 
+    DxvkLatencyStats latencyStats = { };
+
+    if (m_latencyTracker && status == VK_SUCCESS) {
+      latencyStats = m_latencyTracker->getStatistics(m_wctx->frameId);
+      m_latencyTracker->sleepAndBeginFrame(m_wctx->frameId + 1, std::abs(m_targetFrameRate));
+
+      m_parent->BeginFrame(m_latencyTracker, m_wctx->frameId + 1u);
+    }
+
+    if (m_latencyHud)
+      m_latencyHud->accumulateStats(latencyStats);
+
     // Rotate swap chain buffers so that the back
     // buffer at index 0 becomes the front buffer.
     for (uint32_t i = 1; i < m_backBuffers.size(); i++)
@@ -941,6 +973,9 @@ namespace dxvk {
 
       entry->second.frameLatencySignal = new sync::Fence(entry->second.frameId);
       entry->second.presenter = CreatePresenter(m_window, entry->second.frameLatencySignal);
+
+      if (m_presentParams.hDeviceWindow == m_window && m_latencyTracking)
+        m_latencyTracker = m_device->createLatencyTracker(entry->second.presenter);
     }
 
     m_wctx = &entry->second;
@@ -1017,6 +1052,10 @@ namespace dxvk {
 
     if (hud) {
       m_apiHud = hud->addItem<hud::HudClientApiItem>("api", 1, GetApiName());
+
+      if (m_latencyTracking)
+        m_latencyHud = hud->addItem<hud::HudLatencyItem>("latency", 4);
+
       hud->addItem<hud::HudSamplerCount>("samplers", -1, m_parent);
       hud->addItem<hud::HudFixedFunctionShaders>("ffshaders", -1, m_parent);
       hud->addItem<hud::HudSWVPState>("swvp", -1, m_parent);
@@ -1041,6 +1080,18 @@ namespace dxvk {
   }
 
 
+  void D3D9SwapChainEx::DestroyLatencyTracker() {
+    if (!m_latencyTracker)
+      return;
+
+    m_parent->InjectCs([
+      cTracker = std::move(m_latencyTracker)
+    ] (DxvkContext* ctx) {
+      ctx->endLatencyTracking(cTracker);
+    });
+  }
+
+
   void D3D9SwapChainEx::UpdateTargetFrameRate(uint32_t SyncInterval) {
     double frameRateOption = double(m_parent->GetOptions()->maxFrameRate);
     double frameRate = std::max(frameRateOption, 0.0);
@@ -1049,6 +1100,7 @@ namespace dxvk {
       frameRate = -m_displayRefreshRate / double(SyncInterval);
 
     m_wctx->presenter->setFrameRateLimit(frameRate, GetActualFrameLatency());
+    m_targetFrameRate = frameRate;
   }
 
 
diff --git a/src/d3d9/d3d9_swapchain.h b/src/d3d9/d3d9_swapchain.h
index 9ab394975..9f69e44c5 100644
--- a/src/d3d9/d3d9_swapchain.h
+++ b/src/d3d9/d3d9_swapchain.h
@@ -69,7 +69,8 @@ namespace dxvk {
     D3D9SwapChainEx(
             D3D9DeviceEx*          pDevice,
             D3DPRESENT_PARAMETERS* pPresentParams,
-      const D3DDISPLAYMODEEX*      pFullscreenDisplayMode);
+      const D3DDISPLAYMODEEX*      pFullscreenDisplayMode,
+            bool                   EnableLatencyTracking);
 
     ~D3D9SwapChainEx();
 
@@ -173,12 +174,17 @@ namespace dxvk {
     wsi::DxvkWindowState      m_windowState;
 
     double                    m_displayRefreshRate = 0.0;
+    double                    m_targetFrameRate = 0.0;
 
     bool                      m_warnedAboutGDIFallback = false;
 
     VkColorSpaceKHR           m_colorspace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
 
+    bool                      m_latencyTracking = false;
+    Rc<DxvkLatencyTracker>    m_latencyTracker = nullptr;
+
     Rc<hud::HudClientApiItem> m_apiHud;
+    Rc<hud::HudLatencyItem>   m_latencyHud;
 
     std::optional<VkHdrMetadataEXT> m_hdrMetadata;
     bool m_unlockAdditionalFormats = false;
@@ -197,6 +203,8 @@ namespace dxvk {
 
     void CreateBlitter();
 
+    void DestroyLatencyTracker();
+
     void InitRamp();
 
     void UpdateTargetFrameRate(uint32_t SyncInterval);