// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "ui/display/manager/configure_displays_task.h"
#include <cstddef>
#include <string>
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/sparse_histogram.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "ui/display/manager/util/display_manager_util.h"
#include "ui/display/types/display_configuration_params.h"
#include "ui/display/types/display_constants.h"
#include "ui/display/types/display_mode.h"
#include "ui/display/types/display_snapshot.h"
#include "ui/display/types/native_display_delegate.h"
namespace display {
namespace {
// The epsilon by which a refresh rate value may drift. For example:
// 239.76Hz --> 240Hz. This value was chosen with the consideration of the
// refresh rate value drifts presented in the "Video Formats—Video ID Code and
// Aspect Ratios" table on p.40 of the CTA-861-G standard.
constexpr float kRefreshRateEpsilon = 0.5f;
// Because we do not offer hardware mirroring, the maximal number of external
// displays that can be configured is limited by the number of available CRTCs,
// which is usually three. Since the lifetime of the UMA using this value is one
// year (exp. Nov. 2021), five buckets are more than enough for
// its histogram (between 0 to 4 external monitors).
constexpr int kMaxDisplaysCount = 5;
// Consolidates the UMA name prefix creation to one location, since it is used
// in many different call-sites.
const std::string GetUmaNamePrefixForRequest(
const DisplayConfigureRequest& request) {
return request.display->type() == DISPLAY_CONNECTION_TYPE_INTERNAL
? std::string("ConfigureDisplays.Internal.Modeset.")
: std::string("ConfigureDisplays.External.Modeset.");
}
// Find the next best mode that is smaller than |request->mode|. The next best
// mode is found by comparing resolutions, and if those are similar, comparing
// refresh rates. If no mode is found, return nullptr.
const DisplayMode* FindNextMode(const DisplayConfigureRequest& request) {
DCHECK(request.mode);
// Internal displays are restricted to their native mode. We do not
// attempt to downgrade their modes upon failure.
if (request.display->type() == DISPLAY_CONNECTION_TYPE_INTERNAL)
return nullptr;
if (request.display->modes().size() <= 1)
return nullptr;
const DisplayMode* best_mode = nullptr;
for (const auto& mode : request.display->modes()) {
if (*mode < *request.mode && (!best_mode || *mode > *best_mode))
best_mode = mode.get();
}
return best_mode;
}
void LogIfInvalidRequestForInternalDisplay(
const DisplayConfigureRequest& request) {
if (request.display->type() != DISPLAY_CONNECTION_TYPE_INTERNAL)
return;
if (request.mode == nullptr)
return;
if (request.display->native_mode() &&
*request.mode == *request.display->native_mode()) {
return;
}
LOG(ERROR) << "A mode other than the preferred mode was requested for the "
"internal display: preferred="
<< request.display->native_mode()->ToString()
<< " vs. requested=" << request.mode->ToString()
<< ". Current mode="
<< (request.display->current_mode()
? request.display->current_mode()->ToString()
: "nullptr (disabled)")
<< ".";
}
// Samples used to define buckets used by DisplayResolution enum.
// The enum is used to record screen resolution statistics.
const int32_t kDisplayResolutionSamples[] = {1024, 1280, 1440, 1920,
2560, 3840, 5120, 7680};
void UpdateResolutionUma(const DisplayConfigureRequest& request,
const std::string& uma_name) {
// Display is powered off.
if (!request.mode)
return;
// First, compute the index of the enum DisplayResolution.
// The index has to match the definition of the enum in enums.xml.
const uint32_t samples_list_size = std::size(kDisplayResolutionSamples);
const gfx::Size size = request.mode->size();
uint32_t width_idx = 0;
uint32_t height_idx = 0;
for (; width_idx < samples_list_size; width_idx++) {
if (size.width() <= kDisplayResolutionSamples[width_idx])
break;
}
for (; height_idx < samples_list_size; height_idx++) {
if (size.height() <= kDisplayResolutionSamples[height_idx])
break;
}
int display_resolution_index = 0;
if (width_idx == samples_list_size || height_idx == samples_list_size) {
// Check if we are in the overflow bucket.
display_resolution_index = samples_list_size * samples_list_size + 1;
} else {
// Compute the index of DisplayResolution, starting from 1, since 0 is used
// when powering off the display.
display_resolution_index = width_idx * samples_list_size + height_idx + 1;
}
base::UmaHistogramExactLinear(uma_name, display_resolution_index,
samples_list_size * samples_list_size + 2);
}
// A list of common refresh rates that are used to help fit approximate refresh
// rate values into one of the common refresh rate bins.
constexpr float kCommonDisplayRefreshRates[] = {
24.0, 25.0, 30.0, 45.0, 48.0, 50.0, 60.0, 75.0,
90.0, 100.0, 120.0, 144.0, 165.0, 200.0, 240.0};
void UpdateRefreshRateUma(const DisplayConfigureRequest& request,
const std::string& uma_name) {
// Display is powered off.
if (!request.mode)
return;
base::HistogramBase* histogram = base::SparseHistogram::FactoryGet(
uma_name, base::HistogramBase::kUmaTargetedHistogramFlag);
// Check if the refresh value is within an epsilon from one of the common
// refresh rate values.
for (float common_rate : kCommonDisplayRefreshRates) {
const bool is_within_epsilon = std::abs(request.mode->refresh_rate() -
common_rate) < kRefreshRateEpsilon;
if (is_within_epsilon) {
histogram->Add(common_rate);
return;
}
}
// Since this is not a common refresh rate value, report it as is.
histogram->Add(request.mode->refresh_rate());
}
void UpdateAttemptSucceededUma(
const std::vector<DisplayConfigureRequest>& requests,
bool display_success) {
for (const auto& request : requests) {
const std::string uma_name_prefix = GetUmaNamePrefixForRequest(request);
base::UmaHistogramBoolean(uma_name_prefix + "AttemptSucceeded",
display_success);
VLOG(2) << "Configured status=" << display_success
<< " display=" << request.display->display_id()
<< " origin=" << request.origin.ToString()
<< " mode=" << (request.mode ? request.mode->ToString() : "null")
<< " enable_vrr=" << request.enable_vrr;
}
}
void UpdateFinalStatusUma(
const std::vector<RequestAndStatusList>& requests_and_statuses,
ConfigureDisplaysTask::Status status) {
int mst_external_displays = 0;
size_t total_external_displays = requests_and_statuses.size();
for (auto& request_and_status : requests_and_statuses) {
const DisplayConfigureRequest* request = request_and_status.first;
// Is this display SST (single-stream vs. MST multi-stream).
const bool sst_display = request->display->base_connector_id() &&
request->display->path_topology().empty();
if (!sst_display)
mst_external_displays++;
if (request->display->type() == DISPLAY_CONNECTION_TYPE_INTERNAL)
total_external_displays--;
const std::string uma_name_prefix = GetUmaNamePrefixForRequest(*request);
if (request_and_status.second) {
UpdateResolutionUma(*request, uma_name_prefix + "Success.Resolution");
UpdateRefreshRateUma(*request, uma_name_prefix + "Success.RefreshRate");
}
base::UmaHistogramBoolean(uma_name_prefix + "FinalStatus",
request_and_status.second);
}
base::UmaHistogramEnumeration("ConfigureDisplays.Modeset.FinalTaskStatus",
status);
base::UmaHistogramExactLinear(
"ConfigureDisplays.Modeset.TotalExternalDisplaysCount",
base::checked_cast<int>(total_external_displays), kMaxDisplaysCount);
base::UmaHistogramExactLinear(
"ConfigureDisplays.Modeset.MstExternalDisplaysCount",
mst_external_displays, kMaxDisplaysCount);
if (total_external_displays > 0) {
const int mst_displays_percentage =
100.0 * mst_external_displays / total_external_displays;
UMA_HISTOGRAM_PERCENTAGE(
"ConfigureDisplays.Modeset.MstExternalDisplaysPercentage",
mst_displays_percentage);
}
}
// Updates properties of the |display| according to the given |request| after a
// successful configuration. The display ids of |display| and |request| must
// match.
void UpdateSnapshotAfterConfiguration(
DisplaySnapshot* display,
const DisplayConfigurationParams& request) {
CHECK_EQ(display->display_id(), request.id);
display->set_current_mode(request.mode.get());
display->set_origin(request.origin);
if (display->IsVrrCapable()) {
display->set_variable_refresh_rate_state(
request.enable_vrr ? VariableRefreshRateState::kVrrEnabled
: VariableRefreshRateState::kVrrDisabled);
}
}
} // namespace
DisplayConfigureRequest::DisplayConfigureRequest(DisplaySnapshot* display,
const DisplayMode* mode,
const gfx::Point& origin,
bool enable_vrr)
: display(display),
mode(mode ? mode->Clone() : nullptr),
origin(origin),
enable_vrr(enable_vrr) {}
DisplayConfigureRequest::DisplayConfigureRequest(DisplaySnapshot* display,
const DisplayMode* mode,
const gfx::Point& origin)
: DisplayConfigureRequest(display,
mode,
origin,
/*enable_vrr=*/false) {}
DisplayConfigureRequest::DisplayConfigureRequest(
const DisplayConfigureRequest& other)
: DisplayConfigureRequest(other.display,
other.mode.get(),
other.origin,
other.enable_vrr) {}
DisplayConfigureRequest::~DisplayConfigureRequest() = default;
ConfigureDisplaysTask::ConfigureDisplaysTask(
NativeDisplayDelegate* delegate,
const std::vector<DisplayConfigureRequest>& requests,
ResponseCallback callback,
ConfigurationType configuration_type)
: delegate_(delegate),
requests_(requests),
configuration_type_(configuration_type),
callback_(std::move(callback)),
task_status_(SUCCESS) {
delegate_->AddObserver(this);
}
ConfigureDisplaysTask::RequestToOriginalMode::RequestToOriginalMode(
DisplayConfigureRequest* request,
const DisplayMode* original_mode)
: request(request),
original_mode(original_mode ? original_mode->Clone() : nullptr) {}
ConfigureDisplaysTask::RequestToOriginalMode::RequestToOriginalMode(
const RequestToOriginalMode& other)
: RequestToOriginalMode(other.request, other.original_mode.get()) {}
ConfigureDisplaysTask::RequestToOriginalMode::~RequestToOriginalMode() =
default;
ConfigureDisplaysTask::~ConfigureDisplaysTask() {
delegate_->RemoveObserver(this);
}
void ConfigureDisplaysTask::Run() {
DCHECK(!requests_.empty());
const bool is_first_attempt = pending_display_group_requests_.empty();
std::vector<display::DisplayConfigurationParams> config_requests;
for (const auto& request : requests_) {
LogIfInvalidRequestForInternalDisplay(request);
config_requests.emplace_back(request.display->display_id(), request.origin,
request.mode.get(), request.enable_vrr);
if (is_first_attempt) {
const std::string uma_name_prefix = GetUmaNamePrefixForRequest(request);
UpdateResolutionUma(request,
uma_name_prefix + "OriginalRequest.Resolution");
}
}
const auto& on_configured =
is_first_attempt ? &ConfigureDisplaysTask::OnFirstAttemptConfigured
: &ConfigureDisplaysTask::OnRetryConfigured;
display::ModesetFlags modeset_flags{display::ModesetFlag::kTestModeset};
if (configuration_type_ == kConfigurationTypeSeamless)
modeset_flags.Put(display::ModesetFlag::kSeamlessModeset);
delegate_->Configure(
config_requests,
base::BindOnce(on_configured, weak_ptr_factory_.GetWeakPtr()),
modeset_flags);
}
void ConfigureDisplaysTask::OnConfigurationChanged() {}
void ConfigureDisplaysTask::OnDisplaySnapshotsInvalidated() {
// From now on, don't access |requests_[index]->display|; they're invalid.
task_status_ = ERROR;
weak_ptr_factory_.InvalidateWeakPtrs();
std::move(callback_).Run(task_status_);
}
void ConfigureDisplaysTask::OnFirstAttemptConfigured(
const std::vector<DisplayConfigurationParams>& request_results,
bool config_success) {
UpdateAttemptSucceededUma(requests_, config_success);
if (!config_success) {
// Partition |requests_| into smaller groups via
// |pending_display_group_requests_|, update the task's state, and initiate
// the retry logic. The next time |delegate_|->Configure() terminates
// OnRetryConfigured() will be executed instead.
PartitionRequests();
DCHECK(!pending_display_group_requests_.empty());
// Prep the first group
for (const auto& pair : pending_display_group_requests_.front()) {
pair.request->mode =
pair.original_mode ? pair.original_mode->Clone() : nullptr;
}
task_status_ = PARTIAL_SUCCESS;
Run();
return;
}
// This code execute only when the first modeset attempt fully succeeds.
// Submit the current |requests_| for modeset. Note that |requests_| is used
// directly instead of |request_results|, since that is what was tested (ozone
// sometimes alters the resulting requests to achieve better results during
// mode matching).
std::vector<display::DisplayConfigurationParams> config_requests;
for (const auto& request : requests_) {
final_requests_status_.emplace_back(&request, true);
config_requests.emplace_back(request.display->display_id(), request.origin,
request.mode.get(), request.enable_vrr);
}
display::ModesetFlags modeset_flags{display::ModesetFlag::kCommitModeset};
if (configuration_type_ == kConfigurationTypeSeamless)
modeset_flags.Put(display::ModesetFlag::kSeamlessModeset);
delegate_->Configure(config_requests,
base::BindOnce(&ConfigureDisplaysTask::OnConfigured,
weak_ptr_factory_.GetWeakPtr()),
modeset_flags);
}
void ConfigureDisplaysTask::OnRetryConfigured(
const std::vector<DisplayConfigurationParams>& request_results,
bool config_success) {
UpdateAttemptSucceededUma(requests_, config_success);
if (!config_success) {
// If one of the largest display request can be downgraded, try again.
// Otherwise this configuration task is a failure.
if (DowngradeDisplayRequestGroup()) {
Run();
return;
} else {
// Disable all displays in the current group, since we failed to find an
// alternative mode. Note that we skip modeset if the latest (or a
// single) pending group fails. There is no point in disabling displays
// that are already disabled from previous attempts and failed to change
// mode.
for (const auto& pair : pending_display_group_requests_.front()) {
pair.request->mode.reset();
}
task_status_ = ERROR;
}
} else {
// This configuration attempt passed test-modeset. Cache |requests_| so we
// can use it to modeset the displays once we are done testing, or if no
// other future attempts succeed. Note that |requests_| is used directly
// instead of |request_results|, since that is what was tested (ozone
// sometimes alters the resulting requests to achieve better results during
// mode matching).
last_successful_config_parameters_.clear();
for (const auto& request : requests_) {
last_successful_config_parameters_.emplace_back(
request.display->display_id(), request.origin, request.mode.get(),
request.enable_vrr);
}
}
// This code executes only when this display group request fully succeeds or
// fails to modeset. Update the final status of this group.
for (const auto& pair : pending_display_group_requests_.front())
final_requests_status_.emplace_back(pair.request, config_success);
// Subsequent modeset attempts will be done on the next pending display group,
// if one exists.
pending_display_group_requests_.pop();
if (!pending_display_group_requests_.empty()) {
// Prep the next group
for (const auto& pair : pending_display_group_requests_.front()) {
pair.request->mode =
pair.original_mode ? pair.original_mode->Clone() : nullptr;
}
Run();
return;
}
if (task_status_ == ERROR) {
LOG(WARNING) << "One or more of the connected display groups failed to "
"pass test-modeset entirely and will be disabled.";
if (last_successful_config_parameters_.empty()) {
LOG(ERROR) << "Display configuration failed. No modeset was attempted.";
UpdateFinalStatusUma(final_requests_status_, task_status_);
std::move(callback_).Run(task_status_);
return;
}
}
// Configure the displays using the last successful configuration parameter
// list.
display::ModesetFlags modeset_flags{display::ModesetFlag::kCommitModeset};
if (configuration_type_ == kConfigurationTypeSeamless)
modeset_flags.Put(display::ModesetFlag::kSeamlessModeset);
delegate_->Configure(last_successful_config_parameters_,
base::BindOnce(&ConfigureDisplaysTask::OnConfigured,
weak_ptr_factory_.GetWeakPtr()),
modeset_flags);
}
void ConfigureDisplaysTask::OnConfigured(
const std::vector<DisplayConfigurationParams>& request_results,
bool config_success) {
if (config_success) {
base::flat_map<int64_t, DisplaySnapshot*> snapshot_map;
for (const DisplayConfigureRequest& request : requests_) {
snapshot_map.emplace(request.display->display_id(), request.display);
}
// Use |request_results| to update the snapshots.
for (const DisplayConfigurationParams& request : request_results) {
const auto it = snapshot_map.find(request.id);
CHECK(it != snapshot_map.end());
UpdateSnapshotAfterConfiguration(it->second, request);
}
}
UpdateFinalStatusUma(final_requests_status_, task_status_);
std::move(callback_).Run(task_status_);
}
void ConfigureDisplaysTask::PartitionRequests() {
pending_display_group_requests_ = PartitionedRequestsQueue();
base::flat_set<uint64_t> handled_connectors;
for (size_t i = 0; i < requests_.size(); ++i) {
uint64_t connector_id = requests_[i].display->base_connector_id();
if (handled_connectors.find(connector_id) != handled_connectors.end())
continue;
std::vector<ConfigureDisplaysTask::RequestToOriginalMode> request_group;
for (size_t j = i; j < requests_.size(); ++j) {
if (connector_id == requests_[j].display->base_connector_id()) {
// Disable all requests in preparation increment connector retries after
// mapping them to their original request.
request_group.emplace_back(&requests_[j], requests_[j].mode.get());
requests_[j].mode.reset();
}
}
handled_connectors.insert(connector_id);
pending_display_group_requests_.push(request_group);
}
}
bool ConfigureDisplaysTask::DowngradeDisplayRequestGroup() {
auto cmp = [](DisplayConfigureRequest* lhs, DisplayConfigureRequest* rhs) {
return *lhs->mode < *rhs->mode;
};
std::priority_queue<DisplayConfigureRequest*,
std::vector<DisplayConfigureRequest*>, decltype(cmp)>
sorted_requests(cmp);
for (const auto& pair : pending_display_group_requests_.front()) {
if (pair.request->display->type() == DISPLAY_CONNECTION_TYPE_INTERNAL)
continue;
if (!pair.request->mode)
continue;
sorted_requests.push(pair.request);
}
// Fail if there are no viable candidates to downgrade
if (sorted_requests.empty())
return false;
while (!sorted_requests.empty()) {
DisplayConfigureRequest* next_request = sorted_requests.top();
sorted_requests.pop();
const DisplayMode* next_mode = FindNextMode(*next_request);
if (next_mode) {
next_request->mode = next_mode->Clone();
return true;
}
}
return false;
}
} // namespace display