// 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/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/win/windows_version.h"
#include "ui/gl/gl_features.h"
#include "ui/gl/gl_switches.h"
#include "ui/gl/vsync_thread_win_dcomp.h"
#include "ui/gl/vsync_thread_win_dxgi.h"

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

// static
VSyncThreadWin* VSyncThreadWin::GetInstance() {
  static VSyncThreadWin* vsync_thread = []() -> VSyncThreadWin* {
    if (features::UseCompositorClockVSyncInterval()) {
      return new VSyncThreadWinDComp();
    } else {
      return new VSyncThreadWinDXGI();
    }
  }();
  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() : vsync_thread_("GpuVSyncThread") {
  is_suspended_ = base::PowerMonitor::GetInstance()
                      ->AddPowerSuspendObserverAndReturnSuspendedState(this);
  vsync_thread_.StartWithOptions(
      base::Thread::Options(base::ThreadType::kPresentation));
}

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();
}

void VSyncThreadWin::WaitForVSync() {
  base::TimeDelta vsync_interval;
  bool should_sleep = false;

  if (auto fake_interval = switches::GetFakeVsyncIntervalFromCommandLine()) {
    // Skip the real hardware wait entirely, for either derived class
    // (VSyncThreadWinDXGI's vblank wait or VSyncThreadWinDComp's compositor
    // clock wait), and pace via Sleep() at the fake rate instead. Observers
    // are still notified through the same path a real vsync signal would
    // use below.
    vsync_interval = *fake_interval;
    should_sleep = true;
  } else {
    const base::TimeTicks wait_for_vsync_start_time = base::TimeTicks::Now();
    bool wait_succeeded = WaitForVSyncImpl(&vsync_interval);
    const base::TimeDelta wait_for_vsync_elapsed_time =
        base::TimeTicks::Now() - wait_for_vsync_start_time;

    // WaitForVBlank and DCompositionWaitForCompositorClock returns very early
    // instead of waiting until vblank when the monitor goes to sleep or is
    // unplugged (nothing to present due to desktop occlusion). We use 1ms as
    // a threshold for the duration of the wait functions 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);
    if (!wait_succeeded ||
        wait_for_vsync_elapsed_time < kVBlankIntervalThreshold) {
      should_sleep = true;
    }
  }

  if (should_sleep) {
    TRACE_EVENT0("gpu", "WaitForVSync Sleep");
    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();
  observers_.Notify(&VSyncObserver::OnVSync, vsync_time, vsync_interval);
}

}  // namespace gl
