910e62b5创建于 1月15日历史提交
// 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/ca_display_link_mac.h"

#import <AppKit/AppKit.h>
#import <QuartzCore/CADisplayLink.h>

#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/trace_event/trace_event.h"

namespace {

NSScreen* GetNSScreenFromDisplayID(CGDirectDisplayID display_id) {
  for (NSScreen* screen in NSScreen.screens) {
    CGDirectDisplayID screenNumber =
        [screen.deviceDescription[@"NSScreenNumber"] unsignedIntValue];
    if (screenNumber == display_id) {
      return screen;
    }
  }

  return nullptr;
}

}  // namespace

API_AVAILABLE(macos(14.0))
@interface CADisplayLinkTarget : NSObject {
  base::RepeatingClosure _callback;
}
- (void)step:(CADisplayLink*)displayLink;
- (void)setCallback:(base::RepeatingClosure)callback;
@end

@implementation CADisplayLinkTarget
- (void)step:(CADisplayLink*)displayLink {
  DCHECK(_callback);
  _callback.Run();
}

- (void)setCallback:(base::RepeatingClosure)callback {
  _callback = callback;
}

@end

namespace ui {

namespace {
API_AVAILABLE(macos(14.0))
ui::VSyncParamsMac ComputeVSyncParametersMac(CADisplayLink* display_link) {
  // The time interval that represents when the last frame displayed.
  base::TimeTicks callback_time =
      base::TimeTicks() + base::Seconds(display_link.timestamp);
  // The time interval that represents when the next frame displays.
  base::TimeTicks next_callback_time =
      base::TimeTicks() + base::Seconds(display_link.targetTimestamp);

  bool times_valid = true;
  base::TimeDelta current_interval = next_callback_time - callback_time;

  // Sanity check.
  if (callback_time.is_null() || next_callback_time.is_null() ||
      !current_interval.is_positive()) {
    times_valid = false;
  }

  ui::VSyncParamsMac params;
  params.callback_times_valid = times_valid;
  params.callback_timebase = callback_time;
  params.callback_interval = current_interval;

  params.display_times_valid = times_valid;
  params.display_timebase = next_callback_time + 0.5 * current_interval;
  params.display_interval = current_interval;

  return params;
}
}  // namespace

struct ObjCState {
  CADisplayLink* __strong display_link API_AVAILABLE(macos(14.0));
  CADisplayLinkTarget* __strong target API_AVAILABLE(macos(14.0));
};

void CADisplayLinkMac::Step() {
  TRACE_EVENT0("ui", "CADisplayLinkCallback");

  if (@available(macos 14.0, *)) {
    // Allow extra callbacks before stopping CADisplayLink.
    if (!vsync_callback_) {
      consecutive_vsyncs_with_no_callbacks_ += 1;
      if (consecutive_vsyncs_with_no_callbacks_ >=
          VSyncCallbackMac::kMaxExtraVSyncs) {
        // It's time to stop CADisplayLink.
        objc_state_->display_link.paused = YES;
      }
      return;
    }

    consecutive_vsyncs_with_no_callbacks_ = 0;

    ui::VSyncParamsMac params =
        ComputeVSyncParametersMac(objc_state_->display_link);

    // UnregisterCallback() might be called while running the callbacks.
    vsync_callback_->callback_for_displaylink_thread_.Run(params);
  }
}

double CADisplayLinkMac::GetRefreshRate() const {
  NSScreen* screen = GetNSScreenFromDisplayID(display_id_);
  return 1.0 / screen.minimumRefreshInterval;
}

void CADisplayLinkMac::GetRefreshIntervalRange(
    base::TimeDelta& min_interval,
    base::TimeDelta& max_interval,
    base::TimeDelta& granularity) const {
  NSScreen* screen = GetNSScreenFromDisplayID(display_id_);
  min_interval = base::Seconds(1) * screen.minimumRefreshInterval;
  // No support for dynamic refresh range for now. Just return the minimum
  // interval instead of using screen.maximumRefreshInterval and
  // screen.displayUpdateGranularity.
  max_interval = min_interval;
  granularity = min_interval;
}

// static
scoped_refptr<DisplayLinkMac> CADisplayLinkMac::GetForDisplayOnCurrentThread(
    CGDirectDisplayID display_id) {
  if (@available(macos 14.0, *)) {
    scoped_refptr<CADisplayLinkMac> display_link(
        new CADisplayLinkMac(display_id));
    auto* objc_state = display_link->objc_state_.get();

    NSScreen* screen = GetNSScreenFromDisplayID(display_id);
    if (!screen) {
      return nullptr;
    }

    objc_state->target = [[CADisplayLinkTarget alloc] init];
    objc_state->display_link = [screen displayLinkWithTarget:objc_state->target
                                                    selector:@selector(step:)];

    if (!objc_state->display_link) {
      return nullptr;
    }

    // Pause CADisplaylink callback until a request for start.
    objc_state->display_link.paused = YES;

    // This display link interface requires the task executor of the current
    // thread (CrGpuMain or VizCompositorThread) to run with
    // MessagePumpType::NS_RUNLOOP. CADisplayLinkTarget and display_link are
    // NSObject, and MessagePumpType::DEFAULT does not support system work.
    // There will be no callbacks (CADisplayLinkTarget::step()) at all if
    // MessagePumpType NS_RUNLOOP is not chosen during thread initialization.
    [objc_state->display_link addToRunLoop:NSRunLoop.currentRunLoop
                                   forMode:NSDefaultRunLoopMode];

    // Set the CADisplayLinkTarget's callback to call back into the C++ code.
    [objc_state->target
        setCallback:base::BindRepeating(
                        &CADisplayLinkMac::Step,
                        display_link->weak_factory_.GetWeakPtr())];

    display_link->min_interval_ =
        base::Seconds(1) * screen.minimumRefreshInterval;
    display_link->max_interval_ = display_link->min_interval_;
    display_link->preferred_interval_ = display_link->min_interval_;

    return display_link;
  }

  return nullptr;
}

base::TimeTicks CADisplayLinkMac::GetCurrentTime() const {
  return base::TimeTicks() + base::Seconds(CACurrentMediaTime());
}

CADisplayLinkMac::CADisplayLinkMac(CGDirectDisplayID display_id)
    : display_id_(display_id), objc_state_(std::make_unique<ObjCState>()) {}

CADisplayLinkMac::~CADisplayLinkMac() {
  // 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> CADisplayLinkMac::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(&CADisplayLinkMac::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 CADisplayLinkMac::UnregisterCallback(VSyncCallbackMac* callback) {
  vsync_callback_ = nullptr;
}

}  // namespace ui