// 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.
#include "ash/display/refresh_rate_controller.h"
#include <string>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/shell.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/ui_base_features.h"
#include "ui/compositor/compositor.h"
#include "ui/display/display_features.h"
#include "ui/display/screen.h"
#include "ui/display/types/display_snapshot.h"
#include "ui/display/util/display_util.h"
namespace ash {
namespace {
const float kMinThrottledRefreshRate = 59.f;
using DisplayStateList = display::DisplayConfigurator::DisplayStateList;
using ModeState = DisplayPerformanceModeController::ModeState;
using RefreshRateOverrideMap =
display::DisplayConfigurator::RefreshRateOverrideMap;
std::string RefreshRatesToString(const std::vector<float>& refresh_rates) {
std::vector<std::string> entries;
for (auto refresh_rate : refresh_rates) {
entries.push_back(base::NumberToString(refresh_rate));
}
return "{" + base::JoinString(entries, ", ") + "}";
}
} // namespace
RefreshRateController::RefreshRateController(
display::DisplayConfigurator* display_configurator,
PowerStatus* power_status,
DisplayPerformanceModeController* display_performance_mode_controller)
: display_configurator_(display_configurator),
power_status_(power_status),
display_performance_mode_controller_(display_performance_mode_controller),
force_throttle_(
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kForceRefreshRateThrottle)) {
power_status_observer_.Observe(power_status);
display_configurator_observer_.Observe(display_configurator);
current_performance_mode_ =
display_performance_mode_controller_->AddObserver(this);
// Ensure initial states are calculated.
UpdateStates();
OnDisplayConfigurationChanged(display_configurator->cached_displays());
}
RefreshRateController::~RefreshRateController() {
display_performance_mode_controller_->RemoveObserver(this);
}
void RefreshRateController::OnPowerStatusChanged() {
UpdateStates();
}
void RefreshRateController::SetGameMode(aura::Window* window,
bool game_mode_on) {
// Update the |game_window_observer_|.
if (game_mode_on) {
if (game_window_observer_.GetSource() != window) {
game_window_observer_.Reset();
// The GameModeController will always turn off game mode before the
// observed window is destroyed.
game_window_observer_.Observe(window);
}
} else {
if (game_window_observer_.GetSource() == window) {
game_window_observer_.Reset();
} else {
DCHECK(!game_window_observer_.IsObserving());
// Game mode is already off. Nothing to do.
}
}
UpdateStates();
}
void RefreshRateController::OnWindowAddedToRootWindow(aura::Window* window) {
DCHECK_EQ(window, game_window_observer_.GetSource());
// Refresh state in case the window changed displays.
UpdateStates();
}
void RefreshRateController::OnWindowDestroying(aura::Window* window) {
DCHECK_EQ(window, game_window_observer_.GetSource());
game_window_observer_.Reset();
UpdateStates();
}
void RefreshRateController::OnDisplayConfigurationChanged(
const DisplayStateList& displays) {
for (const display::DisplaySnapshot* snapshot : displays) {
if (!snapshot->current_mode()) {
continue;
}
UpdateSeamlessRefreshRates(snapshot->display_id());
}
}
void RefreshRateController::StopObservingPowerStatusForTest() {
power_status_observer_.Reset();
power_status_ = nullptr;
}
void RefreshRateController::UpdateSeamlessRefreshRates(int64_t display_id) {
// Don't attempt dynamic refresh rate adjustment with hardware mirroring
// enabled.
if (display::features::IsHardwareMirrorModeEnabled()) {
return;
}
auto callback =
base::BindOnce(&RefreshRateController::OnSeamlessRefreshRatesReceived,
weak_ptr_factory_.GetWeakPtr(), display_id);
display_configurator_->GetSeamlessRefreshRates(display_id,
std::move(callback));
}
void RefreshRateController::OnSeamlessRefreshRatesReceived(
int64_t display_id,
const std::optional<std::vector<float>>& received_refresh_rates) {
VLOG(3) << "Received refresh rates for display " << display_id << ": "
<< (received_refresh_rates
? RefreshRatesToString(*received_refresh_rates)
: "empty");
if (!received_refresh_rates || received_refresh_rates->empty()) {
// These cases could occur if there is a race between requesting the refresh
// rates and some display topology change such as removing or disabling a
// display.
display_refresh_rates_.erase(display_id);
refresh_rate_preferences_.erase(display_id);
return;
}
// Sort in ascending order.
std::vector<float> refresh_rates = received_refresh_rates.value();
std::sort(refresh_rates.begin(), refresh_rates.end());
// If the received refresh rates are equal to the last received refresh rates,
// then we're done.
auto it = display_refresh_rates_.find(display_id);
if (it != display_refresh_rates_.end() && it->second == refresh_rates) {
return;
}
// Insert the new refresh rates, possibly replacing the old ones.
display_refresh_rates_[display_id] = std::move(refresh_rates);
refresh_rate_preferences_.erase(display_id);
RefreshOverrideState();
aura::Window* window = Shell::GetRootWindowForDisplayId(display_id);
if (window) {
window->GetHost()->compositor()->SetSeamlessRefreshRates(
display_refresh_rates_[display_id]);
}
}
void RefreshRateController::OnDisplayMetricsChanged(
const display::Display& display,
uint32_t changed_metrics) {
if (game_window_observer_.IsObserving() &&
(changed_metrics & DISPLAY_METRIC_PRIMARY)) {
// Refresh state in case the window is affected by the primary display
// change.
UpdateStates();
}
}
void RefreshRateController::OnDisplayPerformanceModeChanged(
ModeState new_state) {
current_performance_mode_ = new_state;
UpdateStates();
}
void RefreshRateController::UpdateStates() {
RefreshOverrideState();
RefreshVrrState();
}
void RefreshRateController::RefreshOverrideState() {
if (!base::FeatureList::IsEnabled(
ash::features::kSeamlessRefreshRateSwitching)) {
return;
}
// Don't attempt dynamic refresh rate adjustment with hardware mirroring
// enabled.
if (display::features::IsHardwareMirrorModeEnabled()) {
return;
}
RefreshRateOverrideMap refresh_rate_overrides = GetThrottleOverrides();
// Use preferred refresh rates instead of throttled refresh rates if present.
for (const auto& it : refresh_rate_preferences_) {
if (display_refresh_rates_.contains(it.first)) {
refresh_rate_overrides[it.first] = it.second;
}
}
display_configurator_->SetRefreshRateOverrides(refresh_rate_overrides);
}
RefreshRateOverrideMap RefreshRateController::GetThrottleOverrides() {
const ThrottleState throttle_state = GetDesiredThrottleState();
if (throttle_state == ThrottleState::kDisabled) {
return {};
}
// Update the override state for each display.
RefreshRateOverrideMap refresh_rate_overrides;
for (const auto& it : display_refresh_rates_) {
// Only throttle the internal display.
if (!display::IsInternalDisplayId(it.first)) {
continue;
}
// Filter out refresh rates lower than the minimum.
std::vector<float> throttle_candidates;
for (auto refresh_rate : it.second) {
if (refresh_rate >= kMinThrottledRefreshRate) {
throttle_candidates.push_back(refresh_rate);
}
}
if (throttle_candidates.size() < 2) {
VLOG(3) << "Fewer than 2 throttle candidates for display " << it.first;
continue;
}
refresh_rate_overrides[it.first] = throttle_candidates.front();
VLOG(3) << "Request refresh rate for display " << it.first << ": "
<< refresh_rate_overrides[it.first];
}
return refresh_rate_overrides;
}
void RefreshRateController::RefreshVrrState() {
// If VRR is always on, state will not need to be refreshed.
if (::features::IsVariableRefreshRateAlwaysOn()) {
return;
}
if (!::features::IsVariableRefreshRateEnabled()) {
return;
}
// Enable VRR on the borealis-hosting display if battery saver is inactive.
if (game_window_observer_.IsObserving() &&
current_performance_mode_ != ModeState::kPowerSaver) {
display_configurator_->SetVrrEnabled(
{display::Screen::GetScreen()
->GetDisplayNearestWindow(game_window_observer_.GetSource())
.id()});
} else {
display_configurator_->SetVrrEnabled({});
}
}
RefreshRateController::ThrottleState
RefreshRateController::GetDesiredThrottleState() {
if (force_throttle_) {
return ThrottleState::kEnabled;
}
switch (current_performance_mode_) {
case ModeState::kPowerSaver:
return ThrottleState::kEnabled;
case ModeState::kHighPerformance:
return ThrottleState::kDisabled;
case ModeState::kIntelligent:
return GetDynamicThrottleState();
default:
NOTREACHED();
}
}
RefreshRateController::ThrottleState
RefreshRateController::GetDynamicThrottleState() {
// Do not throttle when Borealis is active on the internal display.
if (game_window_observer_.IsObserving() &&
display::Screen::GetScreen()
->GetDisplayNearestWindow(game_window_observer_.GetSource())
.id() == display::Display::InternalDisplayId()) {
return ThrottleState::kDisabled;
}
if (power_status_->IsMainsChargerConnected()) {
return ThrottleState::kDisabled;
}
return ThrottleState::kEnabled;
}
void RefreshRateController::OnSetPreferredRefreshRate(
aura::WindowTreeHost* host,
float preferred_refresh_rate) {
CHECK(display::Screen::HasScreen());
const int64_t display_id = display::Screen::GetScreen()
->GetDisplayNearestWindow(host->window())
.id();
// Only honor preferences for the internal display.
if (!display::IsInternalDisplayId(display_id)) {
return;
}
// No change.
const auto& it = refresh_rate_preferences_.find(display_id);
if (it != refresh_rate_preferences_.end() &&
it->second == preferred_refresh_rate) {
return;
}
if (preferred_refresh_rate) {
refresh_rate_preferences_[display_id] = preferred_refresh_rate;
} else {
refresh_rate_preferences_.erase(display_id);
}
RefreshOverrideState();
}
void RefreshRateController::OnWindowTreeHostCreated(
aura::WindowTreeHost* host) {
host->AddObserver(this);
}
} // namespace ash