// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "components/viz/service/frame_sinks/external_begin_frame_source_ios.h"
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>
#include "base/apple/mach_logging.h"
#include "base/logging.h"
#include "base/numerics/checked_math.h"
#include "components/viz/common/frame_sinks/begin_frame_args.h"
namespace {
// ProMotion devices only support up to 120Hz.
constexpr float kMaxRefreshRate = 120;
constexpr float kMinimumRefreshRate =
(viz::BeginFrameArgs::MinInterval() == base::TimeDelta()
? 1
: 1 / viz::BeginFrameArgs::MinInterval().InSecondsF());
// Translates CFTimeInterval to absolute time.
uint64_t GetMachTimeFromSeconds(CFTimeInterval seconds) {
mach_timebase_info_data_t info;
kern_return_t kr = mach_timebase_info(&info);
MACH_DCHECK(kr == KERN_SUCCESS, kr) << "mach_timebase_info";
DCHECK(info.numer);
DCHECK(info.denom);
// From https://developer.apple.com/library/archive/qa/qa1643/_index.html.
base::CheckedNumeric<uint64_t> time(base::Time::kNanosecondsPerSecond *
seconds);
// Skip for fast path.
if (info.denom != info.numer) {
time *= info.denom;
time /= info.numer;
}
if (!time.IsValid()) {
DLOG(ERROR) << "Bailing due to overflow: "
<< base::Time::kNanosecondsPerSecond << " * " << seconds
<< " / " << info.numer << " * " << info.denom;
return 0;
}
return time.ValueOrDie();
}
} // namespace
@interface CADisplayLinkImpl : NSObject {
@private
// A timer object that helps to synchronize with the refresh rate of the
// display.
CADisplayLink* __strong _displayLink;
// Determines if a vsync listener is enabled.
bool _enabled;
// A client that receives vsync updates. Owns us.
raw_ptr<viz::ExternalBeginFrameSourceIOS> _client;
// Current preferred refresh rate in frames per second. The system may ignore
// this and, for example, throttle the frame rate. Please note that the frame
// rate that the system chooses will be rounded to the nearest factor of a
// maximum refresh rate of display. Eg, if a display supports 60Hz, the
// refresh rate might be rounded to 15, 20, 30, and 60 FPS respectively.
float _preferredRefreshRate;
// The maximum refresh rate that depends on a maximum supported refresh rate
// of a display that a device uses.
float _maximumRefreshRate;
}
@end
@implementation CADisplayLinkImpl
- (instancetype)initWithClient:(viz::ExternalBeginFrameSourceIOS*)client {
self = [super init];
if (self) {
_client = client;
// Create a CADisplayLink and suspend it until a request for begin frames
// comes.
_displayLink =
[CADisplayLink displayLinkWithTarget:self
selector:@selector(displayLinkDidFire:)];
_maximumRefreshRate = kMaxRefreshRate;
[self setPreferredInterval:base::Hertz(_maximumRefreshRate)];
[self setEnabled:false];
[_displayLink addToRunLoop:NSRunLoop.currentRunLoop
forMode:NSRunLoopCommonModes];
}
return self;
}
- (id)init {
NOTREACHED_IN_MIGRATION();
return nil;
}
- (void)setEnabled:(bool)enabled {
if (!_displayLink || _enabled == enabled) {
return;
}
_enabled = enabled;
// Resume or suspend the display link's notifications.
if (_enabled) {
_displayLink.paused = NO;
} else {
_displayLink.paused = YES;
}
}
- (void)invalidateDisplayLink {
[self setEnabled:false];
[_displayLink invalidate];
_displayLink = nil;
_client = nil;
}
- (void)setPreferredInterval:(base::TimeDelta)interval {
if (!_displayLink) {
return;
}
DCHECK_GE(interval, base::TimeDelta());
const float refresh_rate = 1 / interval.InSecondsF();
if (_preferredRefreshRate != refresh_rate) {
// The preferred refresh rate mustn't exceed the maximum one. The floating
// part can result in exceeding the maximum rate because of the division
// operation.
_preferredRefreshRate =
refresh_rate > _maximumRefreshRate ? _maximumRefreshRate : refresh_rate;
if (@available(iOS 15, *)) {
[_displayLink
setPreferredFrameRateRange:CAFrameRateRange{
.minimum = kMinimumRefreshRate,
.maximum = _maximumRefreshRate,
.preferred = _preferredRefreshRate}];
} else if (@available(iOS 10, *)) {
[_displayLink setPreferredFramesPerSecond:_preferredRefreshRate];
}
// _displayLink.frameInterval is used on iOS 3-10. However, these are pretty
// old iOS versions, which we are not targeting.
}
}
- (void)displayLinkDidFire:(CADisplayLink*)displayLink {
DCHECK(_client);
// Get the previous vsync time.
const base::TimeTicks vsync_time = base::TimeTicks::FromMachAbsoluteTime(
GetMachTimeFromSeconds(displayLink.timestamp));
// Get the next vsync time.
const base::TimeTicks next_vsync_time = base::TimeTicks::FromMachAbsoluteTime(
GetMachTimeFromSeconds(displayLink.targetTimestamp));
// An error happened. Skip.
if (vsync_time.is_null() || next_vsync_time.is_null()) {
return;
}
// Get the interval of the current vsync.
const base::TimeDelta vsync_interval = next_vsync_time - vsync_time;
// If interval is not positive, we have to skip this frame.
if (!vsync_interval.is_positive()) {
return;
}
_client->OnVSync(vsync_time, next_vsync_time, vsync_interval);
}
- (int64_t)maximumRefreshRate {
return _maximumRefreshRate;
}
@end
namespace viz {
struct ExternalBeginFrameSourceIOS::ObjCStorage {
CADisplayLinkImpl* __strong display_link_impl;
};
ExternalBeginFrameSourceIOS::ExternalBeginFrameSourceIOS(uint32_t restart_id)
: ExternalBeginFrameSource(this, restart_id),
objc_storage_(std::make_unique<ObjCStorage>()) {
objc_storage_->display_link_impl =
[[CADisplayLinkImpl alloc] initWithClient:this];
}
ExternalBeginFrameSourceIOS::~ExternalBeginFrameSourceIOS() {
// We must manually invalidate the CADisplayLink as its addToRunLoop keeps
// strong reference to its target. Thus, releasing our wrapper won't really
// result in destroying the object.
[objc_storage_->display_link_impl invalidateDisplayLink];
objc_storage_->display_link_impl = nil;
}
void ExternalBeginFrameSourceIOS::SetPreferredInterval(
base::TimeDelta interval) {
[objc_storage_->display_link_impl setPreferredInterval:interval];
}
base::TimeDelta ExternalBeginFrameSourceIOS::GetMaximumRefreshFrameInterval() {
const int64_t max_refresh_rate =
[objc_storage_->display_link_impl maximumRefreshRate];
if (max_refresh_rate <= 0) [[unlikely]] {
return BeginFrameArgs::DefaultInterval();
}
return base::Hertz(max_refresh_rate);
}
void ExternalBeginFrameSourceIOS::OnVSync(base::TimeTicks vsync_time,
base::TimeTicks next_vsync_time,
base::TimeDelta vsync_interval) {
OnBeginFrame(begin_frame_args_generator_.GenerateBeginFrameArgs(
source_id(), vsync_time, next_vsync_time, vsync_interval));
}
void ExternalBeginFrameSourceIOS::OnNeedsBeginFrames(bool needs_begin_frames) {
SetEnabled(needs_begin_frames);
}
void ExternalBeginFrameSourceIOS::SetEnabled(bool enabled) {
[objc_storage_->display_link_impl setEnabled:enabled];
}
} // namespace viz