chromium/ui/gl/vsync_thread_win.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/gl/vsync_thread_win.h"

#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/singleton.h"
#include "base/memory/stack_allocated.h"
#include "base/notreached.h"
#include "base/power_monitor/power_monitor.h"
#include "base/synchronization/lock_subtle.h"
#include "base/time/time.h"
#include "base/trace_event/typed_macros.h"
#include "base/win/windows_version.h"
#include "ui/gl/direct_composition_support.h"
#include "ui/gl/gl_features.h"

namespace gl {
namespace {

// Whether the current thread holds the `VSyncThreadWin` lock.
thread_local bool g_current_thread_holds_lock = false;

// Check if a DXGI adapter is stale and needs to be replaced. This can happen
// e.g. when detaching/reattaching remote desktop sessions and causes subsequent
// WaitForVSyncs on the stale adapter/output to return instantly.
bool DXGIFactoryIsCurrent(IDXGIAdapter* dxgi_adapter) {
  CHECK(dxgi_adapter);

  HRESULT hr = S_OK;
  Microsoft::WRL::ComPtr<IDXGIFactory1> dxgi_factory;
  hr = dxgi_adapter->GetParent(IID_PPV_ARGS(&dxgi_factory));
  CHECK_EQ(S_OK, hr);
  return dxgi_factory->IsCurrent();
}

// Create a new factory and find a DXGI adapter matching a LUID. This is useful
// if we have a previous adapter whose factory has become stale.
Microsoft::WRL::ComPtr<IDXGIAdapter> FindDXGIAdapterOnNewFactory(
    const LUID luid) {
  HRESULT hr = S_OK;

  Microsoft::WRL::ComPtr<IDXGIFactory1> factory;
  hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory));
  CHECK_EQ(S_OK, hr);

  Microsoft::WRL::ComPtr<IDXGIAdapter1> new_adapter;
  for (uint32_t i = 0;; i++) {
    hr = factory->EnumAdapters1(i, &new_adapter);
    if (hr == DXGI_ERROR_NOT_FOUND) {
      break;
    }
    if (FAILED(hr)) {
      DLOG(ERROR) << "EnumAdapters1 failed: "
                  << logging::SystemErrorCodeToString(hr);
      return nullptr;
    }

    DXGI_ADAPTER_DESC1 new_adapter_desc;
    hr = new_adapter->GetDesc1(&new_adapter_desc);
    CHECK_EQ(S_OK, hr);

    if (new_adapter_desc.AdapterLuid.HighPart == luid.HighPart &&
        new_adapter_desc.AdapterLuid.LowPart == luid.LowPart) {
      return new_adapter;
    }
  }

  DLOG(ERROR) << "Failed to find DXGI adapter with matching LUID";
  return nullptr;
}

// Return true if |output| is on |monitor|.
bool DXGIOutputIsOnMonitor(IDXGIOutput* output, const HMONITOR monitor) {
  CHECK(output);

  DXGI_OUTPUT_DESC desc = {};
  HRESULT hr = output->GetDesc(&desc);
  CHECK_EQ(S_OK, hr);
  return desc.Monitor == monitor;
}

Microsoft::WRL::ComPtr<IDXGIOutput> DXGIOutputFromMonitor(
    HMONITOR monitor,
    IDXGIAdapter* dxgi_adapter) {
  CHECK(dxgi_adapter);

  HRESULT hr = S_OK;

  Microsoft::WRL::ComPtr<IDXGIOutput> output;
  for (uint32_t i = 0;; i++) {
    hr = dxgi_adapter->EnumOutputs(i, &output);
    if (hr == DXGI_ERROR_NOT_FOUND) {
      break;
    }
    if (FAILED(hr)) {
      DLOG(ERROR) << "EnumOutputs failed: "
                  << logging::SystemErrorCodeToString(hr);
      return nullptr;
    }

    if (DXGIOutputIsOnMonitor(output.Get(), monitor)) {
      return output;
    }
  }

  DLOG(ERROR) << "Failed to find DXGI output with matching monitor";
  return nullptr;
}

Microsoft::WRL::ComPtr<IDXGIAdapter> GetAdapter(IDXGIDevice* device) {
  CHECK(device);

  Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
  CHECK_EQ(S_OK, device->GetAdapter(&adapter));
  return adapter;
}

LUID GetLuid(IDXGIAdapter* adapter) {
  CHECK(adapter);

  DXGI_ADAPTER_DESC desc;
  HRESULT hr = adapter->GetDesc(&desc);
  CHECK_EQ(S_OK, hr);
  return desc.AdapterLuid;
}
}  // namespace

// static
VSyncThreadWin* VSyncThreadWin::GetInstance() {
  static VSyncThreadWin* vsync_thread = []() -> VSyncThreadWin* {
    Microsoft::WRL::ComPtr<ID3D11Device> d3d11_device(
        GetDirectCompositionD3D11Device());
    if (!d3d11_device) {
      return nullptr;
    }
    Microsoft::WRL::ComPtr<IDXGIDevice> dxgi_device;
    CHECK_EQ(d3d11_device.As(&dxgi_device), S_OK);
    return new VSyncThreadWin(std::move(dxgi_device));
  }();
  return vsync_thread;
}

class VSyncThreadWin::AutoVSyncThreadLock {
  STACK_ALLOCATED();

 public:
  AutoVSyncThreadLock(VSyncThreadWin* vsync_thread)
      EXCLUSIVE_LOCK_FUNCTION(vsync_thread->lock_) {
    if (g_current_thread_holds_lock) {
      vsync_thread->lock_.AssertAcquired();
    } else {
      auto_lock_.emplace(
          vsync_thread->lock_,
          // This lock is used to satisfy a mutual exclusion guarantee verified
          // by a SEQUENCE_CHECKER in `observers_`.
          base::subtle::LockTracking::kEnabled);
      g_current_thread_holds_lock = true;
    }
  }

  ~AutoVSyncThreadLock() UNLOCK_FUNCTION() {
    DCHECK(g_current_thread_holds_lock);
    if (auto_lock_.has_value()) {
      g_current_thread_holds_lock = false;
    }
  }

 private:
  std::optional<base::AutoLock> auto_lock_;
};

VSyncThreadWin::VSyncThreadWin(Microsoft::WRL::ComPtr<IDXGIDevice> dxgi_device)
    : vsync_thread_("GpuVSyncThread"),
      vsync_provider_(gfx::kNullAcceleratedWidget),
      dxgi_adapter_(GetAdapter(dxgi_device.Get())),
      original_adapter_luid_(GetLuid(dxgi_adapter_.Get())) {
  is_suspended_ =
      base::PowerMonitor::AddPowerSuspendObserverAndReturnSuspendedState(this);
  vsync_thread_.StartWithOptions(
      base::Thread::Options(base::ThreadType::kDisplayCritical));
}

VSyncThreadWin::~VSyncThreadWin() {
  NOTREACHED();
}

void VSyncThreadWin::PostTaskIfNeeded() {
  lock_.AssertAcquired();
  // PostTaskIfNeeded is called from AddObserver and OnResume.
  // Before queuing up a task, make sure that there are observers waiting for
  // VSync and that we're not already firing events to consumers. Avoid firing
  // events if we're suspended to conserve battery life.
  if (!is_vsync_task_posted_ && !observers_.empty() && !is_suspended_) {
    vsync_thread_.task_runner()->PostTask(
        FROM_HERE,
        base::BindOnce(&VSyncThreadWin::WaitForVSync, base::Unretained(this)));
    is_vsync_task_posted_ = true;
  }
}

void VSyncThreadWin::AddObserver(VSyncObserver* obs) {
  AutoVSyncThreadLock auto_lock(this);
  observers_.AddObserver(obs);
  PostTaskIfNeeded();
}

void VSyncThreadWin::RemoveObserver(VSyncObserver* obs) {
  AutoVSyncThreadLock auto_lock(this);
  observers_.RemoveObserver(obs);
}

void VSyncThreadWin::OnSuspend() {
  AutoVSyncThreadLock auto_lock(this);
  is_suspended_ = true;
}

void VSyncThreadWin::OnResume() {
  AutoVSyncThreadLock auto_lock(this);
  is_suspended_ = false;
  PostTaskIfNeeded();
}

base::TimeDelta VSyncThreadWin::GetVsyncInterval() {
  base::TimeTicks vsync_timebase;
  base::TimeDelta vsync_interval;

  // This is a simplified initial approach to fix crbug.com/1456399
  // In Windows SV3 builds DWM will operate with per monitor refresh
  // rates. As a result of this, DwmGetCompositionTimingInfo is no longer
  // guaranteed to align with the primary monitor but will instead align
  // with the current highest refresh rate monitor. This can cause issues
  // in clients which may be waiting on the primary monitor's vblank as
  // the reported interval may no longer match with the vblank wait.
  // To work around this discrepancy get the VSync interval directly from
  // monitor associated with window_ or the primary monitor.
  static bool use_sv3_workaround =
      base::win::GetVersion() > base::win::Version::WIN11_22H2 &&
      base::FeatureList::IsEnabled(
          features::kUsePrimaryMonitorVSyncIntervalOnSV3);

  const bool get_vsync_params_succeeded =
      use_sv3_workaround
          ? vsync_provider_.GetVSyncIntervalIfAvailable(&vsync_interval)
          : vsync_provider_.GetVSyncParametersIfAvailable(&vsync_timebase,
                                                          &vsync_interval);
  DCHECK(get_vsync_params_succeeded);
  return vsync_interval;
}

void VSyncThreadWin::WaitForVSync() {
  base::TimeDelta vsync_interval = GetVsyncInterval();

  if (!dxgi_adapter_ || !DXGIFactoryIsCurrent(dxgi_adapter_.Get())) {
    TRACE_EVENT("gpu", "DXGIFactoryIsCurrent non-current factory");
    dxgi_adapter_ = FindDXGIAdapterOnNewFactory(original_adapter_luid_);
    primary_output_.Reset();
  }

  // From Raymond Chen's blog "How do I get a handle to the primary monitor?"
  // https://devblogs.microsoft.com/oldnewthing/20141106-00/?p=43683
  const HMONITOR monitor = MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY);
  if (primary_output_ &&
      !DXGIOutputIsOnMonitor(primary_output_.Get(), monitor)) {
    TRACE_EVENT("gpu", "DXGIOutputIsOnMonitor primary monitor changed");
    primary_output_.Reset();
  }

  if (!primary_output_ && dxgi_adapter_) {
    primary_output_ = DXGIOutputFromMonitor(monitor, dxgi_adapter_.Get());
  }

  const base::TimeTicks wait_for_vblank_start_time = base::TimeTicks::Now();
  const bool wait_for_vblank_succeeded =
      primary_output_ && SUCCEEDED(primary_output_->WaitForVBlank());

  // WaitForVBlank returns very early instead of waiting until vblank when the
  // monitor goes to sleep.  We use 1ms as a threshold for the duration of
  // WaitForVBlank and fallback to Sleep() if it returns before that.  This
  // could happen during normal operation for the first call after the vsync
  // thread becomes non-idle, but it shouldn't happen often.
  constexpr auto kVBlankIntervalThreshold = base::Milliseconds(1);
  const base::TimeDelta wait_for_vblank_elapsed_time =
      base::TimeTicks::Now() - wait_for_vblank_start_time;
  if (!wait_for_vblank_succeeded ||
      wait_for_vblank_elapsed_time < kVBlankIntervalThreshold) {
    TRACE_EVENT2("gpu", "WaitForVSync Sleep", "has adapter", !!dxgi_adapter_,
                 "has output", !!primary_output_);
    base::Time::ActivateHighResolutionTimer(true);
    Sleep(static_cast<DWORD>(vsync_interval.InMillisecondsRoundedUp()));
    base::Time::ActivateHighResolutionTimer(false);
  }

  AutoVSyncThreadLock auto_lock(this);
  DCHECK(is_vsync_task_posted_);
  is_vsync_task_posted_ = false;
  PostTaskIfNeeded();

  const base::TimeTicks vsync_time = base::TimeTicks::Now();
  for (VSyncObserver& obs : observers_) {
    obs.OnVSync(vsync_time, vsync_interval);
  }
}

}  // namespace gl