blob: eacefc5f71c5b764ce03fb85fa0fa58687de84df [file]
// 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/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.Create' 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
// 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) {
RecordDisplayLinkCreation(success);
}
}
// static
scoped_refptr<CVDisplayLinkMac> CVDisplayLinkMac::GetForDisplay(
CGDirectDisplayID display_id) {
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());
if (ret != kCVReturnSuccess) {
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_);
if (!callbacks_.empty()) {
consecutive_vsyncs_with_no_callbacks_ = 0;
return;
}
consecutive_vsyncs_with_no_callbacks_ += 1;
if (consecutive_vsyncs_with_no_callbacks_ <
VSyncCallbackMac::kMaxExtraVSyncs) {
return;
}
if (!display_link_is_running_) {
DCHECK(!CVDisplayLinkIsRunning(display_link_.get()));
return;
}
CVReturn ret = CVDisplayLinkStop(display_link_.get());
if (ret != kCVReturnSuccess) {
LOG(ERROR) << "CVDisplayLinkStop failed. CVReturn: " << ret;
}
display_link_is_running_ = false;
consecutive_vsyncs_with_no_callbacks_ = 0;
}
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));
return (base::Seconds(1) * refresh_interval);
} else {
return display::GetNSScreenRefreshInterval(display_id_);
}
}
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_);
for (auto* callback : callbacks_) {
callback->callback_for_displaylink_thread_.Run(params);
}
StopDisplayLinkIfNeeded();
}
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_) {
CVReturn ret = CVDisplayLinkStop(display_link_.get());
if (ret != kCVReturnSuccess) {
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_);
// 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=*/true));
// 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_);
callbacks_.erase(callback);
}
} // namespace ui