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++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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