From ea01dd79bd4c3731da8b9267881a4d82cf53a5cb Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:46:04 -0700 Subject: [PATCH] Add support for smooth follow to the effect (no UI updates) --- kwin/bin/setup | 2 - kwin/src/breezydesktopeffect.cpp | 35 +++++ kwin/src/breezydesktopeffect.h | 14 +- kwin/src/qml/BreezyDesktop.qml | 221 +++++++++++++++++++++++------- kwin/src/qml/CameraController.qml | 33 ++++- kwin/src/qml/Displays.qml | 16 ++- 6 files changed, 259 insertions(+), 62 deletions(-) diff --git a/kwin/bin/setup b/kwin/bin/setup index 28a63f1..e7fd3b1 100755 --- a/kwin/bin/setup +++ b/kwin/bin/setup @@ -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 diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 31105ae..5520e32 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -487,6 +487,14 @@ bool BreezyDesktopEffect::mirrorPhysicalDisplays() const { return m_mirrorPhysicalDisplays; } +QList 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(data[DataView::IMU_PARITY_BYTE[DataView::OFFSET_INDEX]]); uint8_t parity = 0; @@ -627,6 +635,33 @@ void BreezyDesktopEffect::updateImuRotation() { m_imuTimeElapsedMs = static_cast(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 diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index 3639c2c..f7f1c2a 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -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 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 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 m_smoothFollowOrigin; bool m_customBannerEnabled; QFileSystemWatcher *m_shmFileWatcher = nullptr; QFileSystemWatcher *m_shmDirectoryWatcher = nullptr; diff --git a/kwin/src/qml/BreezyDesktop.qml b/kwin/src/qml/BreezyDesktop.qml index 7409a87..ac017aa 100644 --- a/kwin/src/qml/BreezyDesktop.qml +++ b/kwin/src/qml/BreezyDesktop.qml @@ -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 { diff --git a/kwin/src/qml/CameraController.qml b/kwin/src/qml/CameraController.qml index f4ee58c..2d69ebc 100644 --- a/kwin/src/qml/CameraController.qml +++ b/kwin/src/qml/CameraController.qml @@ -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(); + } } diff --git a/kwin/src/qml/Displays.qml b/kwin/src/qml/Displays.qml index 0e7f069..e82b98d 100644 --- a/kwin/src/qml/Displays.qml +++ b/kwin/src/qml/Displays.qml @@ -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; + } } \ No newline at end of file