Add support for smooth follow to the effect (no UI updates)

This commit is contained in:
wheaney 2025-09-17 14:46:04 -07:00
parent 5623fe1126
commit ea01dd79bd
6 changed files with 259 additions and 62 deletions

View File

@ -73,7 +73,6 @@ if [[ ! -f "$BASH_PROFILE" ]] || ! grep -Fq "$QT_PLUGIN_EXPORT" "$BASH_PROFILE"
# Added by Breezy Desktop installer: QT plugin path setup
$QT_PLUGIN_EXPORT
export QT_DEBUG_PLUGINS=1
EOF
fi
@ -85,7 +84,6 @@ if [[ ! -f "$PLASMA_ENV_SCRIPT" ]]; then
# Added by Breezy Desktop installer: QT plugin path setup
$QT_PLUGIN_EXPORT
export QT_DEBUG_PLUGINS=1
EOF
fi

View File

@ -487,6 +487,14 @@ bool BreezyDesktopEffect::mirrorPhysicalDisplays() const {
return m_mirrorPhysicalDisplays;
}
QList<QQuaternion> BreezyDesktopEffect::smoothFollowOrigin() const {
return m_smoothFollowOrigin;
}
bool BreezyDesktopEffect::smoothFollowEnabled() const {
return m_smoothFollowEnabled;
}
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;
@ -627,6 +635,33 @@ void BreezyDesktopEffect::updateImuRotation() {
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);
if (m_smoothFollowEnabled != nextSmoothFollowEnabled) {
m_smoothFollowEnabled = nextSmoothFollowEnabled;
Q_EMIT smoothFollowEnabledChanged();
}
}
QString BreezyDesktopEffect::cursorImageSource() const

View File

@ -37,7 +37,9 @@ namespace KWin
Q_PROPERTY(int displayWrappingScheme READ displayWrappingScheme NOTIFY displayWrappingSchemeChanged)
Q_PROPERTY(qreal diagonalFOV READ diagonalFOV NOTIFY devicePropertiesChanged)
Q_PROPERTY(qreal lensDistanceRatio READ lensDistanceRatio NOTIFY devicePropertiesChanged)
Q_PROPERTY(bool sbsEnabled READ sbsEnabled NOTIFY devicePropertiesChanged)
Q_PROPERTY(bool sbsEnabled READ sbsEnabled NOTIFY sbsEnabledChanged)
Q_PROPERTY(bool smoothFollowEnabled READ smoothFollowEnabled NOTIFY smoothFollowEnabledChanged)
Q_PROPERTY(QList<QQuaternion> smoothFollowOrigin READ smoothFollowOrigin)
Q_PROPERTY(bool customBannerEnabled READ customBannerEnabled NOTIFY devicePropertiesChanged)
Q_PROPERTY(int antialiasingQuality READ antialiasingQuality NOTIFY antialiasingQualityChanged)
Q_PROPERTY(bool removeVirtualDisplaysOnDisable READ removeVirtualDisplaysOnDisable NOTIFY removeVirtualDisplaysOnDisableChanged)
@ -79,6 +81,8 @@ namespace KWin
qreal diagonalFOV() const;
qreal lensDistanceRatio() const;
bool sbsEnabled() const;
bool smoothFollowEnabled() const;
QList<QQuaternion> smoothFollowOrigin() const;
bool customBannerEnabled() const;
int antialiasingQuality() const;
bool removeVirtualDisplaysOnDisable() const;
@ -110,12 +114,14 @@ namespace KWin
void enabledStateChanged();
void zoomOnFocusChanged();
void imuResetStateChanged();
void cursorImageSourceChanged();
void cursorPosChanged();
void sbsEnabledChanged();
void smoothFollowEnabledChanged();
void devicePropertiesChanged();
void antialiasingQualityChanged();
void removeVirtualDisplaysOnDisableChanged();
void mirrorPhysicalDisplaysChanged();
void cursorImageSourceChanged();
void cursorPosChanged();
protected:
QVariantMap initialProperties(Output *screen) override;
@ -142,6 +148,8 @@ namespace KWin
qreal m_diagonalFOV;
qreal m_lensDistanceRatio;
bool m_sbsEnabled;
bool m_smoothFollowEnabled;
QList<QQuaternion> m_smoothFollowOrigin;
bool m_customBannerEnabled;
QFileSystemWatcher *m_shmFileWatcher = nullptr;
QFileSystemWatcher *m_shmDirectoryWatcher = nullptr;

View File

@ -6,10 +6,12 @@ Node {
id: breezyDesktop
property var viewportResolution: effect.displayResolution
property bool smoothFollowEnabled: effect.smoothFollowEnabled
required property var screens
required property var fovDetails
required property var monitorPlacements
property int focusedMonitorIndex: -1
property var smoothFollowFocusedDisplay
Displays {
id: displays
@ -22,6 +24,124 @@ Node {
return breezyDesktopDisplays.objectAt(index);
}
function updateFocus(smoothFollowEnabledChanged = false) {
const rotations = smoothFollowEnabled ? effect.smoothFollowOrigin : effect.imuRotations;
if (rotations && rotations.length > 0) {
let focusedIndex = -1;
if (effect.zoomOnFocusEnabled || smoothFollowEnabled) {
focusedIndex = displays.findFocusedMonitor(
displays.eusToNwuQuat(rotations[0]),
breezyDesktop.monitorPlacements.map(monitorVectors => monitorVectors.centerLook),
breezyDesktop.focusedMonitorIndex,
smoothFollowEnabled,
breezyDesktop.fovDetails,
breezyDesktop.screens.map(screen => screen.geometry)
);
}
let focusedDisplay;
let unfocusedDisplay;
let startSmoothFollowFocusAnimation = false;
if (smoothFollowEnabledChanged) {
let targetDisplay;
let targetProgress;
if (focusedIndex !== -1) {
focusedDisplay = breezyDesktop.displayAtIndex(focusedIndex);
targetDisplay = focusedDisplay;
targetProgress = 1.0;
startSmoothFollowFocusAnimation = true;
} else if (breezyDesktop.focusedMonitorIndex !== -1) {
unfocusedDisplay = breezyDesktop.displayAtIndex(breezyDesktop.focusedMonitorIndex);
targetDisplay = unfocusedDisplay;
targetProgress = 0.0;
}
smoothFollowTransitionAnimation.stop();
smoothFollowTransitionAnimation.target = targetDisplay;
smoothFollowTransitionAnimation.from = targetDisplay.smoothFollowTransitionProgress;
smoothFollowTransitionAnimation.to = targetProgress;
smoothFollowTransitionAnimation.start();
}
if (focusedIndex !== breezyDesktop.focusedMonitorIndex) {
const unfocusedIndex = breezyDesktop.focusedMonitorIndex;
if (!focusedDisplay) focusedDisplay = focusedIndex !== -1 ? breezyDesktop.displayAtIndex(focusedIndex) : null;
const allDisplaysDistanceBinding = Qt.binding(function() { return effect.allDisplaysDistance; });
const focusedDisplayDistanceBinding = Qt.binding(function() { return effect.focusedDisplayDistance; });
if (focusedDisplay === null) {
if (!unfocusedDisplay) unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex);
zoomOutAnimation.target = unfocusedDisplay;
zoomOutAnimation.target.targetDistance = effect.allDisplaysDistance;
zoomOutAnimation.start();
} else {
if (unfocusedIndex === -1) {
zoomInAnimation.target = focusedDisplay;
focusedDisplay.targetDistance = effect.focusedDisplayDistance;
zoomInAnimation.start();
} else {
zoomInSeqAnimation.target = focusedDisplay;
focusedDisplay.targetDistance = effect.focusedDisplayDistance;
if (!unfocusedDisplay) unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex);
zoomOutSeqAnimation.target = unfocusedDisplay;
zoomOutSeqAnimation.target.targetDistance = effect.allDisplaysDistance;
zoomOnFocusSequence.start();
}
}
breezyDesktop.focusedMonitorIndex = focusedIndex;
}
if (startSmoothFollowFocusAnimation) smoothFollowFocusedAnimation.restart();
}
}
// monitorPlacement assumed to be present
function displayEusVector(display) {
const displayNwu =
display.monitorPlacement.centerNoRotate
.times(display.monitorDistance / effect.allDisplaysDistance);
return displays.nwuToEusVector(displayNwu);
}
function displayRotationVector(display, eusVector) {
return display.rotationMatrix.times(eusVector);
}
// smoothFollowOrigin is the rotation away from the original placement of the displays
// imuRotations is the smooth follow rotation relative to the camera (very near an identity quat)
// subtract the latter from the former to get the complete rotation
function smoothFollowQuat() {
return effect.smoothFollowOrigin[0].times(effect.imuRotations[0].conjugated());
}
function displaySmoothFollowVector(display, eusVector) {
return smoothFollowQuat().times(eusVector);
}
// don't call this from the delegate to avoid binding the position property to the effect properties
// used for smooth follow
function displayPosition(display, smoothFollowRotation) {
const displayEus = displayEusVector(display);
// short circuit to avoid slerping if not needed
if (display.smoothFollowTransitionProgress === 0.0) {
return displayRotationVector(display, displayEus);
}
if (display.smoothFollowTransitionProgress === 1.0) {
return displaySmoothFollowVector(display, displayEus, smoothFollowRotation);
}
const finalPosition = displays.slerpVector(
displayRotationVector(display, displayEus),
displaySmoothFollowVector(display, displayEus, smoothFollowRotation),
display.smoothFollowTransitionProgress
);
return finalPosition
}
Repeater3D {
id: breezyDesktopDisplays
model: breezyDesktop.screens.length
@ -29,6 +149,7 @@ Node {
screen: breezyDesktop.screens[index]
monitorPlacement: breezyDesktop.monitorPlacements[index]
property real smoothFollowTransitionProgress: 0.0
property real monitorDistance: effect.allDisplaysDistance
property real targetDistance: effect.allDisplaysDistance
property real screenRotationY: displays.radianToDegree(monitorPlacement?.rotationAngleRadians.y ?? 0)
@ -53,64 +174,63 @@ Node {
position: {
if (!monitorPlacement) return Qt.vector3d(0, 0, 0);
const displayNwu =
monitorPlacement.centerNoRotate
.times(monitorDistance / effect.allDisplaysDistance);
return rotationMatrix.times(displays.nwuToEusVector(displayNwu));
return displayRotationVector(this, displayEusVector(this));
}
}
}
// smoothFollowEnabled gets cleared before the IMU begins slerping back to the origin so we can't just
// switch off smooth follow logic based on this flag. Instead, we have to rely on
// smoothFollowTransitionProgress to determine how much of the IMU positions to apply.
onSmoothFollowEnabledChanged: {
updateFocus(true);
}
FrameAnimation {
id: smoothFollowFocusedAnimation
running: false
onTriggered: {
if (!breezyDesktop.smoothFollowFocusedDisplay && breezyDesktop.focusedMonitorIndex !== -1) {
breezyDesktop.smoothFollowFocusedDisplay = breezyDesktopDisplays.objectAt(breezyDesktop.focusedMonitorIndex)
}
let continueRunning = false;
const focusedDisplay = breezyDesktop.smoothFollowFocusedDisplay;
if (focusedDisplay) {
const smoothFollowRotation = smoothFollowQuat();
focusedDisplay.position = displayPosition(focusedDisplay, smoothFollowRotation);
continueRunning = focusedDisplay.smoothFollowTransitionProgress > 0.0;
if (continueRunning) {
focusedDisplay.eulerRotation = Qt.vector3d(0, 0, 0);
focusedDisplay.rotation = smoothFollowRotation;
} else {
focusedDisplay.eulerRotation.x = focusedDisplay.screenRotationX;
focusedDisplay.eulerRotation.y = focusedDisplay.screenRotationY;
focusedDisplay.eulerRotation.z = 0.0;
}
}
if (!continueRunning) {
smoothFollowFocusedAnimation.stop();
breezyDesktop.smoothFollowFocusedDisplay = null;
}
}
}
NumberAnimation {
id: smoothFollowTransitionAnimation
duration: 150
property: "smoothFollowTransitionProgress"
running: false
}
Timer {
interval: 500 // 500ms - 2x per second to avoid running this check too frequently
repeat: true
running: true
onTriggered: {
if (effect.imuRotations && effect.imuRotations.length > 0) {
let focusedIndex = -1;
if (effect.zoomOnFocusEnabled) {
focusedIndex = displays.findFocusedMonitor(
displays.eusToNwuQuat(effect.imuRotations[0]),
breezyDesktop.monitorPlacements.map(monitorVectors => monitorVectors.centerLook),
breezyDesktop.focusedMonitorIndex,
false, // TODO smooth follow
breezyDesktop.fovDetails,
breezyDesktop.screens.map(screen => screen.geometry)
);
}
if (focusedIndex !== breezyDesktop.focusedMonitorIndex) {
const unfocusedIndex = breezyDesktop.focusedMonitorIndex;
const focusedDisplay = focusedIndex !== -1 ? breezyDesktop.displayAtIndex(focusedIndex) : null;
const allDisplaysDistanceBinding = Qt.binding(function() { return effect.allDisplaysDistance; });
const focusedDisplayDistanceBinding = Qt.binding(function() { return effect.focusedDisplayDistance; });
if (focusedDisplay === null) {
const unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex);
zoomOutAnimation.target = unfocusedDisplay;
zoomOutAnimation.target.targetDistance = effect.allDisplaysDistance;
zoomOutAnimation.start();
} else {
if (unfocusedIndex === -1) {
zoomInAnimation.target = focusedDisplay;
focusedDisplay.targetDistance = effect.focusedDisplayDistance;
zoomInAnimation.start();
} else {
zoomInSeqAnimation.target = focusedDisplay;
focusedDisplay.targetDistance = effect.focusedDisplayDistance;
const unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex);
zoomOutSeqAnimation.target = unfocusedDisplay;
zoomOutSeqAnimation.target.targetDistance = effect.allDisplaysDistance;
zoomOnFocusSequence.start();
}
}
breezyDesktop.focusedMonitorIndex = focusedIndex;
}
}
updateFocus();
}
}
@ -120,11 +240,14 @@ Node {
zoomOutAnimation.stop();
zoomInAnimation.stop();
zoomOnFocusSequence.stop();
smoothFollowTransitionAnimation.stop();
smoothFollowFocusedAnimation.stop();
zoomOutAnimation.target = null;
zoomInAnimation.target = null;
zoomOutSeqAnimation.target = null;
zoomInSeqAnimation.target = null;
smoothFollowTransitionAnimation.target = null;
}
NumberAnimation {

View File

@ -12,19 +12,24 @@ Item {
property real lensDistanceRatio: effect.lensDistanceRatio
property bool sbsEnabled: effect.sbsEnabled
property bool customBannerEnabled: effect.customBannerEnabled
property bool smoothFollowEnabled: effect.smoothFollowEnabled
// if true, then smoothFollowEnabled just cleared and the IMU data is slerping back,
// continue to use the origin data for the duration of the Timer
property bool smoothFollowDisabling: false
Displays {
id: displays
}
function updateCamera() {
function updateCamera(rotations) {
camera.eulerRotation = applyLookAhead(
effect.imuRotations[0],
effect.imuRotations[1],
rotations[0],
rotations[1],
effect.imuTimeElapsedMs,
lookAheadMS(effect.imuTimestamp, effect.lookAheadConfig, effect.lookAheadOverride)
);
camera.position = effect.imuRotations[0].times(Qt.vector3d(0, 0, -fovDetails.lensDistancePixels));
camera.position = rotations[0].times(Qt.vector3d(0, 0, -fovDetails.lensDistancePixels));
}
// how far to look ahead is how old the IMU data is plus a constant that is either the default for this device or an override
@ -71,9 +76,25 @@ Item {
FrameAnimation {
running: true
onTriggered: {
if (effect.imuRotations && effect.imuRotations.length > 0) {
updateCamera();
const rotations = (effect.smoothFollowEnabled || smoothFollowDisabling) ? effect.smoothFollowOrigin : effect.imuRotations;
if (rotations && rotations.length > 0) {
updateCamera(rotations);
}
}
}
Timer {
id: smoothFollowDisablingTimer
interval: 750
repeat: false
onTriggered: {
cameraController.smoothFollowDisabling = false;
}
}
onSmoothFollowEnabledChanged: {
smoothFollowDisablingTimer.stop();
smoothFollowDisabling = !smoothFollowEnabled;
if (smoothFollowDisabling) smoothFollowDisablingTimer.start();
}
}

View File

@ -336,6 +336,8 @@ QtObject {
}
function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, smoothFollowEnabled, fovDetails, monitorsDetails) {
if (currentFocusedIndex !== -1 && smoothFollowEnabled) return currentFocusedIndex;
var lookVector = Qt.vector3d(1.0, 0.0, 0.0); // NWU vector pointing to the center of the screen
var rotatedLookVector = quaternion.times(lookVector);
@ -369,8 +371,7 @@ QtObject {
westConversionFns.angleToLength
) * effect.focusedDisplayDistance / effect.allDisplaysDistance;
if (smoothFollowEnabled || focusedDistance < unfocusThreshold)
return currentFocusedIndex;
if (focusedDistance < unfocusThreshold) return currentFocusedIndex;
}
var closestIndex = -1;
@ -402,4 +403,15 @@ QtObject {
// Unfocus all displays
return -1;
}
function slerpVector(from, to, progress) {
const inverseProgress = 1.0 - progress;
const finalVector = Qt.vector3d(
from.x * inverseProgress + to.x * progress,
from.y * inverseProgress + to.y * progress,
from.z * inverseProgress + to.z * progress
);
return finalVector;
}
}