930 lines
33 KiB
C++
930 lines
33 KiB
C++
#include "core/output.h"
|
|
#include "core/rendertarget.h"
|
|
#include "core/renderviewport.h"
|
|
#include "cursor.h"
|
|
#include "pointer_input.h"
|
|
#include "kcm/shortcuts.h"
|
|
#include "breezydesktopeffect.h"
|
|
#include "breezydesktopconfig.h"
|
|
#include "effect/effect.h"
|
|
#include "effect/effecthandler.h"
|
|
#include "opengl/glutils.h"
|
|
#include "xrdriveripc.h"
|
|
|
|
#include <kwin/main.h>
|
|
#include <core/outputbackend.h>
|
|
|
|
#include <functional>
|
|
#include <QAction>
|
|
#include <QBuffer>
|
|
#include <QFile>
|
|
#include <QFileSystemWatcher>
|
|
#include <QLoggingCategory>
|
|
#include <QQuickItem>
|
|
#include <QTimer>
|
|
#include <QDBusConnection>
|
|
#include <QDateTime>
|
|
|
|
#include <KGlobalAccel>
|
|
#include <KLocalizedString>
|
|
|
|
#include <algorithm>
|
|
|
|
Q_LOGGING_CATEGORY(KWIN_XR, "kwin.xr")
|
|
|
|
// A small DBus adaptor to expose effect controls to the KCM.
|
|
// Service is provided by KWin (org.kde.KWin). We only register an object path.
|
|
// Interface: com.xronlinux.BreezyDesktop, Path: /com/xronlinux/BreezyDesktop
|
|
namespace {
|
|
class BreezyDesktopDBusAdaptor : public QObject {
|
|
Q_OBJECT
|
|
Q_CLASSINFO("D-Bus Interface", "com.xronlinux.BreezyDesktop")
|
|
public:
|
|
explicit BreezyDesktopDBusAdaptor(KWin::BreezyDesktopEffect *effect)
|
|
: QObject(effect), m_effect(effect) {}
|
|
|
|
public Q_SLOTS:
|
|
QVariantList AddVirtualDisplay(int width, int height) {
|
|
m_effect->addVirtualDisplay(QSize(width, height));
|
|
return m_effect->listVirtualDisplays();
|
|
}
|
|
|
|
QVariantList ListVirtualDisplays() const {
|
|
return m_effect->listVirtualDisplays();
|
|
}
|
|
|
|
QVariantList RemoveVirtualDisplay(const QString &id) {
|
|
m_effect->removeVirtualDisplay(id);
|
|
return m_effect->listVirtualDisplays();
|
|
}
|
|
|
|
bool CurvedDisplaySupported() {
|
|
return m_effect->curvedDisplaySupported();
|
|
}
|
|
|
|
private:
|
|
KWin::BreezyDesktopEffect *m_effect;
|
|
};
|
|
} // namespace
|
|
|
|
namespace DataView
|
|
{
|
|
const QString SHM_DIR = QStringLiteral("/dev/shm");
|
|
const QString SHM_PATH = SHM_DIR + QStringLiteral("/breezy_desktop_imu");
|
|
|
|
// Helper constants and functions for shared memory buffer offsets
|
|
constexpr int UINT8_SIZE = sizeof(uint8_t);
|
|
constexpr int BOOL_SIZE = UINT8_SIZE;
|
|
constexpr int UINT_SIZE = sizeof(uint32_t);
|
|
constexpr int FLOAT_SIZE = sizeof(float);
|
|
|
|
// DataView info: [offset, size, count]
|
|
constexpr int OFFSET_INDEX = 0;
|
|
constexpr int SIZE_INDEX = 1;
|
|
constexpr int COUNT_INDEX = 2;
|
|
|
|
// Computes the end offset, exclusive
|
|
constexpr int dataViewEnd(const int info[3]) {
|
|
return info[OFFSET_INDEX] + info[SIZE_INDEX] * info[COUNT_INDEX];
|
|
}
|
|
|
|
constexpr int VERSION[3] = {0, UINT8_SIZE, 1};
|
|
constexpr int ENABLED[3] = {dataViewEnd(VERSION), BOOL_SIZE, 1};
|
|
constexpr int LOOK_AHEAD_CFG[3] = {dataViewEnd(ENABLED), FLOAT_SIZE, 4};
|
|
constexpr int DISPLAY_RES[3] = {dataViewEnd(LOOK_AHEAD_CFG), UINT_SIZE, 2};
|
|
constexpr int DISPLAY_FOV[3] = {dataViewEnd(DISPLAY_RES), FLOAT_SIZE, 1};
|
|
constexpr int LENS_DISTANCE_RATIO[3] = {dataViewEnd(DISPLAY_FOV), FLOAT_SIZE, 1};
|
|
constexpr int SBS_ENABLED[3] = {dataViewEnd(LENS_DISTANCE_RATIO), BOOL_SIZE, 1};
|
|
constexpr int CUSTOM_BANNER_ENABLED[3] = {dataViewEnd(SBS_ENABLED), BOOL_SIZE, 1};
|
|
constexpr int SMOOTH_FOLLOW_ENABLED[3] = {dataViewEnd(CUSTOM_BANNER_ENABLED), BOOL_SIZE, 1};
|
|
constexpr int SMOOTH_FOLLOW_ORIGIN_DATA[3] = {dataViewEnd(SMOOTH_FOLLOW_ENABLED), FLOAT_SIZE, 16};
|
|
constexpr int IMU_DATE_MS[3] = {dataViewEnd(SMOOTH_FOLLOW_ORIGIN_DATA), UINT_SIZE, 2};
|
|
constexpr int IMU_QUAT_ENTRIES = 4;
|
|
constexpr int IMU_QUAT_DATA[3] = {dataViewEnd(IMU_DATE_MS), FLOAT_SIZE, 4 * IMU_QUAT_ENTRIES};
|
|
constexpr int IMU_PARITY_BYTE[3] = {dataViewEnd(IMU_QUAT_DATA), UINT8_SIZE, 1};
|
|
constexpr int LENGTH = dataViewEnd(IMU_PARITY_BYTE);
|
|
}
|
|
|
|
namespace KWin
|
|
{
|
|
|
|
BreezyDesktopEffect::BreezyDesktopEffect()
|
|
{
|
|
qCCritical(KWIN_XR) << "\t\t\tBreezy - constructor";
|
|
qmlRegisterUncreatableType<BreezyDesktopEffect>("org.kde.kwin.effect.breezy_desktop", 1, 0, "BreezyDesktopEffect", QStringLiteral("BreezyDesktop cannot be created in QML"));
|
|
|
|
setupGlobalShortcut(
|
|
BreezyShortcuts::TOGGLE,
|
|
[this]() { this->toggle(); }
|
|
);
|
|
setupGlobalShortcut(
|
|
BreezyShortcuts::RECENTER,
|
|
[this]() { this->recenter(); }
|
|
);
|
|
setupGlobalShortcut(
|
|
BreezyShortcuts::TOGGLE_ZOOM_ON_FOCUS,
|
|
[this]() {
|
|
this->setZoomOnFocusEnabled(!m_zoomOnFocusEnabled);
|
|
}
|
|
);
|
|
setupGlobalShortcut(
|
|
BreezyShortcuts::TOGGLE_FOLLOW_MODE,
|
|
[this]() { this->toggleSmoothFollow(); }
|
|
);
|
|
setupGlobalShortcut(
|
|
BreezyShortcuts::CURSOR_TO_FOCUSED_DISPLAY,
|
|
[this]() { this->moveCursorToFocusedDisplay(); }
|
|
);
|
|
|
|
connect(effects, &EffectsHandler::cursorShapeChanged, this, &BreezyDesktopEffect::updateCursorImage);
|
|
updateCursorImage();
|
|
reconfigure(ReconfigureAll);
|
|
|
|
setSource(QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kwin/effects/breezy_desktop/qml/main.qml"))));
|
|
|
|
// Monitor the IMU file for changes, even if it doesn't exist at startup
|
|
m_shmDirectoryWatcher = new QFileSystemWatcher(this);
|
|
m_shmDirectoryWatcher->addPath(DataView::SHM_DIR);
|
|
|
|
m_shmFileWatcher = new QFileSystemWatcher(this);
|
|
|
|
// Setup file watcher with recreation detection
|
|
auto setupFileWatcher = [this]() {
|
|
if (QFile::exists(DataView::SHM_PATH) && (
|
|
m_imuTimestamp == 0 ||
|
|
QDateTime::currentMSecsSinceEpoch() - m_imuTimestamp > 50 || // file may have been deleted and recreated
|
|
!m_shmFileWatcher->files().contains(DataView::SHM_PATH)
|
|
)) {
|
|
m_shmFileWatcher->removePath(DataView::SHM_PATH);
|
|
disconnect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updateImuRotation);
|
|
m_shmFileWatcher->addPath(DataView::SHM_PATH);
|
|
connect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updateImuRotation);
|
|
}
|
|
};
|
|
|
|
// Handle directory changes (file creation/recreation)
|
|
connect(m_shmDirectoryWatcher, &QFileSystemWatcher::directoryChanged, this, setupFileWatcher);
|
|
|
|
// Initial setup
|
|
setupFileWatcher();
|
|
|
|
m_imuWatchdogTimer = new QTimer(this);
|
|
m_imuWatchdogTimer->setInterval(1000);
|
|
connect(m_imuWatchdogTimer, &QTimer::timeout, this, [this]() {
|
|
if (!m_enabled) return;
|
|
this->updateImuRotation();
|
|
});
|
|
m_imuWatchdogTimer->start();
|
|
|
|
m_cursorUpdateTimer = new QTimer(this);
|
|
connect(m_cursorUpdateTimer, &QTimer::timeout, this, &BreezyDesktopEffect::updateCursorPos);
|
|
m_cursorUpdateTimer->setInterval(16); // ~60Hz
|
|
m_cursorUpdateTimer->start();
|
|
|
|
// Register DBus object under KWin's session bus name
|
|
auto *adaptor = new BreezyDesktopDBusAdaptor(this);
|
|
const bool dbusOk = QDBusConnection::sessionBus().registerObject(
|
|
QStringLiteral("/com/xronlinux/BreezyDesktop"),
|
|
adaptor,
|
|
QDBusConnection::ExportAllSlots);
|
|
if (!dbusOk) {
|
|
qCWarning(KWIN_XR) << "Failed to register DBus object /com/xronlinux/BreezyDesktop";
|
|
}
|
|
}
|
|
|
|
BreezyDesktopEffect::~BreezyDesktopEffect()
|
|
{
|
|
qCCritical(KWIN_XR) << "\t\t\tBreezy - destructor";
|
|
if (m_shmFileWatcher) {
|
|
if (!DataView::SHM_PATH.isEmpty()) {
|
|
m_shmFileWatcher->removePath(DataView::SHM_PATH);
|
|
}
|
|
m_shmFileWatcher->deleteLater();
|
|
m_shmFileWatcher = nullptr;
|
|
}
|
|
if (m_shmDirectoryWatcher) {
|
|
m_shmDirectoryWatcher->deleteLater();
|
|
m_shmDirectoryWatcher = nullptr;
|
|
}
|
|
if (m_imuWatchdogTimer) {
|
|
m_imuWatchdogTimer->stop();
|
|
m_imuWatchdogTimer->deleteLater();
|
|
m_imuWatchdogTimer = nullptr;
|
|
}
|
|
deactivate();
|
|
}
|
|
|
|
void BreezyDesktopEffect::setupGlobalShortcut(const BreezyShortcuts::Shortcut &shortcut, std::function<void()> triggeredFunc) {
|
|
QAction *action = new QAction(this);
|
|
action->setObjectName(shortcut.actionName);
|
|
action->setText(shortcut.actionText);
|
|
KGlobalAccel::self()->setDefaultShortcut(action, {shortcut.shortcut});
|
|
KGlobalAccel::self()->setShortcut(action, {shortcut.shortcut});
|
|
connect(action, &QAction::triggered, this, triggeredFunc);
|
|
}
|
|
|
|
void BreezyDesktopEffect::recenter() {
|
|
QJsonObject flags;
|
|
flags.insert(QStringLiteral("recenter_screen"), true);
|
|
XRDriverIPC::instance().writeControlFlags(flags);
|
|
}
|
|
|
|
void BreezyDesktopEffect::setLookingAtScreenIndex(int index)
|
|
{
|
|
m_lookingAtScreenIndex = index;
|
|
if (m_smoothFollowEnabled) updateDriverSmoothFollowSettings();
|
|
}
|
|
|
|
void BreezyDesktopEffect::reconfigure(ReconfigureFlags)
|
|
{
|
|
BreezyDesktopConfig::self()->read();
|
|
setLookAheadOverride(BreezyDesktopConfig::lookAheadOverride());
|
|
setFocusedDisplayDistance(BreezyDesktopConfig::focusedDisplayDistance() / 100.0f);
|
|
setAllDisplaysDistance(BreezyDesktopConfig::allDisplaysDistance() / 100.0f);
|
|
setDisplaySpacing(BreezyDesktopConfig::displaySpacing() / 1000.0f);
|
|
setZoomOnFocusEnabled(BreezyDesktopConfig::zoomOnFocusEnabled());
|
|
setSmoothFollowThreshold(BreezyDesktopConfig::smoothFollowThreshold());
|
|
|
|
qreal horiz = BreezyDesktopConfig::displayHorizontalOffset() / 100.0f;
|
|
qreal vert = BreezyDesktopConfig::displayVerticalOffset() / 100.0f;
|
|
bool offsetchanged = false;
|
|
if (!qFuzzyCompare(m_displayHorizontalOffset, horiz)) { m_displayHorizontalOffset = horiz; offsetchanged = true; }
|
|
if (!qFuzzyCompare(m_displayVerticalOffset, vert)) { m_displayVerticalOffset = vert; offsetchanged = true; }
|
|
if (offsetchanged) Q_EMIT displayOffsetChanged();
|
|
|
|
int wrap = BreezyDesktopConfig::displayWrappingScheme();
|
|
int aaQuality = BreezyDesktopConfig::antialiasingQuality();
|
|
bool removeVD = BreezyDesktopConfig::removeVirtualDisplaysOnDisable();
|
|
bool mirrorPhysicalDisplays = BreezyDesktopConfig::mirrorPhysicalDisplays();
|
|
if (m_displayWrappingScheme != wrap) { m_displayWrappingScheme = wrap; Q_EMIT displayWrappingSchemeChanged(); }
|
|
if (m_antialiasingQuality != aaQuality) { m_antialiasingQuality = aaQuality; Q_EMIT antialiasingQualityChanged(); }
|
|
if (m_removeVirtualDisplaysOnDisable != removeVD) { m_removeVirtualDisplaysOnDisable = removeVD; Q_EMIT removeVirtualDisplaysOnDisableChanged(); }
|
|
if (m_mirrorPhysicalDisplays != mirrorPhysicalDisplays) { m_mirrorPhysicalDisplays = mirrorPhysicalDisplays; Q_EMIT mirrorPhysicalDisplaysChanged(); }
|
|
|
|
bool curved = BreezyDesktopConfig::curvedDisplay() && m_curvedDisplaySupported;
|
|
if (m_curvedDisplay != curved) { m_curvedDisplay = curved; Q_EMIT curvedDisplayChanged(); }
|
|
|
|
// this one doesn't have a signal, just always assign it
|
|
m_allDisplaysFollowMode = BreezyDesktopConfig::allDisplaysFollowMode();
|
|
}
|
|
|
|
QVariantMap BreezyDesktopEffect::initialProperties(Output *screen)
|
|
{
|
|
return QVariantMap{
|
|
{QStringLiteral("effect"), QVariant::fromValue(this)},
|
|
{QStringLiteral("targetScreen"), QVariant::fromValue(screen)}
|
|
};
|
|
}
|
|
|
|
int BreezyDesktopEffect::requestedEffectChainPosition() const
|
|
{
|
|
return 70;
|
|
}
|
|
|
|
void BreezyDesktopEffect::toggle()
|
|
{
|
|
if (isRunning()) {
|
|
qCCritical(KWIN_XR) << "\t\t\tBreezy - toggle - disabling";
|
|
disableDriver();
|
|
} else {
|
|
qCCritical(KWIN_XR) << "\t\t\tBreezy - toggle - enabling";
|
|
enableDriver();
|
|
}
|
|
}
|
|
|
|
void BreezyDesktopEffect::activate()
|
|
{
|
|
qCCritical(KWIN_XR) << "\t\t\tBreezy - activate";
|
|
|
|
if (!isRunning()) setRunning(true);
|
|
|
|
connect(effects, &EffectsHandler::cursorShapeChanged, this, &BreezyDesktopEffect::updateCursorImage);
|
|
m_cursorUpdateTimer->start();
|
|
|
|
// QuickSceneEffect grabs the keyboard and mouse input, which pulls focus away from the active window
|
|
// and doesn't allow for interaction with anything on the desktop. These two calls fix that.
|
|
effects->ungrabKeyboard();
|
|
effects->stopMouseInterception(this);
|
|
}
|
|
|
|
void BreezyDesktopEffect::deactivate()
|
|
{
|
|
qCCritical(KWIN_XR) << "\t\t\tBreezy - deactivate";
|
|
disconnect(effects, &EffectsHandler::cursorShapeChanged, this, &BreezyDesktopEffect::updateCursorImage);
|
|
m_cursorUpdateTimer->stop();
|
|
showCursor();
|
|
|
|
if (m_removeVirtualDisplaysOnDisable) {
|
|
for (auto it = m_virtualDisplays.begin(); it != m_virtualDisplays.end(); ++it) {
|
|
if (it->output) {
|
|
KWin::kwinApp()->outputBackend()->removeVirtualOutput(it->output);
|
|
}
|
|
}
|
|
m_virtualDisplays.clear();
|
|
}
|
|
|
|
setRunning(false);
|
|
}
|
|
|
|
void BreezyDesktopEffect::enableDriver()
|
|
{
|
|
qCCritical(KWIN_XR) << "\t\t\tBreezy - enableDriver";
|
|
QJsonObject newConfig = QJsonObject();
|
|
auto configJsonOpt = XRDriverIPC::instance().retrieveConfig();
|
|
if (configJsonOpt) {
|
|
newConfig = configJsonOpt.value();
|
|
}
|
|
newConfig.insert(QStringLiteral("disabled"), false);
|
|
newConfig.insert(QStringLiteral("output_mode"), QStringLiteral("external_only"));
|
|
newConfig.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop"));
|
|
XRDriverIPC::instance().writeConfig(newConfig);
|
|
}
|
|
|
|
void BreezyDesktopEffect::disableDriver()
|
|
{
|
|
qCCritical(KWIN_XR) << "\t\t\tBreezy - disableDriver";
|
|
QJsonObject newConfig = QJsonObject();
|
|
auto configJsonOpt = XRDriverIPC::instance().retrieveConfig();
|
|
if (configJsonOpt) {
|
|
newConfig = configJsonOpt.value();
|
|
}
|
|
newConfig.insert(QStringLiteral("external_mode"), QStringLiteral("none"));
|
|
XRDriverIPC::instance().writeConfig(newConfig);
|
|
}
|
|
|
|
void BreezyDesktopEffect::addVirtualDisplay(QSize size)
|
|
{
|
|
static int virtualDisplayCount = 0;
|
|
++virtualDisplayCount;
|
|
QString name = QStringLiteral("BreezyDesktop_%1").arg(virtualDisplayCount);
|
|
#if defined(KWIN_VERSION_ENCODED) && KWIN_VERSION_ENCODED >= 60290
|
|
QString description = QStringLiteral("Breezy Display %1x%2 (%3)").arg(size.width()).arg(size.height()).arg(virtualDisplayCount);
|
|
auto output = KWin::kwinApp()->outputBackend()->createVirtualOutput(name, description, size, 1.0);
|
|
#else
|
|
auto output = KWin::kwinApp()->outputBackend()->createVirtualOutput(name, size, 1.0);
|
|
#endif
|
|
if (output) {
|
|
VirtualOutputInfo info;
|
|
info.output = output;
|
|
info.id = name;
|
|
info.size = size;
|
|
m_virtualDisplays.insert(info.id, info);
|
|
}
|
|
}
|
|
|
|
QVariantList BreezyDesktopEffect::listVirtualDisplays() const {
|
|
QVariantList list;
|
|
for (auto it = m_virtualDisplays.constBegin(); it != m_virtualDisplays.constEnd(); ++it) {
|
|
const auto &info = it.value();
|
|
if (!info.output)
|
|
continue;
|
|
QVariantMap entry;
|
|
entry.insert(QStringLiteral("id"), info.id);
|
|
entry.insert(QStringLiteral("width"), info.size.width());
|
|
entry.insert(QStringLiteral("height"), info.size.height());
|
|
list.push_back(entry);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
bool BreezyDesktopEffect::removeVirtualDisplay(const QString &id) {
|
|
auto it = m_virtualDisplays.find(id);
|
|
if (it != m_virtualDisplays.end()) {
|
|
Output *output = it->output;
|
|
if (output) {
|
|
KWin::kwinApp()->outputBackend()->removeVirtualOutput(output);
|
|
}
|
|
m_virtualDisplays.erase(it);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool BreezyDesktopEffect::isEnabled() const {
|
|
return m_enabled;
|
|
}
|
|
|
|
void BreezyDesktopEffect::setEffectTargetScreenIndex(int index) {
|
|
if (m_effectTargetScreenIndex != index) {
|
|
m_effectTargetScreenIndex = index;
|
|
invalidateEffectOnScreenGeometryCache();
|
|
evaluateCursorOnScreenState(m_cursorPos, m_cursorPos);
|
|
}
|
|
}
|
|
|
|
bool BreezyDesktopEffect::isZoomOnFocusEnabled() const {
|
|
return m_zoomOnFocusEnabled;
|
|
}
|
|
|
|
void BreezyDesktopEffect::setZoomOnFocusEnabled(bool enabled) {
|
|
if (m_zoomOnFocusEnabled != enabled) {
|
|
m_zoomOnFocusEnabled = enabled;
|
|
if (m_zoomOnFocusEnabled && m_focusedDisplayDistance > m_allDisplaysDistance) {
|
|
setFocusedDisplayDistance(m_allDisplaysDistance);
|
|
BreezyDesktopConfig::setFocusedDisplayDistance(static_cast<int>(m_focusedDisplayDistance * 100.0f));
|
|
}
|
|
BreezyDesktopConfig::setZoomOnFocusEnabled(enabled);
|
|
BreezyDesktopConfig::self()->save();
|
|
Q_EMIT zoomOnFocusChanged();
|
|
}
|
|
}
|
|
|
|
void BreezyDesktopEffect::toggleSmoothFollow() {
|
|
QJsonObject flags;
|
|
flags.insert(QStringLiteral("toggle_breezy_desktop_smooth_follow"), true);
|
|
XRDriverIPC::instance().writeControlFlags(flags);
|
|
}
|
|
|
|
bool BreezyDesktopEffect::imuResetState() const {
|
|
return m_imuResetState;
|
|
}
|
|
|
|
QList<QQuaternion> BreezyDesktopEffect::imuRotations() const {
|
|
return m_imuRotations;
|
|
}
|
|
|
|
quint32 BreezyDesktopEffect::imuTimeElapsedMs() const {
|
|
return m_imuTimeElapsedMs;
|
|
}
|
|
|
|
quint64 BreezyDesktopEffect::imuTimestamp() const {
|
|
return m_imuTimestamp;
|
|
}
|
|
|
|
QList<qreal> BreezyDesktopEffect::lookAheadConfig() const {
|
|
return m_lookAheadConfig;
|
|
}
|
|
|
|
qreal BreezyDesktopEffect::lookAheadOverride() const {
|
|
return m_lookAheadOverride;
|
|
}
|
|
|
|
void BreezyDesktopEffect::setLookAheadOverride(qreal override) {
|
|
if (override != m_lookAheadOverride) {
|
|
m_lookAheadOverride = override;
|
|
Q_EMIT lookAheadOverrideChanged();
|
|
}
|
|
}
|
|
|
|
QList<quint32> BreezyDesktopEffect::displayResolution() const {
|
|
return m_displayResolution;
|
|
}
|
|
|
|
qreal BreezyDesktopEffect::focusedDisplayDistance() const {
|
|
return m_focusedDisplayDistance;
|
|
}
|
|
|
|
void BreezyDesktopEffect::setFocusedDisplayDistance(qreal distance) {
|
|
if (distance != m_focusedDisplayDistance) {
|
|
m_focusedDisplayDistance = std::clamp(distance, 0.2, m_allDisplaysDistance);
|
|
Q_EMIT focusedDisplayDistanceChanged();
|
|
|
|
if (m_smoothFollowEnabled) updateDriverSmoothFollowSettings();
|
|
}
|
|
}
|
|
|
|
qreal BreezyDesktopEffect::allDisplaysDistance() const {
|
|
return m_allDisplaysDistance;
|
|
}
|
|
|
|
void BreezyDesktopEffect::setAllDisplaysDistance(qreal distance) {
|
|
if (distance != m_allDisplaysDistance) {
|
|
qreal min = m_zoomOnFocusEnabled ? m_focusedDisplayDistance : 0.2;
|
|
m_allDisplaysDistance = std::clamp(distance, min, 2.5);
|
|
Q_EMIT allDisplaysDistanceChanged();
|
|
}
|
|
}
|
|
|
|
qreal BreezyDesktopEffect::displaySpacing() const {
|
|
return m_displaySpacing;
|
|
}
|
|
|
|
void BreezyDesktopEffect::setDisplaySpacing(qreal spacing) {
|
|
if (spacing != m_displaySpacing) {
|
|
m_displaySpacing = spacing;
|
|
Q_EMIT displaySpacingChanged();
|
|
}
|
|
}
|
|
|
|
qreal BreezyDesktopEffect::displayHorizontalOffset() const {
|
|
return m_displayHorizontalOffset;
|
|
}
|
|
|
|
qreal BreezyDesktopEffect::displayVerticalOffset() const {
|
|
return m_displayVerticalOffset;
|
|
}
|
|
|
|
int BreezyDesktopEffect::displayWrappingScheme() const {
|
|
return m_displayWrappingScheme;
|
|
}
|
|
|
|
qreal BreezyDesktopEffect::diagonalFOV() const {
|
|
return m_diagonalFOV;
|
|
}
|
|
|
|
qreal BreezyDesktopEffect::lensDistanceRatio() const {
|
|
return m_lensDistanceRatio;
|
|
}
|
|
|
|
bool BreezyDesktopEffect::sbsEnabled() const {
|
|
return m_sbsEnabled;
|
|
}
|
|
|
|
bool BreezyDesktopEffect::customBannerEnabled() const {
|
|
return m_customBannerEnabled;
|
|
}
|
|
|
|
int BreezyDesktopEffect::antialiasingQuality() const {
|
|
return m_antialiasingQuality;
|
|
}
|
|
|
|
bool BreezyDesktopEffect::removeVirtualDisplaysOnDisable() const {
|
|
return m_removeVirtualDisplaysOnDisable;
|
|
}
|
|
|
|
bool BreezyDesktopEffect::mirrorPhysicalDisplays() const {
|
|
return m_mirrorPhysicalDisplays;
|
|
}
|
|
|
|
bool BreezyDesktopEffect::curvedDisplay() const {
|
|
return m_curvedDisplay;
|
|
}
|
|
|
|
bool BreezyDesktopEffect::curvedDisplaySupported() const {
|
|
return m_curvedDisplaySupported;
|
|
}
|
|
|
|
void BreezyDesktopEffect::setCurvedDisplaySupported(bool supported) {
|
|
if (m_curvedDisplaySupported != supported) {
|
|
m_curvedDisplaySupported = supported;
|
|
Q_EMIT curvedDisplaySupportedChanged();
|
|
}
|
|
|
|
bool curvedDisplayEnabled = supported && BreezyDesktopConfig::curvedDisplay();
|
|
if (curvedDisplayEnabled != m_curvedDisplay) {
|
|
m_curvedDisplay = curvedDisplayEnabled;
|
|
Q_EMIT curvedDisplayChanged();
|
|
}
|
|
}
|
|
|
|
QList<QQuaternion> BreezyDesktopEffect::smoothFollowOrigin() const {
|
|
return m_smoothFollowOrigin;
|
|
}
|
|
|
|
bool BreezyDesktopEffect::smoothFollowEnabled() const {
|
|
// the effect doesn't need to know about smooth follow if it's in "all displays" mode
|
|
return m_focusedSmoothFollowEnabled;
|
|
}
|
|
|
|
bool BreezyDesktopEffect::checkParityByte(const char* data) {
|
|
const uint8_t parityByte = static_cast<uint8_t>(data[DataView::IMU_PARITY_BYTE[DataView::OFFSET_INDEX]]);
|
|
uint8_t parity = 0;
|
|
|
|
const int dateBytes = DataView::IMU_DATE_MS[DataView::COUNT_INDEX] * DataView::IMU_DATE_MS[DataView::SIZE_INDEX];
|
|
for (int i = 0; i < dateBytes; ++i) {
|
|
parity ^= static_cast<uint8_t>(data[DataView::IMU_DATE_MS[DataView::OFFSET_INDEX] + i]);
|
|
}
|
|
|
|
const int quatBytes = DataView::IMU_QUAT_DATA[DataView::COUNT_INDEX] * DataView::IMU_QUAT_DATA[DataView::SIZE_INDEX];
|
|
for (int i = 0; i < quatBytes; ++i) {
|
|
parity ^= static_cast<uint8_t>(data[DataView::IMU_QUAT_DATA[DataView::OFFSET_INDEX] + i]);
|
|
}
|
|
|
|
return parityByte == parity;
|
|
}
|
|
|
|
static qint64 lastConfigUpdate = 0;
|
|
static qint64 activatedAt = 0;
|
|
void BreezyDesktopEffect::updateImuRotation() {
|
|
// Reentrancy guard: if an update is already in progress, skip
|
|
bool expected = false;
|
|
if (!m_imuUpdateInProgress.compare_exchange_strong(expected, true)) {
|
|
return;
|
|
}
|
|
|
|
// destructor called on function exit, triggers reset of the flag
|
|
struct ResetFlag { std::atomic<bool>* f; ~ResetFlag(){ f->store(false); } } reset{&m_imuUpdateInProgress};
|
|
|
|
const QString shmPath = QStringLiteral("/dev/shm/breezy_desktop_imu");
|
|
QFile shmFile(shmPath);
|
|
if (!shmFile.open(QIODevice::ReadOnly)) {
|
|
return;
|
|
}
|
|
QByteArray buffer = shmFile.readAll();
|
|
shmFile.close();
|
|
if (buffer.size() != DataView::LENGTH) return;
|
|
|
|
const char* data = buffer.constData();
|
|
if (!checkParityByte(data)) return;
|
|
|
|
uint8_t version = static_cast<uint8_t>(data[DataView::VERSION[DataView::OFFSET_INDEX]]);
|
|
uint8_t enabledFlag = static_cast<uint8_t>(data[DataView::ENABLED[DataView::OFFSET_INDEX]]);
|
|
uint64_t imuDateMs;
|
|
memcpy(&imuDateMs, data + DataView::IMU_DATE_MS[DataView::OFFSET_INDEX], sizeof(imuDateMs));
|
|
imuDateMs = qFromLittleEndian(imuDateMs);
|
|
|
|
const qint64 currentTimeMs = QDateTime::currentMSecsSinceEpoch();
|
|
const bool updateConfig = lastConfigUpdate == 0 || currentTimeMs - lastConfigUpdate > 1000;
|
|
|
|
if (updateConfig) {
|
|
float lookAheadConfig[4];
|
|
memcpy(&lookAheadConfig[0], data + DataView::LOOK_AHEAD_CFG[DataView::OFFSET_INDEX], sizeof(lookAheadConfig));
|
|
m_lookAheadConfig.clear();
|
|
m_lookAheadConfig.append(lookAheadConfig[0]);
|
|
m_lookAheadConfig.append(lookAheadConfig[1]);
|
|
m_lookAheadConfig.append(lookAheadConfig[2]);
|
|
m_lookAheadConfig.append(lookAheadConfig[3]);
|
|
|
|
uint32_t displayResolution[2];
|
|
memcpy(&displayResolution[0], data + DataView::DISPLAY_RES[DataView::OFFSET_INDEX], sizeof(displayResolution));
|
|
m_displayResolution.clear();
|
|
m_displayResolution.append(displayResolution[0]);
|
|
m_displayResolution.append(displayResolution[1]);
|
|
|
|
float displayFov = 0.0f;
|
|
memcpy(&displayFov, data + DataView::DISPLAY_FOV[DataView::OFFSET_INDEX], sizeof(displayFov));
|
|
m_diagonalFOV = displayFov;
|
|
|
|
float lensDistanceRatio = 0.0f;
|
|
memcpy(&lensDistanceRatio, data + DataView::LENS_DISTANCE_RATIO[DataView::OFFSET_INDEX], sizeof(lensDistanceRatio));
|
|
m_lensDistanceRatio = lensDistanceRatio;
|
|
|
|
uint8_t sbsEnabled = false;
|
|
memcpy(&sbsEnabled, data + DataView::SBS_ENABLED[DataView::OFFSET_INDEX], sizeof(sbsEnabled));
|
|
m_sbsEnabled = (sbsEnabled != 0);
|
|
|
|
uint8_t customBannerEnabled = false;
|
|
memcpy(&customBannerEnabled, data + DataView::CUSTOM_BANNER_ENABLED[DataView::OFFSET_INDEX], sizeof(customBannerEnabled));
|
|
m_customBannerEnabled = (customBannerEnabled != 0);
|
|
|
|
lastConfigUpdate = currentTimeMs;
|
|
}
|
|
|
|
const bool validKeepAlive = (currentTimeMs - imuDateMs) < 5000;
|
|
const bool validData = validKeepAlive && m_diagonalFOV != 0.0f;
|
|
const uint8_t expectedVersion = 4;
|
|
bool enabledFlagSet = (enabledFlag != 0);
|
|
bool validVersion = (version == expectedVersion);
|
|
const bool wasEnabled = m_enabled;
|
|
const bool enabled = enabledFlagSet && validVersion && validData;
|
|
if (!enabled) {
|
|
// give a grace period after enabling the effect
|
|
if (wasEnabled && (currentTimeMs - activatedAt > 1000)) {
|
|
qCCritical(KWIN_XR) << "\t\t\tBreezy - disabling effect; currentTimeMs:" << currentTimeMs
|
|
<< "imuDateMs:" << imuDateMs
|
|
<< "enabledFlag:" << enabledFlag
|
|
<< "version:" << version
|
|
<< "diagonalFOV:" << m_diagonalFOV;
|
|
deactivate();
|
|
m_enabled = false;
|
|
Q_EMIT enabledStateChanged();
|
|
return;
|
|
}
|
|
} else if (!wasEnabled) {
|
|
qCCritical(KWIN_XR) << "\t\t\tBreezy - enabling effect; currentTimeMs:" << currentTimeMs
|
|
<< "imuDateMs:" << imuDateMs
|
|
<< "enabledFlag:" << enabledFlag
|
|
<< "version:" << version
|
|
<< "diagonalFOV:" << m_diagonalFOV;
|
|
activate();
|
|
m_enabled = true;
|
|
Q_EMIT enabledStateChanged();
|
|
activatedAt = currentTimeMs;
|
|
}
|
|
|
|
if (updateConfig) Q_EMIT devicePropertiesChanged();
|
|
|
|
float imuData[4 * DataView::IMU_QUAT_ENTRIES]; // 4 quaternion-sized rows
|
|
memcpy(imuData, data + DataView::IMU_QUAT_DATA[DataView::OFFSET_INDEX], sizeof(imuData));
|
|
bool wasImuResetState = m_imuResetState;
|
|
m_imuResetState = (imuData[0] == 0.0f && imuData[1] == 0.0f && imuData[2] == 0.0f && imuData[3] == 1.0f);
|
|
if (m_imuResetState != wasImuResetState) {
|
|
if (m_imuResetState) recenter();
|
|
Q_EMIT imuResetStateChanged();
|
|
}
|
|
|
|
// convert NWU to EUS by passing root.rotation values: -y, z, -x
|
|
QQuaternion quatT0(imuData[3], -imuData[1], imuData[2], -imuData[0]);
|
|
|
|
int imuDataOffset = DataView::IMU_QUAT_ENTRIES;
|
|
QQuaternion quatT1(imuData[imuDataOffset + 3], -imuData[imuDataOffset + 1], imuData[imuDataOffset + 2], -imuData[imuDataOffset + 0]);
|
|
|
|
imuDataOffset += DataView::IMU_QUAT_ENTRIES;
|
|
|
|
// skip the 3rd quaternion
|
|
imuDataOffset += DataView::IMU_QUAT_ENTRIES;
|
|
|
|
// set imuRotations to the last two rotations, leave out the elapsed time
|
|
m_imuRotations.clear();
|
|
m_imuRotations.append(quatT0);
|
|
m_imuRotations.append(quatT1);
|
|
|
|
// 4th row isn't actually a quaternion, it contains the timestamps for each of the 3 quaternions
|
|
// elapsed time between T0 and T1 is: imuData[0] - imuData[1]
|
|
m_imuTimeElapsedMs = static_cast<quint32>(imuData[imuDataOffset + 0] - imuData[imuDataOffset + 1]);
|
|
|
|
m_imuTimestamp = imuDateMs;
|
|
|
|
float originData[4 * DataView::IMU_QUAT_ENTRIES]; // 4 quaternion-sized rows
|
|
memcpy(originData, data + DataView::SMOOTH_FOLLOW_ORIGIN_DATA[DataView::OFFSET_INDEX], sizeof(originData));
|
|
|
|
// convert NWU to EUS by passing root.rotation values: -y, z, -x
|
|
QQuaternion sfQuatT0(originData[3], -originData[1], originData[2], -originData[0]);
|
|
|
|
int originDataOffset = DataView::IMU_QUAT_ENTRIES;
|
|
QQuaternion sfQuatT1(originData[originDataOffset + 3], -originData[originDataOffset + 1], originData[originDataOffset + 2], -originData[originDataOffset + 0]);
|
|
|
|
originDataOffset += DataView::IMU_QUAT_ENTRIES;
|
|
|
|
// skip the 3rd quaternion
|
|
originDataOffset += DataView::IMU_QUAT_ENTRIES;
|
|
|
|
// set smoothFollowOrigin to the last two rotations, leave out the elapsed time
|
|
m_smoothFollowOrigin.clear();
|
|
m_smoothFollowOrigin.append(sfQuatT0);
|
|
m_smoothFollowOrigin.append(sfQuatT1);
|
|
|
|
uint8_t smoothFollowEnabled = false;
|
|
memcpy(&smoothFollowEnabled, data + DataView::SMOOTH_FOLLOW_ENABLED[DataView::OFFSET_INDEX], sizeof(smoothFollowEnabled));
|
|
bool nextSmoothFollowEnabled = (smoothFollowEnabled != 0);
|
|
bool focusedSmoothFollowEnabled = nextSmoothFollowEnabled && !m_allDisplaysFollowMode;
|
|
if (m_smoothFollowEnabled != nextSmoothFollowEnabled || m_focusedSmoothFollowEnabled != focusedSmoothFollowEnabled) {
|
|
m_smoothFollowEnabled = nextSmoothFollowEnabled;
|
|
|
|
if (m_focusedSmoothFollowEnabled != focusedSmoothFollowEnabled) {
|
|
m_focusedSmoothFollowEnabled = focusedSmoothFollowEnabled;
|
|
|
|
// only emit the signal if it affects the effect
|
|
Q_EMIT smoothFollowEnabledChanged();
|
|
}
|
|
|
|
if (m_smoothFollowEnabled) updateDriverSmoothFollowSettings();
|
|
} else if (enabled && !wasEnabled) {
|
|
Q_EMIT smoothFollowEnabledChanged();
|
|
if (m_smoothFollowEnabled) updateDriverSmoothFollowSettings();
|
|
}
|
|
}
|
|
|
|
void BreezyDesktopEffect::setSmoothFollowThreshold(float threshold) {
|
|
if (m_smoothFollowThreshold != threshold) {
|
|
m_smoothFollowThreshold = threshold;
|
|
if (m_smoothFollowEnabled) updateDriverSmoothFollowSettings();
|
|
}
|
|
}
|
|
|
|
void BreezyDesktopEffect::updateDriverSmoothFollowSettings() {
|
|
qreal adjustedDistance = m_focusedDisplayDistance;
|
|
|
|
if (m_lookingAtScreenIndex != -1 && !m_displayResolution.isEmpty()) {
|
|
// Adjust display distance by relative monitor size compared to the FOV monitor
|
|
const Output *focusedOutput = effects->screens().at(m_lookingAtScreenIndex);
|
|
const QSize focusedSize = focusedOutput ? focusedOutput->geometry().size() : QSize();
|
|
|
|
if (focusedSize.isValid()) {
|
|
const qreal fovW = static_cast<qreal>(m_displayResolution.at(0));
|
|
const qreal fovH = static_cast<qreal>(m_displayResolution.at(1));
|
|
|
|
const qreal ratioW = static_cast<qreal>(focusedSize.width()) / fovW;
|
|
const qreal ratioH = static_cast<qreal>(focusedSize.height()) / fovH;
|
|
const qreal focusedMonitorSizeAdjustment = std::max(ratioW, ratioH);
|
|
|
|
adjustedDistance = m_focusedDisplayDistance / focusedMonitorSizeAdjustment;
|
|
}
|
|
}
|
|
|
|
QJsonObject flags;
|
|
flags.insert(QStringLiteral("breezy_desktop_display_distance"), adjustedDistance);
|
|
flags.insert(QStringLiteral("breezy_desktop_follow_threshold"), m_smoothFollowThreshold);
|
|
XRDriverIPC::instance().writeControlFlags(flags);
|
|
}
|
|
|
|
QString BreezyDesktopEffect::cursorImageSource() const
|
|
{
|
|
return m_cursorImageSource;
|
|
}
|
|
|
|
QSize BreezyDesktopEffect::cursorImageSize() const
|
|
{
|
|
return m_cursorImageSize;
|
|
}
|
|
|
|
QPointF BreezyDesktopEffect::cursorPos() const
|
|
{
|
|
return m_cursorPos;
|
|
}
|
|
|
|
void BreezyDesktopEffect::showCursor()
|
|
{
|
|
if (!m_cursorHidden) return;
|
|
|
|
effects->showCursor();
|
|
m_cursorHidden = false;
|
|
}
|
|
|
|
void BreezyDesktopEffect::hideCursor()
|
|
{
|
|
if (m_cursorHidden) return;
|
|
|
|
updateCursorImage();
|
|
effects->hideCursor();
|
|
m_cursorHidden = true;
|
|
}
|
|
|
|
void BreezyDesktopEffect::updateCursorImage()
|
|
{
|
|
const auto cursor = effects->cursorImage();
|
|
if (!cursor.image().isNull()) {
|
|
QByteArray data;
|
|
QBuffer buffer(&data);
|
|
buffer.open(QIODevice::WriteOnly);
|
|
cursor.image().save(&buffer, "PNG");
|
|
|
|
m_cursorImageSource = QStringLiteral("data:image/png;base64,%1").arg(QString::fromLatin1(data.toBase64()));
|
|
m_cursorImageSize = cursor.image().size();
|
|
} else {
|
|
m_cursorImageSource = QString();
|
|
m_cursorImageSize = QSize();
|
|
}
|
|
// Cursor size affects the expanded geometry margin; invalidate cache.
|
|
invalidateEffectOnScreenGeometryCache();
|
|
Q_EMIT cursorImageSourceChanged();
|
|
}
|
|
|
|
void BreezyDesktopEffect::updateCursorPos()
|
|
{
|
|
// Update cursor position from effects
|
|
const auto cursor = effects->cursorImage();
|
|
QPointF newPos = effects->cursorPos() - cursor.hotSpot();
|
|
if (m_cursorPos != newPos) {
|
|
const QPointF prevPos = m_cursorPos;
|
|
m_cursorPos = newPos;
|
|
Q_EMIT cursorPosChanged();
|
|
|
|
evaluateCursorOnScreenState(prevPos, m_cursorPos);
|
|
}
|
|
}
|
|
|
|
void BreezyDesktopEffect::evaluateCursorOnScreenState(const QPointF &prevPos, const QPointF &newPos)
|
|
{
|
|
if (!updateEffectOnScreenGeometryCache()) return;
|
|
|
|
const QPointF velocity = newPos - prevPos;
|
|
const QPointF predicted = newPos + velocity;
|
|
|
|
const bool onScreen =
|
|
m_effectOnScreenExpandedGeometry.contains(newPos.toPoint()) ||
|
|
m_effectOnScreenExpandedGeometry.contains(predicted.toPoint());
|
|
if (m_enabled && !m_imuResetState && !m_cursorHidden && onScreen) {
|
|
hideCursor();
|
|
} else if (m_cursorHidden && (!m_enabled || m_imuResetState || !onScreen)) {
|
|
showCursor();
|
|
}
|
|
}
|
|
|
|
void BreezyDesktopEffect::invalidateEffectOnScreenGeometryCache()
|
|
{
|
|
m_effectOnScreenGeometryValid = false;
|
|
}
|
|
|
|
bool BreezyDesktopEffect::updateEffectOnScreenGeometryCache()
|
|
{
|
|
if (m_effectOnScreenGeometryValid)
|
|
return true;
|
|
|
|
if (m_effectTargetScreenIndex == -1)
|
|
return false;
|
|
|
|
Output *effectOnScreen = effects->screens().at(m_effectTargetScreenIndex);
|
|
if (!effectOnScreen)
|
|
return false;
|
|
|
|
const QRect geometry = effectOnScreen->geometry();
|
|
const int marginX = (m_cursorImageSize.width() > 0) ? m_cursorImageSize.width() : 10;
|
|
const int marginY = (m_cursorImageSize.height() > 0) ? m_cursorImageSize.height() : 10;
|
|
m_effectOnScreenExpandedGeometry = geometry.adjusted(-marginX, -marginY, marginX, marginY);
|
|
m_effectOnScreenGeometryValid = true;
|
|
return true;
|
|
}
|
|
|
|
void BreezyDesktopEffect::warpPointerToOutputCenter(Output *output)
|
|
{
|
|
if (!output) {
|
|
return;
|
|
}
|
|
const QRect geometry = output->geometry();
|
|
const QPointF center = geometry.center();
|
|
Cursors::self()->mouse()->setPos(center);
|
|
|
|
// When warping, we don't have a meaningful previous position; use center for both.
|
|
evaluateCursorOnScreenState(center, center);
|
|
}
|
|
|
|
void BreezyDesktopEffect::moveCursorToFocusedDisplay()
|
|
{
|
|
if (m_lookingAtScreenIndex == -1) return;
|
|
|
|
warpPointerToOutputCenter(effects->screens().at(m_lookingAtScreenIndex));
|
|
}
|
|
}
|
|
|
|
#include "breezydesktopeffect.moc" |