blob: 2b68500efeeb7163de55b50c7240c1cec2724b59 [file]
// Copyright 2026 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/ca_metal_display_link_mac.h"
#import <AppKit/AppKit.h>
#include <Metal/Metal.h>
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/trace_event/trace_event.h"
#include "ui/display/mac/screen_utils_mac.h"
API_AVAILABLE(macos(14.0))
@interface DisplayLinkDelegate : NSObject <CAMetalDisplayLinkDelegate>
- (instancetype)initWithLayer:(CAMetalLayer*)layer;
- (void)metalDisplayLink:(CAMetalDisplayLink*)link
needsUpdate:(CAMetalDisplayLinkUpdate*)update;
- (void)setCallback:
(base::RepeatingCallback<void(CAMetalDisplayLink*,
CAMetalDisplayLinkUpdate*)>)callback;
- (void)setPresentationCallback:
(base::RepeatingCallback<void(id<MTLDrawable>)>)callback;
@end
@implementation DisplayLinkDelegate {
base::RepeatingCallback<void(CAMetalDisplayLink*, CAMetalDisplayLinkUpdate*)>
_callback;
base::RepeatingCallback<void(id<MTLDrawable>)> _presentationCallback;
}
- (instancetype)initWithLayer:(CAMetalLayer*)layer {
if ((self = [super init])) {
layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
layer.framebufferOnly = YES;
if (!layer.device) {
layer.device = layer.preferredDevice;
}
}
return self;
}
- (void)metalDisplayLink:(CAMetalDisplayLink*)link
needsUpdate:(CAMetalDisplayLinkUpdate*)update {
if (!_presentationCallback.is_null()) {
id<CAMetalDrawable> drawable = [update drawable];
__weak DisplayLinkDelegate* weakSelf = self;
[drawable addPresentedHandler:^(id<MTLDrawable> d) {
// Convert weak reference back to strong to ensure 'self' exists
// for the duration of this block execution. Also ensure
// _presentationCallback has not been unregistered.
__strong DisplayLinkDelegate* strongSelf = weakSelf;
if (strongSelf && !strongSelf->_presentationCallback.is_null()) {
strongSelf->_presentationCallback.Run(d);
}
}];
}
_callback.Run(link, update);
}
- (void)setCallback:
(base::RepeatingCallback<void(CAMetalDisplayLink*,
CAMetalDisplayLinkUpdate*)>)callback {
_callback = callback;
}
- (void)setPresentationCallback:
(base::RepeatingCallback<void(id<MTLDrawable>)>)callback {
_presentationCallback = callback;
}
@end
namespace ui {
namespace {
// While the interval between consecutive VSync callback
// |update.targetTimestamp| parameters is typically very small (e.g. 2
// microseconds), we use 800 microseconds here. This provides sufficient margin
// to identify the current frame interval while remaining significantly lower
// than the 8333 microsecond interval of a 120Hz display. This fallback is rare
// and intended only for cases where VSync parameters are invalid.
bool AlmostEqual(base::TimeDelta a, base::TimeDelta b) {
return (a - b).magnitude() < base::Microseconds(800);
}
API_AVAILABLE(macos(14.0))
ui::VSyncParamsMac ComputeVSyncParametersMac(CAMetalDisplayLink* display_link,
CAMetalDisplayLinkUpdate* update,
base::TimeTicks last_target_time,
CGDirectDisplayID display_id) {
// |callback_time| is the time the callback function is called - current time.
base::TimeTicks callback_time =
base::TimeTicks() + base::Seconds(CACurrentMediaTime());
// Calculate the frame interval.
base::TimeTicks target_time =
base::TimeTicks() + base::Seconds(update.targetTimestamp);
base::TimeDelta interval = target_time - last_target_time;
if (target_time.is_null() || last_target_time.is_null() ||
!interval.is_positive()) {
interval = display::GetNSScreenRefreshInterval(display_id);
}
// The time the system estimates until the display of the next frame.
base::TimeTicks target_presented_time =
base::TimeTicks() + base::Seconds(update.targetPresentationTimestamp);
if (target_presented_time.is_null()) {
// Just assign a value as we observe 5 frames away @ 120Hz and 2 frames
// aways @ 60Hz in our tests.
if (AlmostEqual(interval, base::Hertz(120))) {
target_presented_time = callback_time + interval * 5;
} else {
target_presented_time = callback_time + interval * 2;
}
}
ui::VSyncParamsMac params;
params.callback_times_valid = true;
params.callback_timebase = callback_time;
params.callback_interval = interval;
params.display_times_valid = true;
params.display_timebase = target_presented_time;
params.display_interval = interval;
params.drawable_id = update.drawable.drawableID;
return params;
}
} // namespace
struct MetalObjCState {
CAMetalDisplayLink* __strong display_link API_AVAILABLE(macos(14.0));
DisplayLinkDelegate* __strong delegate API_AVAILABLE(macos(14.0));
};
void CAMetalDisplayLinkMac::MetalDisplayLinkCallback(
CAMetalDisplayLink* display_link,
CAMetalDisplayLinkUpdate* update) {
TRACE_EVENT0("ui", "MetalDisplayLinkCallback");
if (@available(macos 14.0, *)) {
if (!vsync_callback_) {
return;
}
ui::VSyncParamsMac params = ComputeVSyncParametersMac(
display_link, update, last_target_time_, display_id_);
// UnregisterCallback() might be called while running the callbacks.
vsync_callback_->callback_for_displaylink_thread_.Run(params);
last_target_time_ =
base::TimeTicks() + base::Seconds(update.targetTimestamp);
last_target_time_is_valid_ = true;
}
}
void CAMetalDisplayLinkMac::MetalPresentationCallback(
id<MTLDrawable> drawable) {
if (!presented_callback_) {
return;
}
base::TimeTicks presented_time =
base::TimeTicks() + base::Seconds(drawable.presentedTime);
presented_callback_->callback_for_displaylink_thread_.Run(drawable.drawableID,
presented_time);
}
base::TimeDelta CAMetalDisplayLinkMac::GetRefreshInterval() const {
return display::GetNSScreenRefreshInterval(display_id_);
}
void CAMetalDisplayLinkMac::GetRefreshIntervalRange(
base::TimeDelta& min_interval,
base::TimeDelta& max_interval,
base::TimeDelta& granularity) const {
display::GetNSScreenRefreshIntervalRange(display_id_, min_interval,
max_interval, granularity);
}
// static
scoped_refptr<DisplayLinkMac> CAMetalDisplayLinkMac::GetForDisplay(
CGDirectDisplayID display_id) {
NOTREACHED();
}
// static
scoped_refptr<DisplayLinkMac> CAMetalDisplayLinkMac::MetalGetForDisplay(
CGDirectDisplayID display_id,
CAMetalLayer* metal_layer) {
if (@available(macos 14.0, *)) {
scoped_refptr<CAMetalDisplayLinkMac> display_link(
new CAMetalDisplayLinkMac(display_id));
auto* objc_state = display_link->objc_state_.get();
objc_state->delegate =
[[DisplayLinkDelegate alloc] initWithLayer:metal_layer];
if (!objc_state->delegate) {
LOG(ERROR) << "CAMetalDisplayLinkMac::MetalGetForDisplay(). Fail to "
"alloc DisplayLinkDelegate";
}
objc_state->display_link =
[[CAMetalDisplayLink alloc] initWithMetalLayer:metal_layer];
objc_state->display_link.delegate = objc_state->delegate;
objc_state->display_link.preferredFrameLatency = 1.0f;
if (!objc_state->display_link) {
RecordDisplayLinkCreation(false);
return nullptr;
}
RecordDisplayLinkCreation(true);
// Pause CADisplaylink callback until a request for start.
objc_state->display_link.paused = YES;
[objc_state->display_link addToRunLoop:NSRunLoop.currentRunLoop
forMode:NSRunLoopCommonModes];
// Set the DisplayLinkDeletate's callback to call into the C++ code.
[objc_state->delegate
setCallback:base::BindRepeating(
&CAMetalDisplayLinkMac::MetalDisplayLinkCallback,
display_link->weak_factory_.GetWeakPtr())];
return display_link;
}
return nullptr;
}
base::TimeTicks CAMetalDisplayLinkMac::GetCurrentTime() const {
return base::TimeTicks() + base::Seconds(CACurrentMediaTime());
}
CAMetalDisplayLinkMac::CAMetalDisplayLinkMac(CGDirectDisplayID display_id)
: display_id_(display_id),
objc_state_(std::make_unique<MetalObjCState>()) {}
CAMetalDisplayLinkMac::~CAMetalDisplayLinkMac() {
// We must manually invalidate the CADisplayLink as its addToRunLoop keeps
// strong reference to its target. Thus, releasing our objc_state won't
// really result in destroying the object.
if (@available(macos 14.0, *)) {
if (objc_state_->display_link) {
[objc_state_->display_link invalidate];
}
}
}
std::unique_ptr<VSyncCallbackMac> CAMetalDisplayLinkMac::RegisterCallback(
VSyncCallbackMac::Callback callback) {
// Make CADisplayLink callbacks to run on the same RUNLOOP of the register
// thread without PostTask accross threads.
auto new_callback = base::WrapUnique(new VSyncCallbackMac(
base::BindOnce(&CAMetalDisplayLinkMac::UnregisterCallback, this),
std::move(callback), /*post_callback_to_ctor_thread=*/false));
vsync_callback_ = new_callback->weak_factory_.GetWeakPtr();
// Ensure that CADisplayLink is running.
if (@available(macos 14.0, *)) {
objc_state_->display_link.paused = NO;
}
return new_callback;
}
void CAMetalDisplayLinkMac::UnregisterCallback(VSyncCallbackMac* callback) {
vsync_callback_ = nullptr;
if (@available(macos 14.0, *)) {
objc_state_->display_link.paused = YES;
}
last_target_time_is_valid_ = false;
last_target_time_ = base::TimeTicks();
}
std::unique_ptr<PresentationCallbackMac>
CAMetalDisplayLinkMac::RegisterPresentationCallback(
PresentationCallbackMac::Callback callback) {
[objc_state_->delegate
setPresentationCallback:
base::BindRepeating(&CAMetalDisplayLinkMac::MetalPresentationCallback,
weak_factory_.GetWeakPtr())];
// Make CADisplayLink callbacks to run on the same RUNLOOP of the register
// thread without PostTask accross threads.
auto new_callback = base::WrapUnique(new PresentationCallbackMac(
base::BindOnce(&CAMetalDisplayLinkMac::UnregisterPresentationCallback,
this),
std::move(callback), /*post_callback_to_ctor_thread=*/false));
presented_callback_ = new_callback->weak_factory_.GetWeakPtr();
return new_callback;
}
void CAMetalDisplayLinkMac::UnregisterPresentationCallback(
PresentationCallbackMac* callback) {
[objc_state_->delegate
setPresentationCallback:base::RepeatingCallback<void(id<MTLDrawable>)>()];
presented_callback_ = nullptr;
}
} // namespace ui