// Copyright 2024 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/display/mac/cv_display_link_mac.h"

#import <QuartzCore/CVDisplayLink.h>
#include <stdint.h>

#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/numerics/safe_conversions.h"
#include "base/synchronization/lock.h"
#include "base/task/bind_post_task.h"
#include "base/trace_event/trace_event.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
#include "ui/display/mac/screen_utils_mac.h"

namespace base::apple {

template <>
struct ScopedTypeRefTraits<CVDisplayLinkRef> {
  static CVDisplayLinkRef InvalidValue() { return nullptr; }
  static CVDisplayLinkRef Retain(CVDisplayLinkRef object) {
    return CVDisplayLinkRetain(object);
  }
  static void Release(CVDisplayLinkRef object) { CVDisplayLinkRelease(object); }
};

}  // namespace base::apple

namespace ui {

namespace {

struct DisplayLinkGlobals {
  // A map from (display ID, thread ID) pairs to raw CVDisplayLink pointers.
  std::map<std::pair<CGDirectDisplayID, base::PlatformThreadId>,
           raw_ptr<CVDisplayLinkMac>>
      GUARDED_BY(lock) map;

  // Making any calls to the CVDisplayLink API while `lock` is held can
  // result in deadlock, because `lock` is taken inside the CVDisplayLink
  // system callback.
  // https://crbug.com/1427235#c2
  base::Lock lock;

  // Indicate whether the display creation has been logged within the
  // 'Viz.ExternalBeginFrameSourceMac.DisplayLink.Create2 histogram.
  absl::flat_hash_set<CGDirectDisplayID> recorded_displays;

  static DisplayLinkGlobals& Get() {
    static base::NoDestructor<DisplayLinkGlobals> instance;
    return *instance;
  }
};

bool ComputeVSyncParameters(const CVTimeStamp& cv_time,
                            base::TimeTicks* timebase,
                            base::TimeDelta* interval) {
  // Verify that videoRefreshPeriod fits in 32 bits.
  DCHECK((cv_time.videoRefreshPeriod & ~0xFFFF'FFFFull) == 0ull);

  // Verify that the numerator and denominator make some sense.
  uint32_t numerator = static_cast<uint32_t>(cv_time.videoRefreshPeriod);
  uint32_t denominator = cv_time.videoTimeScale;
  if (numerator == 0 || denominator == 0) {
    LOG(WARNING) << "Unexpected numerator or denominator, bailing.";
    return false;
  }

  base::CheckedNumeric<int64_t> interval_us(base::Time::kMicrosecondsPerSecond);
  interval_us *= numerator;
  interval_us /= denominator;
  if (!interval_us.IsValid()) {
    LOG(DFATAL) << "Bailing due to overflow: "
                << base::Time::kMicrosecondsPerSecond << " * " << numerator
                << " / " << denominator;
    return false;
  }

  *timebase = base::TimeTicks::FromMachAbsoluteTime(cv_time.hostTime);
  *interval = base::Microseconds(int64_t{interval_us.ValueOrDie()});
  return true;
}

}  // namespace

// static
// Called by the system on the CVDisplayLink thread, and posts a call to the
// thread indicated in CVDisplayLinkMac::RegisterCallback().
CVReturn CVDisplayLinkMac::CVDisplayLinkCallback(CVDisplayLinkRef display_link,
                                                 const CVTimeStamp* now,
                                                 const CVTimeStamp* output_time,
                                                 CVOptionFlags flags_in,
                                                 CVOptionFlags* flags_out,
                                                 void* context) {
  // This function is called on the system display link thread.
  TRACE_EVENT0("gpu", "CVDisplayLinkCallback");

  // Convert the time parameters to our VSync parameters.
  VSyncParamsMac params;
  params.callback_times_valid = ComputeVSyncParameters(
      *now, &params.callback_timebase, &params.callback_interval);
  params.display_times_valid = ComputeVSyncParameters(
      *output_time, &params.display_timebase, &params.display_interval);

  // Post a task to the thread on which callbacks are to be run. It is safe to
  // access `display_link_mac` as a raw pointer because it is guaranteed that
  // this function will not be called after `display_link_mac->display_link_`
  // has been stopped (which happens in the CVDisplayLinkMac destructor).
  CVDisplayLinkMac* display_link_mac =
      reinterpret_cast<CVDisplayLinkMac*>(context);
  display_link_mac->task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&CVDisplayLinkMac::CVDisplayLinkCallbackOnCallbackThread,
                     display_link_mac->display_id_,
                     display_link_mac->thread_id_, params));
  return kCVReturnSuccess;
}

// static
void CVDisplayLinkMac::CVDisplayLinkCallbackOnCallbackThread(
    CGDirectDisplayID display_id,
    base::PlatformThreadId thread_id,
    const VSyncParamsMac& params) {
  scoped_refptr<CVDisplayLinkMac> display_link;

  // Look up the CVDisplayLinkMac that this call was made for. It may no longer
  // exist.
  {
    auto key = std::make_pair(display_id, base::PlatformThread::CurrentId());
    auto& globals = DisplayLinkGlobals::Get();
    base::AutoLock lock(globals.lock);
    auto found = globals.map.find(key);
    if (found == globals.map.end()) {
      return;
    }
    display_link = found->second;
  }

  display_link->RunCallbacks(params);
}

// static
void CVDisplayLinkMac::TryRecordDisplayLinkCreation(
    CGDirectDisplayID display_id,
    bool success) {
  auto& globals = DisplayLinkGlobals::Get();
  base::AutoLock lock(globals.lock);

  auto [it, inserted] = globals.recorded_displays.insert(display_id);
  if (inserted) {
    UMA_HISTOGRAM_BOOLEAN("Viz.DisplayLink.Create.GPU.CVDisplayLink", success);
    RecordDisplayLinkCreation(success);
  }
}

// static
scoped_refptr<CVDisplayLinkMac> CVDisplayLinkMac::GetForDisplay(
    CGDirectDisplayID display_id) {
  TRACE_EVENT("gpu", "CVDisplayLinkMac::GetForDisplay");
  const auto thread_id = base::PlatformThread::CurrentId();

  // If there already exists an object for this display on this thread, return
  // it.
  {
    auto& globals = DisplayLinkGlobals::Get();
    base::AutoLock lock(globals.lock);
    auto found = globals.map.find(std::make_pair(display_id, thread_id));
    if (found != globals.map.end()) {
      return found->second.get();
    }
  }

  // Create the CVDisplayLink object
  CVReturn ret = kCVReturnSuccess;
  base::apple::ScopedTypeRef<CVDisplayLinkRef> display_link;
  ret = CVDisplayLinkCreateWithCGDisplay(display_id,
                                         display_link.InitializeInto());

  if (ret != kCVReturnSuccess) {
    TryRecordDisplayLinkCreation(display_id, false);
    LOG(ERROR) << "CVDisplayLinkCreateWithCGDisplay failed. CVReturn: " << ret;
    return nullptr;
  }

  // Workaround for bug https://crbug.com/1218720. According to
  // https://hg.mozilla.org/releases/mozilla-esr68/rev/db0628eadb86,
  // CVDisplayLinkCreateWithCGDisplays()
  // (called by CVDisplayLinkCreateWithCGDisplay()) sometimes
  // creates a CVDisplayLinkRef with an uninitialized (nulled) internal
  // pointer. If we continue to use this CVDisplayLinkRef, we will
  // eventually crash in CVCGDisplayLink::getDisplayTimes(), where the
  // internal pointer is dereferenced. Fortunately, when this happens
  // another internal variable is also left uninitialized (zeroed),
  // which is accessible via CVDisplayLinkGetCurrentCGDisplay(). In
  // normal conditions the current display is never zero.
  if ((ret == kCVReturnSuccess) &&
      (CVDisplayLinkGetCurrentCGDisplay(display_link.get()) == 0)) {
    TryRecordDisplayLinkCreation(display_id, false);
    LOG(ERROR)
        << "CVDisplayLinkCreateWithCGDisplay failed (no current display)";
    return nullptr;
  }

  scoped_refptr<CVDisplayLinkMac> result =
      new CVDisplayLinkMac(display_id, thread_id, display_link);

  ret = CVDisplayLinkSetOutputCallback(display_link.get(),
                                       &CVDisplayLinkMac::CVDisplayLinkCallback,
                                       result.get());
  if (ret != kCVReturnSuccess) {
    TryRecordDisplayLinkCreation(display_id, false);
    LOG(ERROR) << "CVDisplayLinkSetOutputCallback failed. CVReturn: " << ret;
    return nullptr;
  }

  TryRecordDisplayLinkCreation(display_id, true);
  return result;
}

// Functions to call CVDisplayLinkStart and CVDisplayLinkStop. This is
// reference counted, and takes `display_link_running_lock_`.
bool CVDisplayLinkMac::EnsureDisplayLinkRunning() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!display_link_is_running_) {
    DCHECK(!CVDisplayLinkIsRunning(display_link_.get()));
    CVReturn ret = CVDisplayLinkStart(display_link_.get());
    // https://developer.apple.com/documentation/corevideo/cvdisplaylinkstart(_:)
    // If the specified display link is already running, CVDisplayLinkStart
    // returns an error.
    if (ret != kCVReturnSuccess &&
        !CVDisplayLinkIsRunning(display_link_.get())) {
      LOG(ERROR) << "CVDisplayLinkStart failed. CVReturn: " << ret;
      return false;
    }

    display_link_is_running_ = true;
  }

  return true;
}

// Called on the system CVDisplayLink thread.
void CVDisplayLinkMac::StopDisplayLinkIfNeeded() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Active VSync callbacks are still registered; keep the display link running.
  if (!callbacks_.empty()) {
    return;
  }

  // Stop has already been requested; nothing to do.
  if (!display_link_is_running_) {
    DCHECK(!CVDisplayLinkIsRunning(display_link_.get()));
    return;
  }

  CVReturn ret = CVDisplayLinkStop(display_link_.get());
  // https://developer.apple.com/documentation/corevideo/cvdisplaylinkstop(_:)
  // If the specified display link is already stopped, CVDisplayLinkStop returns
  // an error.
  // If `CVDisplayLinkStop` fails and callbacks are still fired,
  // `RunCallbacks()` will safely do nothing because `callbacks_` is now empty.
  // `CVDisplayLinkStop` will be attempted again in `~CVDisplayLinkMac()`
  if (ret != kCVReturnSuccess && CVDisplayLinkIsRunning(display_link_.get())) {
    LOG(ERROR) << "CVDisplayLinkStop failed. CVReturn: " << ret;
  }

  display_link_is_running_ = false;
}

base::TimeDelta CVDisplayLinkMac::GetRefreshInterval() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  CVTime cv_time =
      CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link_.get());
  if (!(cv_time.flags & kCVTimeIsIndefinite)) {
    double refresh_interval = (static_cast<double>(cv_time.timeValue) /
                               static_cast<double>(cv_time.timeScale));
    base::TimeDelta interval = base::Seconds(1) * refresh_interval;

    // A non-zero safeguard for the refresh interval.
    if (interval.is_positive()) {
      return interval;
    }
  }

  return base::Hertz(60);
}

void CVDisplayLinkMac::GetRefreshIntervalRange(
    base::TimeDelta& min_interval,
    base::TimeDelta& max_interval,
    base::TimeDelta& granularity) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Force the max/min range because CVDisplayLink does not support
  // preferredFrameRateRange.
  min_interval = max_interval = granularity = GetRefreshInterval();
}

base::TimeTicks CVDisplayLinkMac::GetCurrentTime() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  CVTimeStamp out_time;
  CVReturn ret = CVDisplayLinkGetCurrentTime(display_link_.get(), &out_time);
  if (ret == kCVReturnSuccess) {
    return base::TimeTicks::FromMachAbsoluteTime(out_time.hostTime);
  } else {
    return base::TimeTicks();
  }
}

void CVDisplayLinkMac::RunCallbacks(const VSyncParamsMac& params) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Make a local copy first because DisplayLink can be destroyed during the
  // callback.
  auto local_callbacks = callbacks_;
  for (auto* callback : local_callbacks) {
    callback->callback_for_displaylink_thread_.Run(params);
  }
}

CVDisplayLinkMac::CVDisplayLinkMac(
    CGDirectDisplayID display_id,
    base::PlatformThreadId thread_id,
    base::apple::ScopedTypeRef<CVDisplayLinkRef> display_link)
    : display_id_(display_id),
      thread_id_(thread_id),
      display_link_(display_link),
      task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {
  // Insert `this` into the global map. There must be no conflicting entry
  // present.
  {
    auto key = std::make_pair(display_id, base::PlatformThread::CurrentId());
    auto& globals = DisplayLinkGlobals::Get();
    base::AutoLock lock(globals.lock);
    auto result = globals.map.insert(std::make_pair(key, this));
    bool inserted = result.second;
    DCHECK(inserted);
  }
}

CVDisplayLinkMac::~CVDisplayLinkMac() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // All callbacks hold a reference to `this`, so there can be none.
  DCHECK(callbacks_.empty());

  // Stop the display link (if needed). After this call, it is safe to assume
  // that CVDisplayLinkMac::CVDisplayLinkCallback will not be called with
  // `this`.
  if (display_link_is_running_ || CVDisplayLinkIsRunning(display_link_.get())) {
    CVReturn ret = CVDisplayLinkStop(display_link_.get());
    if (ret != kCVReturnSuccess &&
        CVDisplayLinkIsRunning(display_link_.get())) {
      LOG(ERROR) << "CVDisplayLinkStop failed. CVReturn: " << ret;
    }
  }

  // Remove `this` from the global map. It must be present.
  {
    auto key = std::make_pair(display_id_, thread_id_);
    auto& globals = DisplayLinkGlobals::Get();
    base::AutoLock lock(globals.lock);
    auto found = globals.map.find(key);
    DCHECK(found != globals.map.end());
    DCHECK(found->second == this);
    globals.map.erase(found);
  }
}

std::unique_ptr<VSyncCallbackMac> CVDisplayLinkMac::RegisterCallback(
    VSyncCallbackMac::Callback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT("gpu", "CVDisplayLinkMac::RegisterCallback");

  // Check if this is in the fallback path for CADisplayLink
  bool post_callback_to_ctor_thread = !SkipPostTaskForCallbacks();

  // Make add the new callback. Register first before calling
  // EnsureDisplayLinkRunning() to ensure the callback function is available.
  std::unique_ptr<VSyncCallbackMac> new_callback =
      base::WrapUnique(new VSyncCallbackMac(
          base::BindOnce(&CVDisplayLinkMac::UnregisterCallback, this),
          std::move(callback), post_callback_to_ctor_thread));

  // Ensure that the DisplayLink is running. If something goes wrong, return
  // nullptr.
  if (!EnsureDisplayLinkRunning()) {
    new_callback.reset();
  } else {
    callbacks_.insert(new_callback.get());
  }

  return new_callback;
}

void CVDisplayLinkMac::UnregisterCallback(VSyncCallbackMac* callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  TRACE_EVENT("gpu", "CVDisplayLinkMac::UnregisterCallback");
  callbacks_.erase(callback);

  // Stop the display link if there are no more registered callbacks.
  StopDisplayLinkIfNeeded();
}

}  // namespace ui
