You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
libcutefish/clients/display/outputmodel.cpp

641 lines
21 KiB
C++

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Upstream: chromium/src/ash/display/cros_display_config.cc
// Revision: 7acd40ddefff49c9d36144999aafdbb674dbd07a
//
// Adapted for Cutefish: built on the compositor's native display requests.
#include "display/outputmodel.h"
#include <algorithm>
#include <charconv>
#include <climits>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <map>
#include <set>
namespace cutefish::display {
namespace {
struct Size
{
int width = 0;
int height = 0;
bool operator==(const Size&) const = default;
};
struct Rect
{
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool isEmpty() const { return width <= 0 || height <= 0; }
// At least one pixel in common; an empty rectangle intersects nothing.
bool intersects(const Rect& other) const
{
return !isEmpty() && !other.isEmpty() && x < other.x + other.width && other.x < x + width
&& y < other.y + other.height && other.y < y + height;
}
};
} // namespace
// qBound's order: the minimum wins when the bounds cross.
static int bound(int minimum, int value, int maximum)
{
return std::max(minimum, std::min(maximum, value));
}
OutputModel::OutputModel() = default;
std::string OutputModel::name(const Entry& entry) const
{
const auto plainName = [](const Entry& candidate) -> std::string {
if (candidate.physical.connectionType == 1)
return "Built-in Display";
// The compositor falls back to the connector when the EDID has no name.
return candidate.physical.model.empty() ? candidate.physical.connectorName : candidate.physical.model;
};
const std::string plain = plainName(entry);
for (const Entry& other : m_entries) {
// Two of the same monitor are told apart by where they are plugged in.
if (other.physical.sessionId != entry.physical.sessionId && plainName(other) == plain
&& !entry.physical.connectorName.empty() && plain != entry.physical.connectorName)
return plain + " (" + entry.physical.connectorName + ")";
}
return plain;
}
const Mode* OutputModel::selectedMode(const Entry& entry) const
{
return entry.modeIndex >= 0 && entry.modeIndex < int(entry.physical.modes.size())
? &entry.physical.modes[entry.modeIndex]
: nullptr;
}
std::vector<int> OutputModel::resolutionModes(const Entry& entry) const
{
std::vector<int> result;
std::vector<Size> seen;
for (int i = 0; i < int(entry.physical.modes.size()); ++i) {
const Mode& mode = entry.physical.modes[i];
const Size size{mode.width, mode.height};
if (std::find(seen.begin(), seen.end(), size) == seen.end()) {
seen.push_back(size);
result.push_back(i);
}
}
std::stable_sort(result.begin(), result.end(), [&entry](int a, int b) {
const Mode& x = entry.physical.modes[a];
const Mode& y = entry.physical.modes[b];
return int64_t(x.width) * x.height > int64_t(y.width) * y.height;
});
return result;
}
std::vector<int> OutputModel::refreshModes(const Entry& entry) const
{
std::vector<int> result;
const Mode* selected = selectedMode(entry);
if (!selected)
return result;
for (int i = 0; i < int(entry.physical.modes.size()); ++i) {
const Mode& mode = entry.physical.modes[i];
if (mode.width == selected->width && mode.height == selected->height)
result.push_back(i);
}
std::stable_sort(result.begin(), result.end(), [&entry](int a, int b) {
return entry.physical.modes[a].refreshMilliHz < entry.physical.modes[b].refreshMilliHz;
});
return result;
}
std::string OutputModel::name(int row) const
{
return isRow(row) ? name(m_entries[row]) : std::string();
}
std::string OutputModel::id(int row) const
{
return isRow(row) ? m_entries[row].physical.sessionId : std::string();
}
bool OutputModel::isEnabled(int row) const
{
return isRow(row) && m_entries[row].enabled;
}
bool OutputModel::isInternal(int row) const
{
return isRow(row) && m_entries[row].physical.connectionType == 1;
}
bool OutputModel::isPrimary(int row) const
{
return isRow(row) && m_entries[row].primary;
}
OutputModel::Point OutputModel::position(int row) const
{
return isRow(row) ? m_entries[row].position : Point();
}
int OutputModel::rotation(int row) const
{
if (!isRow(row))
return None;
const uint32_t rotation = m_entries[row].rotation;
return rotation == 0 ? None : rotation == 1 ? Left : rotation == 2 ? Inverted : Right;
}
double OutputModel::scale(int row) const
{
return isRow(row) ? m_entries[row].scale120 / 120.0 : 1.0;
}
std::vector<std::string> OutputModel::resolutions(int row) const
{
std::vector<std::string> result;
if (!isRow(row))
return result;
const Entry& entry = m_entries[row];
for (int index : resolutionModes(entry)) {
const Mode& mode = entry.physical.modes[index];
result.push_back(std::to_string(mode.width) + " × " + std::to_string(mode.height));
}
return result;
}
int OutputModel::resolutionIndex(int row) const
{
if (!isRow(row))
return 0;
const Entry& entry = m_entries[row];
const Mode* mode = selectedMode(entry);
const std::vector<int> resolutions = resolutionModes(entry);
for (int i = 0; i < int(resolutions.size()); ++i) {
const Mode& candidate = entry.physical.modes[resolutions[i]];
if (mode && candidate.width == mode->width && candidate.height == mode->height)
return i;
}
return 0;
}
int OutputModel::recommendedResolutionIndex(int row) const
{
if (!isRow(row))
return -1;
// The display's preferred mode, the one the compositor starts it in.
const Entry& entry = m_entries[row];
const std::vector<int> resolutions = resolutionModes(entry);
for (int i = 0; i < int(resolutions.size()); ++i) {
const Mode& mode = entry.physical.modes[resolutions[i]];
for (const Mode& preferred : entry.physical.modes) {
if (preferred.preferred && mode.width == preferred.width && mode.height == preferred.height)
return i;
}
}
return -1;
}
double OutputModel::recommendedScale(int row) const
{
return isRow(row) ? m_entries[row].physical.recommendedScale120 / 120.0 : 1.0;
}
std::vector<std::string> OutputModel::refreshRates(int row) const
{
std::vector<std::string> result;
if (!isRow(row))
return result;
const Entry& entry = m_entries[row];
for (int index : refreshModes(entry)) {
char text[32];
const auto end = std::to_chars(text, text + sizeof(text), entry.physical.modes[index].refreshMilliHz / 1000.0,
std::chars_format::fixed, 2);
result.push_back(std::string(text, end.ptr) + " Hz");
}
return result;
}
int OutputModel::refreshRateIndex(int row) const
{
if (!isRow(row))
return -1;
const std::vector<int> modes = refreshModes(m_entries[row]);
const auto found = std::find(modes.begin(), modes.end(), m_entries[row].modeIndex);
return found == modes.end() ? -1 : int(found - modes.begin());
}
std::vector<std::string> OutputModel::replicationSources(int row) const
{
std::vector<std::string> result{"Extend"};
if (!isRow(row))
return result;
for (const Entry& other : m_entries) {
if (other.physical.sessionId != m_entries[row].physical.sessionId)
result.push_back(name(other));
}
return result;
}
int OutputModel::replicationSourceIndex(int row) const
{
if (!isRow(row) || m_entries[row].mirrorSource.empty())
return 0;
const Entry& entry = m_entries[row];
int index = 1;
for (const Entry& other : m_entries) {
if (other.physical.sessionId == entry.physical.sessionId)
continue;
if (other.physical.sessionId == entry.mirrorSource)
return index;
++index;
}
return 0;
}
uint32_t OutputModel::capabilities(int row) const
{
return isRow(row) ? m_entries[row].physical.capabilities : 0;
}
std::vector<double> OutputModel::supportedScales() const
{
return {1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 4.0};
}
std::string OutputModel::drmConnector(int row) const
{
if (!isRow(row))
return {};
const Entry& entry = m_entries[row];
std::error_code error;
const std::filesystem::path device = std::filesystem::canonical(entry.physical.devicePath, error);
if (error || !device.string().starts_with("/dev/dri/card") || entry.physical.connectorName.empty())
return {};
return device.filename().string() + "-" + entry.physical.connectorName;
}
void OutputModel::markDirty()
{
changed.emit();
if (!m_dirty) {
m_dirty = true;
dirtyChanged.emit();
}
}
bool OutputModel::setEnabled(int row, bool enabled)
{
if (!isRow(row))
return false;
Entry& entry = m_entries[row];
if (!enabled) {
const auto count = std::count_if(m_entries.begin(), m_entries.end(), [](const Entry& e) { return e.enabled; });
if (count <= 1)
return false;
}
if (entry.enabled == enabled)
return false;
entry.enabled = enabled;
m_layoutDirty = true;
if (!entry.enabled && entry.primary) {
entry.primary = false;
for (Entry& candidate : m_entries) {
if (candidate.enabled && candidate.mirrorSource.empty()) {
candidate.primary = true;
break;
}
}
}
markDirty();
return true;
}
bool OutputModel::setPrimary(int row)
{
if (!isRow(row))
return false;
Entry& entry = m_entries[row];
if (!entry.enabled || entry.primary)
return false;
for (Entry& other : m_entries)
other.primary = false;
entry.primary = true;
markDirty();
return true;
}
bool OutputModel::setPosition(int row, Point position)
{
if (!isRow(row) || m_entries[row].position == position)
return false;
m_entries[row].position = position;
m_layoutDirty = true;
markDirty();
return true;
}
bool OutputModel::setRotation(int row, int rotation)
{
if (!isRow(row))
return false;
const uint32_t value = rotation == None ? 0
: rotation == Left ? 1
: rotation == Inverted ? 2
: rotation == Right ? 3
: 99;
if (value > 3 || m_entries[row].rotation == value)
return false;
m_entries[row].rotation = value;
markDirty();
return true;
}
bool OutputModel::setScale(int row, double scale)
{
if (!isRow(row))
return false;
const int scale120 = int(std::lround(scale * 120));
if (scale120 < 120 || scale120 > 480 || m_entries[row].scale120 == uint32_t(scale120))
return false;
m_entries[row].scale120 = uint32_t(scale120);
markDirty();
return true;
}
bool OutputModel::setResolutionIndex(int row, int index)
{
if (!isRow(row))
return false;
Entry& entry = m_entries[row];
const std::vector<int> choices = resolutionModes(entry);
if (index < 0 || index >= int(choices.size()))
return false;
const Mode& target = entry.physical.modes[choices[index]];
const Mode* current = selectedMode(entry);
const int currentRefresh = current ? current->refreshMilliHz : 0;
int best = choices[index];
int difference = INT_MAX;
for (int i = 0; i < int(entry.physical.modes.size()); ++i) {
const Mode& candidate = entry.physical.modes[i];
if (candidate.width == target.width && candidate.height == target.height) {
const int distance = std::abs(candidate.refreshMilliHz - currentRefresh);
if (distance < difference) {
best = i;
difference = distance;
}
}
}
if (entry.modeIndex == best)
return false;
entry.modeIndex = best;
markDirty();
return true;
}
bool OutputModel::setRefreshRateIndex(int row, int index)
{
if (!isRow(row))
return false;
Entry& entry = m_entries[row];
const std::vector<int> choices = refreshModes(entry);
if (index < 0 || index >= int(choices.size()) || entry.modeIndex == choices[index])
return false;
entry.modeIndex = choices[index];
markDirty();
return true;
}
bool OutputModel::setReplicationSourceIndex(int row, int index)
{
if (!isRow(row))
return false;
Entry& entry = m_entries[row];
std::string source;
int n = 1;
for (const Entry& other : m_entries) {
if (other.physical.sessionId == entry.physical.sessionId)
continue;
if (n++ == index) {
source = other.physical.sessionId;
break;
}
}
if (entry.mirrorSource == source)
return false;
entry.mirrorSource = source;
m_layoutDirty = true;
if (entry.primary && !entry.mirrorSource.empty()) {
// The compositor takes the primary only from a mirror source.
for (Entry& candidate : m_entries) {
if (candidate.physical.sessionId == entry.mirrorSource && candidate.enabled
&& candidate.mirrorSource.empty()) {
entry.primary = false;
candidate.primary = true;
break;
}
}
}
markDirty();
return true;
}
void OutputModel::setState(const DisplayClient::State& state)
{
m_applied = state;
m_dirty = false;
m_layoutDirty = false;
std::set<std::string> connected;
for (const PhysicalDisplay& physical : state.physical)
connected.insert(physical.sessionId);
const size_t oldCount = m_entries.size();
std::erase_if(m_entries, [&connected](const Entry& entry) { return !connected.contains(entry.physical.sessionId); });
bool rowsAdded = false;
for (const PhysicalDisplay& physical : state.physical) {
auto found = std::find_if(m_entries.begin(), m_entries.end(), [&physical](const Entry& entry) {
return entry.physical.sessionId == physical.sessionId;
});
Entry entry = found == m_entries.end() ? Entry() : *found;
entry.physical = physical;
entry.enabled = false;
entry.primary = false;
entry.mirrorSource.clear();
for (int i = 0; i < int(physical.modes.size()); ++i) {
if (physical.modes[i].current) {
entry.modeIndex = i;
break;
}
}
for (const LogicalDisplay& logical : state.logical) {
if (std::find(logical.destinations.begin(), logical.destinations.end(), physical.sessionId)
== logical.destinations.end())
continue;
entry.enabled = true;
entry.primary = logical.primary && logical.destinations.front() == physical.sessionId;
entry.position = {logical.x, logical.y};
entry.scale120 = logical.scale120;
entry.rotation = logical.rotation;
if (logical.destinations.front() != physical.sessionId)
entry.mirrorSource = logical.destinations.front();
break;
}
if (found == m_entries.end()) {
m_entries.push_back(std::move(entry));
rowsAdded = true;
} else {
*found = std::move(entry);
}
}
if (rowsAdded || m_entries.size() != oldCount)
rowsChanged.emit();
changed.emit();
dirtyChanged.emit();
}
void OutputModel::discard()
{
const DisplayClient::State applied = m_applied;
setState(applied);
}
bool OutputModel::needsConfirmation() const
{
for (const Entry& entry : m_entries) {
const Mode* mode = selectedMode(entry);
if (!entry.enabled || !mode || mode->current)
continue;
// A display being switched on has no current mode to leave behind.
for (const Mode& applied : entry.physical.modes) {
if (applied.current)
return true;
}
}
return false;
}
std::vector<DisplayRequest> OutputModel::requests() const
{
std::vector<DisplayRequest> result;
for (const Entry& entry : m_entries) {
const Mode* mode = selectedMode(entry);
if (!mode)
continue;
result.push_back({entry.physical.sessionId,
entry.enabled,
{mode->width, mode->height, mode->refreshMilliHz, mode->pixelClockKHz,
mode->horizontalTotal, mode->verticalTotal, mode->flags, mode->preferred, mode->name},
entry.scale120,
entry.rotation});
}
return result;
}
std::string OutputModel::primary() const
{
for (const Entry& entry : m_entries) {
if (entry.primary)
return entry.physical.sessionId;
}
return {};
}
std::vector<Placement> OutputModel::placements() const
{
if (!m_layoutDirty)
return m_applied.placements;
std::vector<Placement> result;
const std::string root = primary();
const Entry* parent = nullptr;
for (const Entry& entry : m_entries) {
if (entry.physical.sessionId == root)
parent = &entry;
}
if (!parent)
return result;
std::vector<const Entry*> placed{parent};
const auto logicalSize = [this](const Entry& entry) {
const Mode* mode = selectedMode(entry);
if (!mode)
return Size{1, 1};
Size size{mode->width, mode->height};
if (entry.rotation % 2)
std::swap(size.width, size.height);
const int scale = int(entry.scale120);
return Size{(size.width * 120 + scale - 1) / scale, (size.height * 120 + scale - 1) / scale};
};
std::map<std::string, Rect> resolved;
const Size rootSize = logicalSize(*parent);
resolved[parent->physical.sessionId] = {parent->position.x, parent->position.y, rootSize.width, rootSize.height};
for (const Entry& entry : m_entries) {
if (!entry.enabled || entry.physical.sessionId == root || !entry.mirrorSource.empty())
continue;
int bestDistance = INT_MAX;
Placement best;
Rect bestBounds;
const Size childSize = logicalSize(entry);
for (const Entry* candidate : placed) {
const Size parentSize = logicalSize(*candidate);
const Rect& parentRect = resolved[candidate->physical.sessionId];
const Point delta{entry.position.x - parentRect.x, entry.position.y - parentRect.y};
for (uint32_t edge = 0; edge < 4; ++edge) {
const bool horizontal = edge % 2;
const int childExtent = horizontal ? childSize.height : childSize.width;
const int parentExtent = horizontal ? parentSize.height : parentSize.width;
const int overlap = std::min(32, std::min(childExtent, parentExtent));
const int cross = horizontal ? delta.y : delta.x;
const int offset = bound(-childExtent + overlap, cross, parentExtent - overlap);
const int normal = edge == 0 ? -childSize.height
: edge == 1 ? parentSize.width
: edge == 2 ? parentSize.height
: -childSize.width;
const int distance = std::abs((horizontal ? delta.x : delta.y) - normal) + std::abs(cross - offset);
const Rect bounds{parentRect.x + (horizontal ? normal : offset),
parentRect.y + (horizontal ? offset : normal), childSize.width, childSize.height};
bool overlaps = false;
for (const auto& [id, occupied] : resolved)
overlaps |= occupied.intersects(bounds);
if (overlaps)
continue;
if (distance < bestDistance) {
bestDistance = distance;
best = {entry.physical.sessionId, candidate->physical.sessionId, edge, offset};
bestBounds = bounds;
}
}
}
result.push_back(best);
placed.push_back(&entry);
resolved[entry.physical.sessionId] = bestBounds;
}
return result;
}
MirrorGroups OutputModel::mirrorGroups() const
{
MirrorGroups result;
for (const Entry& entry : m_entries) {
if (!entry.enabled || entry.mirrorSource.empty())
continue;
auto group = std::find_if(result.begin(), result.end(), [&entry](const std::vector<std::string>& members) {
return members.front() == entry.mirrorSource;
});
if (group == result.end())
result.push_back({entry.mirrorSource, entry.physical.sessionId});
else
group->push_back(entry.physical.sessionId);
}
return result;
}
} // namespace cutefish::display