diff --git a/gnome/src/devicedatastream.js b/gnome/src/devicedatastream.js index 2e4169e..aae3706 100644 --- a/gnome/src/devicedatastream.js +++ b/gnome/src/devicedatastream.js @@ -21,7 +21,7 @@ const IPC_FILE_PATH = "/dev/shm/breezy_desktop_imu"; const KEEPALIVE_REFRESH_INTERVAL_SEC = 1; // the driver should be using the same data layout version -const DATA_LAYOUT_VERSION = 3; +const DATA_LAYOUT_VERSION = 4; // DataView info: [offset, size, count] const VERSION = [0, UINT8_SIZE, 1]; @@ -32,7 +32,9 @@ const DISPLAY_FOV = [dataViewEnd(DISPLAY_RES), FLOAT_SIZE, 1]; const LENS_DISTANCE_RATIO = [dataViewEnd(DISPLAY_FOV), FLOAT_SIZE, 1]; const SBS_ENABLED = [dataViewEnd(LENS_DISTANCE_RATIO), BOOL_SIZE, 1]; const CUSTOM_BANNER_ENABLED = [dataViewEnd(SBS_ENABLED), BOOL_SIZE, 1]; -const EPOCH_MS = [dataViewEnd(CUSTOM_BANNER_ENABLED), UINT_SIZE, 2]; +const SMOOTH_FOLLOW_ENABLED = [dataViewEnd(CUSTOM_BANNER_ENABLED), BOOL_SIZE, 1]; +const SMOOTH_FOLLOW_ORIGIN_DATA = [dataViewEnd(SMOOTH_FOLLOW_ENABLED), FLOAT_SIZE, 16]; +const EPOCH_MS = [dataViewEnd(SMOOTH_FOLLOW_ORIGIN_DATA), UINT_SIZE, 2]; const IMU_QUAT_DATA = [dataViewEnd(EPOCH_MS), FLOAT_SIZE, 16]; const IMU_PARITY_BYTE = [dataViewEnd(IMU_QUAT_DATA), UINT8_SIZE, 1]; const DATA_VIEW_LENGTH = dataViewEnd(IMU_PARITY_BYTE); @@ -95,6 +97,13 @@ export const DeviceDataStream = GObject.registerClass({ 'Latest IMU quaternion snapshots and epoch timestamp for when it was collected', GObject.ParamFlags.READWRITE ), + 'smooth-follow-enabled': GObject.ParamSpec.boolean( + 'smooth-follow-enabled', + 'Smooth follow enabled', + 'Whether smooth follow is enabled', + GObject.ParamFlags.READWRITE, + false + ), 'show-banner': GObject.ParamSpec.boolean( 'show-banner', 'Show banner', @@ -182,6 +191,8 @@ export const DeviceDataStream = GObject.registerClass({ const version = dataViewUint8(dataView, VERSION); const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validData; let imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA); + let smoothFollowEnabled = dataViewUint8(dataView, SMOOTH_FOLLOW_ENABLED) !== 0; + let smoothFollowOrigin = dataViewFloatArray(dataView, SMOOTH_FOLLOW_ORIGIN_DATA); const imuResetState = enabled && validData && imuData[0] === 0.0 && imuData[1] === 0.0 && imuData[2] === 0.0 && imuData[3] === 1.0; const customBannerEnabled = dataViewUint8(dataView, CUSTOM_BANNER_ENABLED) !== 0; const sbsEnabled = dataViewUint8(dataView, SBS_ENABLED) !== 0; @@ -215,15 +226,24 @@ export const DeviceDataStream = GObject.registerClass({ } } + if (smoothFollowEnabled !== this.smooth_follow_enabled) { + Globals.logger.log_debug(`Smooth follow enabled: ${smoothFollowEnabled}`); + this.smooth_follow_enabled = smoothFollowEnabled; + } + this.imu_snapshots = { + ...(this.imu_snapshots ?? {}), + smooth_follow_origin: smoothFollowOrigin + } + let attempts = 0; - while (!success && attempts < 3) { + while (!success && attempts < 2) { if (dataView.byteLength === DATA_VIEW_LENGTH) { if (checkParityByte(dataView)) { - this.device_data.imuData = imuData; - this.device_data.imuDateMs = imuDateMs; + this.imu_snapshots = { imu_data: imuData, - timestamp_ms: imuDateMs + timestamp_ms: imuDateMs, + smooth_follow_origin: smoothFollowOrigin }; success = true; } @@ -231,7 +251,7 @@ export const DeviceDataStream = GObject.registerClass({ Globals.logger.log(`[ERROR] Invalid dataView.byteLength: ${dataView.byteLength} !== ${DATA_VIEW_LENGTH}`) } - if (!success && ++attempts < 3) { + if (!success && ++attempts < 2) { data = this._ipc_file.load_contents(null); if (data[0]) { buffer = new Uint8Array(data[1]).buffer; @@ -277,19 +297,17 @@ export const DeviceDataStream = GObject.registerClass({ ...imuDataFirst, 2.0, 1.0, 0.0, 0.0 ] - const imuDateMs = Date.now(); - this.device_data.imuData = imuData; - this.device_data.imuDateMs = imuDateMs; this.imu_snapshots = { imu_data: imuData, - timestamp_ms: imuDateMs + timestamp_ms: Date.now(), + smooth_follow_origin: [0.0, 0.0, 0.0, 1.0] }; } this.breezy_desktop_running = true; } else if (this.breezy_desktop_running !== this.breezy_desktop_actually_running) { // update the breezy_desktop_running property if the state changes to trigger "notify::" events this.breezy_desktop_running = this.breezy_desktop_actually_running; - if (!this.breezy_desktop_running) { + if (!this.breezy_desktop_running && keepalive_only) { this.device_data = null; this.imu_snapshots = null; } diff --git a/gnome/src/extension.js b/gnome/src/extension.js index 17bff30..5665bbe 100644 --- a/gnome/src/extension.js +++ b/gnome/src/extension.js @@ -260,7 +260,8 @@ export default class BreezyDesktopExtension extends Extension { this._data_stream_bindings = [ 'show-banner', - 'custom-banner-enabled' + 'custom-banner-enabled', + 'smooth-follow-enabled' ].map(data_stream_key => Globals.data_stream.bind_property(data_stream_key, this._virtual_displays_actor, data_stream_key, Gio.SettingsBindFlags.DEFAULT) ); @@ -508,6 +509,7 @@ export default class BreezyDesktopExtension extends Extension { _toggle_follow_mode() { Globals.logger.log_debug('BreezyDesktopExtension _toggle_follow_mode'); + this._virtual_displays_actor.set_property('smooth-follow-toggle-epoch-ms', Date.now()); this._write_control('toggle_breezy_desktop_smooth_follow', 'true'); } diff --git a/gnome/src/virtualdisplayeffect.js b/gnome/src/virtualdisplayeffect.js index d565a9d..a1c956d 100644 --- a/gnome/src/virtualdisplayeffect.js +++ b/gnome/src/virtualdisplayeffect.js @@ -1,11 +1,23 @@ import Clutter from 'gi://Clutter' import Cogl from 'gi://Cogl'; +import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Shell from 'gi://Shell'; import Globals from './globals.js'; import { degreeToRadian, diagonalToCrossFOVs } from './math.js'; + +// these need to mirror the values in XRLinuxDriver +// https://github.com/wheaney/XRLinuxDriver/blob/main/src/plugins/smooth_follow.c#L31 +export const SMOOTH_FOLLOW_SLERP_TIMELINE_MS = 1000; +const SMOOTH_FOLLOW_SLERP_FACTOR = Math.pow(1-0.99, 1/SMOOTH_FOLLOW_SLERP_TIMELINE_MS); + +// this mirror's how the driver's slerp function progresses so our effect will match it +function smoothFollowSlerpProgress(elapsedMs) { + return 1 - Math.pow(SMOOTH_FOLLOW_SLERP_FACTOR, elapsedMs); +} + // 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 function lookAheadMS(imuDateMs, lookAheadCfg, override) { // how stale the imu data is @@ -41,6 +53,20 @@ export const VirtualDisplayEffect = GObject.registerClass({ 'Latest IMU quaternion snapshots and epoch timestamp for when it was collected', GObject.ParamFlags.READWRITE ), + 'smooth-follow-enabled': GObject.ParamSpec.boolean( + 'smooth-follow-enabled', + 'Smooth follow enabled', + 'Whether smooth follow is enabled', + GObject.ParamFlags.READWRITE, + false + ), + 'smooth-follow-toggle-epoch-ms': GObject.ParamSpec.uint64( + 'smooth-follow-toggle-epoch-ms', + 'Smooth follow toggle epoch time', + 'ms since epoch when smooth follow was toggled', + GObject.ParamFlags.READWRITE, + 0, Number.MAX_SAFE_INTEGER, 0 + ), 'width': GObject.ParamSpec.int( 'width', 'Width', @@ -78,12 +104,6 @@ export const VirtualDisplayEffect = GObject.registerClass({ 2.5, 1.0 ), - 'display-position': GObject.ParamSpec.jsobject( - 'display-position', - 'Display Position', - 'Position of the display in COGL (ESU) coordinates', - GObject.ParamFlags.READWRITE - ), 'display-distance-default': GObject.ParamSpec.double( 'display-distance-default', 'Display distance default', @@ -148,12 +168,15 @@ export const VirtualDisplayEffect = GObject.registerClass({ this._current_display_distance = this._is_focused() ? this.display_distance : this.display_distance_default; this.no_distance_ease = false; + this._current_follow_ease_progress = 0.0; + this._use_smooth_follow_origin = false; this.connect('notify::display-distance', this._update_display_distance.bind(this)); this.connect('notify::focused-monitor-index', this._update_display_distance.bind(this)); this.connect('notify::monitor-placements', this._update_display_position_uniforms.bind(this)); this.connect('notify::monitor-wrapping-scheme', this._update_display_position_uniforms.bind(this)); this.connect('notify::show-banner', this._handle_banner_update.bind(this)); + this.connect('notify::smooth-follow-enabled', this._handle_smooth_follow_enabled_update.bind(this)); } _is_focused() { @@ -212,8 +235,67 @@ export const VirtualDisplayEffect = GObject.registerClass({ }).bind(this)); this._distance_ease_timeline.start(); + + if (this.smooth_follow_enabled) this._handle_smooth_follow_enabled_update(); } + _handle_smooth_follow_enabled_update() { + // we'll re-trigger this once a monitor becomes focused + if (this.focused_monitor_index === -1) return; + + this._use_smooth_follow_origin = false; + + if (this._follow_ease_timeline?.is_playing()) this._follow_ease_timeline.stop(); + + const from = this._current_follow_ease_progress; + const to = this.smooth_follow_enabled && this._is_focused() ? 1.0 : 0.0; + const toggleTime = this.smooth_follow_toggle_epoch_ms === 0 ? Date.now() : this.smooth_follow_toggle_epoch_ms; + + // would have been a slight delay between request and slerp actually starting + const toggleDelayMs = (Date.now() - toggleTime) * 0.75; + const slerpStartTime = toggleTime + toggleDelayMs; + + const dataAge = Date.now() - this.imu_snapshots.timestamp_ms; + if (to !== from) { + this._follow_ease_timeline = Clutter.Timeline.new_for_actor( + this.get_actor(), + SMOOTH_FOLLOW_SLERP_TIMELINE_MS - toggleDelayMs + ); + this._follow_ease_timeline.connect('new-frame', ((timeline, elapsed_ms) => { + const toggleTimeOffsetMs = Date.now() - slerpStartTime; + + // this relies on the slerp function tuned to reach 100% in about 1 second + const progress = smoothFollowSlerpProgress(toggleTimeOffsetMs); + this._current_follow_ease_progress = from + (to - from) * progress; + this._update_display_position_uniforms(); + }).bind(this)); + + this._follow_ease_timeline.connect('completed', (() => { + this._current_follow_ease_progress = to; + this._use_smooth_follow_origin = false; + this.smooth_follow_toggle_epoch_ms = 0; + this._update_display_position_uniforms(); + }).bind(this)); + + this._follow_ease_timeline.start(); + } else if (!this.smooth_follow_enabled) { + // smooth follow has been turned off and this screen wasn't the focus, + // continue to use the smooth_follow_origin data for 1 more second + this._use_smooth_follow_origin = true; + GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + SMOOTH_FOLLOW_SLERP_TIMELINE_MS - toggleDelayMs, + (() => { + this._use_smooth_follow_origin = false; + this.smooth_follow_toggle_epoch_ms = 0; + this._current_follow_ease_progress = to; + return GLib.SOURCE_REMOVE; + }).bind(this) + ); + } + } + + // follow_ease transitions this from a rotated display (0.0) to a centered/focused display (1.0) _update_display_position_uniforms() { // this is in NWU coordinates const monitorPlacement = this.monitor_placements[this.monitor_index]; @@ -225,12 +307,24 @@ export const VirtualDisplayEffect = GObject.registerClass({ const noRotationVector = monitorPlacement.topLeftNoRotate.map((coord, index) => coord - distanceDelta[index]); // convert to CoGL's east-down-south coordinates and apply display distance - this.set_uniform_float(this.get_uniform_location("u_display_position"), 3, - [-noRotationVector[1], -noRotationVector[2], -noRotationVector[0]]); + const inverse_follow_ease = 1.0 - this._current_follow_ease_progress; + if (this._current_follow_ease_progress === 0.0) { + this.set_uniform_float(this.get_uniform_location("u_display_position"), 3, + [-noRotationVector[1], -noRotationVector[2], -noRotationVector[0]]); + } else { + const focusDistanceNorth = monitorPlacement.centerOrigin[0] * inverseAppliedDistance; + const centerOriginVector = {...monitorPlacement.centerOrigin}; + centerOriginVector[0] -= focusDistanceNorth; + + // slerp from the rotated display to the centered display + const followVector = noRotationVector.map((coord, index) => coord * inverse_follow_ease + centerOriginVector[index] * this._current_follow_ease_progress); + this.set_uniform_float(this.get_uniform_location("u_display_position"), 3, + [-followVector[1], -followVector[2], -followVector[0]]); + } const rotation_radians = this.monitor_placements[this.monitor_index].rotationAngleRadians; - this.set_uniform_float(this.get_uniform_location("u_rotation_x_radians"), 1, [rotation_radians.x]); - this.set_uniform_float(this.get_uniform_location("u_rotation_y_radians"), 1, [rotation_radians.y]); + this.set_uniform_float(this.get_uniform_location("u_rotation_x_radians"), 1, [rotation_radians.x * inverse_follow_ease]); + this.set_uniform_float(this.get_uniform_location("u_rotation_y_radians"), 1, [rotation_radians.y * inverse_follow_ease]); } _handle_banner_update() { @@ -350,7 +444,7 @@ export const VirtualDisplayEffect = GObject.registerClass({ vec3 velocity_t0 = rateOfChange(rotated_vector_t0, rotated_vector_t1, delta_time_t0); // compute the capped look ahead with scanline adjustments - float look_ahead_scanline_ms = vectorToScanline(u_fov_vertical_radians, rotated_vector_t0) * u_look_ahead_cfg[2]; + float look_ahead_scanline_ms = u_look_ahead_ms == 0.0 ? 0.0 : vectorToScanline(u_fov_vertical_radians, rotated_vector_t0) * u_look_ahead_cfg[2]; float effective_look_ahead_ms = min(min(u_look_ahead_ms, look_ahead_ms_cap), u_look_ahead_cfg[3]) + look_ahead_scanline_ms; vec3 look_ahead_vector = applyLookAhead(rotated_vector_t0, velocity_t0, effective_look_ahead_ms); @@ -402,8 +496,20 @@ export const VirtualDisplayEffect = GObject.registerClass({ this._initialized = true; } - this.set_uniform_float(this.get_uniform_location('u_look_ahead_ms'), 1, [lookAheadMS(this.imu_snapshots.timestamp_ms, Globals.data_stream.device_data.lookAheadCfg, this.look_ahead_override)]); - this.set_uniform_matrix(this.get_uniform_location("u_imu_data"), false, 4, this.imu_snapshots.imu_data); + let lookAheadSet = false; + if (!this._use_smooth_follow_origin && (this._is_focused() || this._current_follow_ease_progress > 0.0 || !this.smooth_follow_enabled)) { + if (this._current_follow_ease_progress > 0.0 && this._current_follow_ease_progress < 1.0) { + // don't apply look-ahead while the display is slerping + this.set_uniform_float(this.get_uniform_location('u_look_ahead_ms'), 1, [0.0]); + lookAheadSet = true; + } + this.set_uniform_matrix(this.get_uniform_location("u_imu_data"), false, 4, this.imu_snapshots.imu_data); + } else { + this.set_uniform_matrix(this.get_uniform_location("u_imu_data"), false, 4, this.imu_snapshots.smooth_follow_origin); + } + if (!lookAheadSet) { + this.set_uniform_float(this.get_uniform_location('u_look_ahead_ms'), 1, [lookAheadMS(this.imu_snapshots.timestamp_ms, Globals.data_stream.device_data.lookAheadCfg, this.look_ahead_override)]); + } if (!this.disable_anti_aliasing) { // improves sampling quality for smooth text and edges diff --git a/gnome/src/virtualdisplaysactor.js b/gnome/src/virtualdisplaysactor.js index 2a27ed5..87fac17 100644 --- a/gnome/src/virtualdisplaysactor.js +++ b/gnome/src/virtualdisplaysactor.js @@ -7,7 +7,7 @@ import Mtk from 'gi://Mtk'; import Shell from 'gi://Shell'; import St from 'gi://St'; -import { VirtualDisplayEffect } from './virtualdisplayeffect.js'; +import { VirtualDisplayEffect, SMOOTH_FOLLOW_SLERP_TIMELINE_MS } from './virtualdisplayeffect.js'; import { degreeToRadian, diagonalToCrossFOVs } from './math.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; @@ -31,7 +31,7 @@ function applyQuaternionToVector(vector, quaternion) { const FOCUS_THRESHOLD = 0.95 / 2.0; // if we leave the monitor with some margin, unfocus even if no other monitor is in focus -const UNFOCUS_THRESHOLD = 1.1 / 2.0; +const UNFOCUS_THRESHOLD = 1.2 / 2.0; /** * Find the vector in the array that's closest to the quaternion rotation @@ -40,11 +40,12 @@ const UNFOCUS_THRESHOLD = 1.1 / 2.0; * @param {number[][]} monitorVectors - Array of monitor vectors [x, y, z] to search from * @param {number} currentFocusedIndex - Index of the currently focused monitor * @param {number} focusedMonitorDistance - Distance to the focused monitor, < 1.0 if zoomed in + * @param {boolean} smoothFollowEnabled - If true, always keep the current monitor in focus or choose the closest * @param {Object} fovDetails - Contains reference widthPixels, heightPixels, horizontal and vertical radians, and pixel distance to the center of the screen * @param {Object[]} monitorsDetails - Contains x, y, width, height (coordinates from top-left) for each monitor * @returns {number} Index of the closest vector, if it surpasses the previous closest index by a certain margin, otherwise the previous index */ -function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, focusedMonitorDistance, fovDetails, monitorsDetails) { +function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, focusedMonitorDistance, smoothFollowEnabled, fovDetails, monitorsDetails) { const lookVector = [1.0, 0.0, 0.0]; // NWU vector pointing to the center of the screen const rotatedLookVector = applyQuaternionToVector(lookVector, quaternion); @@ -85,6 +86,7 @@ function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, foc )) ); + // TODO - this assumes the display is facing towards us, need to account for looking in the "flat" direction const distanceFromCenterPixels = fovDetails.completeScreenDistancePixels * Math.tan(distance); const distanceFromCenterSizeRatio = distanceFromCenterPixels / monitor.width; @@ -98,9 +100,9 @@ function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, foc } }); - const keepCurrent = currentFocusedIndex !== -1 && currentFocusedDistance < UNFOCUS_THRESHOLD; + const keepCurrent = currentFocusedIndex !== -1 && (smoothFollowEnabled || currentFocusedDistance < UNFOCUS_THRESHOLD); if (!keepCurrent) { - if (closestDistance < FOCUS_THRESHOLD) return closestIndex; + if (smoothFollowEnabled || closestDistance < FOCUS_THRESHOLD) return closestIndex; // neither the current nor the closest will take focus, unfocus all displays return -1; @@ -200,14 +202,19 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch const monitorWrapDetails = monitorWrap(cachedMonitorRadians, sideEdgeRadius, monitorSpacingPixels, monitorDetails.x, monitorDetails.width); const monitorCenterRadius = Math.sqrt(Math.pow(sideEdgeRadius, 2) - Math.pow(monitorDetails.width / 2, 2)); const upTopPixels = monitorDetails.y + (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels; - const upCenterPixels = upTopPixels + monitorDetails.height / 2 - fovDetails.heightPixels / 2; + + // how to place the monitors at the origin (0, 0) + const westCenterOriginPixels = (monitorDetails.width - fovDetails.widthPixels) / 2; + const upCenterOriginPixels = (monitorDetails.height - fovDetails.heightPixels) / 2; + + const upCenterPixels = upTopPixels + upCenterOriginPixels; monitorPlacements.push({ topLeftNoRotate: [ monitorCenterRadius, - // west stays aligned with (0, 0), will apply rotationAngleRadians value during rendering - -(monitorDetails.width - fovDetails.widthPixels) / 2, + // west stays aligned with the origin, will apply rotationAngleRadians value during rendering + -westCenterOriginPixels, // up is flat when wrapping horizontally, apply it here as a constant, not touched by rendering -upTopPixels @@ -221,7 +228,12 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch // up is flat when wrapping horizontally -upCenterPixels ], - center: [ + centerOrigin: [ + monitorCenterRadius, + -westCenterOriginPixels, + upCenterOriginPixels + ], + centerLook: [ // north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians monitorCenterRadius * Math.cos(monitorWrapDetails.center), @@ -249,7 +261,12 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch const monitorWrapDetails = monitorWrap(cachedMonitorRadians, topEdgeRadius, monitorSpacingPixels, monitorDetails.y, monitorDetails.height); const monitorCenterRadius = Math.sqrt(Math.pow(topEdgeRadius, 2) - Math.pow(monitorDetails.height / 2, 2)); const westPixels = monitorDetails.x + (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels; - const westCenterPixels = westPixels + monitorDetails.width / 2 - fovDetails.widthPixels / 2; + + // how to place the monitors at the origin (0, 0) + const westCenterOriginPixels = (monitorDetails.width - fovDetails.widthPixels) / 2; + const upCenterOriginPixels = (monitorDetails.height - fovDetails.heightPixels) / 2; + + const westCenterPixels = westPixels + westCenterOriginPixels; monitorPlacements.push({ topLeftNoRotate: [ @@ -258,8 +275,8 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch // west is flat when wrapping vertically, apply it here as a constant, not touched by rendering westPixels, - // up stays aligned with (0, 0), will apply rotationAngleRadians value during rendering - (monitorDetails.height - fovDetails.heightPixels) / 2 + // up stays aligned with the origin, will apply rotationAngleRadians value during rendering + upCenterOriginPixels ], centerNoRotate: [ monitorCenterRadius, @@ -270,7 +287,12 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch // west centered about the FOV center 0 ], - center: [ + centerOrigin: [ + monitorCenterRadius, + -westCenterOriginPixels, + upCenterOriginPixels + ], + centerLook: [ // north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians monitorCenterRadius * Math.cos(monitorWrapDetails.center), @@ -293,8 +315,14 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch monitorDetailsList.forEach(monitorDetails => { const upPixels = monitorDetails.y + (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels; const westPixels = monitorDetails.x + (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels; - const westCenterPixels = westPixels + monitorDetails.width / 2 - fovDetails.widthPixels / 2; - const upCenterPixels = upPixels + monitorDetails.height / 2 - fovDetails.heightPixels / 2; + + // how to place the monitors at the origin (0, 0) + const westCenterOriginPixels = (monitorDetails.width - fovDetails.widthPixels) / 2; + const upCenterOriginPixels = (monitorDetails.height - fovDetails.heightPixels) / 2; + + const westCenterPixels = westPixels + westCenterOriginPixels; + const upCenterPixels = upPixels + upCenterOriginPixels; + monitorPlacements.push({ topLeftNoRotate: [ fovDetails.completeScreenDistancePixels, @@ -306,7 +334,12 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch westCenterPixels, -upCenterPixels ], - center: [ + centerOrigin: [ + fovDetails.completeScreenDistancePixels, + -westCenterOriginPixels, + upCenterOriginPixels + ], + centerLook: [ fovDetails.completeScreenDistancePixels, -westCenterPixels, -upCenterPixels @@ -384,6 +417,20 @@ export const VirtualDisplaysActor = GObject.registerClass({ 'Latest IMU quaternion snapshots and epoch timestamp for when it was collected', GObject.ParamFlags.READWRITE ), + 'smooth-follow-enabled': GObject.ParamSpec.boolean( + 'smooth-follow-enabled', + 'Smooth follow enabled', + 'Whether smooth follow is enabled', + GObject.ParamFlags.READWRITE, + false + ), + 'smooth-follow-toggle-epoch-ms': GObject.ParamSpec.uint64( + 'smooth-follow-toggle-epoch-ms', + 'Smooth follow toggle epoch time', + 'ms since epoch when smooth follow was toggled', + GObject.ParamFlags.READWRITE, + 0, Number.MAX_SAFE_INTEGER, 0 + ), 'show-banner': GObject.ParamSpec.boolean( 'show-banner', 'Show banner', @@ -541,6 +588,7 @@ export const VirtualDisplaysActor = GObject.registerClass({ notifyToFunction('show-banner', this._handle_banner_update); notifyToFunction('custom-banner-enabled', this._handle_banner_update); notifyToFunction('framerate-cap', this._handle_frame_rate_cap_change); + notifyToFunction('smooth-follow-enabled', this._handle_smooth_follow_enabled_change); this._handle_display_distance_properties_change(); this._handle_frame_rate_cap_change(); @@ -612,6 +660,8 @@ export const VirtualDisplaysActor = GObject.registerClass({ [ 'monitor-placements', 'imu-snapshots', + 'smooth-follow-enabled', + 'smooth-follow-toggle-epoch-ms', 'focused-monitor-index', 'lens-vector', 'look-ahead-override', @@ -650,12 +700,19 @@ export const VirtualDisplaysActor = GObject.registerClass({ if (this.show_banner) { this.focused_monitor_index = -1; - } else if (this.imu_snapshots) { + } else if (this.imu_snapshots && (!this._smooth_follow_slerping || this.focused_monitor_index === -1)) { + // if smooth follow is enabled, use the origin IMU data to inform the initial focused monitor + // since it reflects where the user is looking in relation to the original monitor positions + const currentPoseQuat = this.smooth_follow_enabled ? + this.imu_snapshots.smooth_follow_origin.splice(0, 4) : + this.imu_snapshots.imu_data.splice(0, 4); + const focusedMonitorIndex = findFocusedMonitor( - this.imu_snapshots.imu_data.splice(0, 4), + currentPoseQuat, this._monitorsAsNormalizedVectors, this.focused_monitor_index, this.display_distance / this._display_distance_default(), + this.smooth_follow_enabled, this._fov_details(), this._sorted_monitors ); @@ -773,7 +830,7 @@ export const VirtualDisplaysActor = GObject.registerClass({ // normalize the center vectors this._monitorsAsNormalizedVectors = this.monitor_placements.map(monitorVectors => { - const vector = monitorVectors.center; + const vector = monitorVectors.centerLook; const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]); return [vector[0] / length, vector[1] / length, vector[2] / length]; }); @@ -801,6 +858,17 @@ export const VirtualDisplaysActor = GObject.registerClass({ this._cap_frametime_ms = this.framerate_cap === 0 ? 0.0 : Math.floor(1000 * frametime_margin / this.framerate_cap); } + _handle_smooth_follow_enabled_change() { + if (this._smooth_follow_timeout_id !== undefined) GLib.source_remove(this._smooth_follow_timeout_id); + + this._smooth_follow_slerping = true; + this._smooth_follow_timeout_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, SMOOTH_FOLLOW_SLERP_TIMELINE_MS, (() => { + this._smooth_follow_slerping = false; + this._smooth_follow_timeout_id = undefined; + return GLib.SOURCE_REMOVE; + }).bind(this)); + } + _change_distance() { this.display_distance = this._is_display_distance_at_end ? this.toggle_display_distance_start : this.toggle_display_distance_end; diff --git a/modules/XRLinuxDriver b/modules/XRLinuxDriver index a274fc2..d6b0223 160000 --- a/modules/XRLinuxDriver +++ b/modules/XRLinuxDriver @@ -1 +1 @@ -Subproject commit a274fc23c385b3f039fe6baba0a138fe31c7ad35 +Subproject commit d6b02234553afe9bea2df9e70c5376bd0526a8a9 diff --git a/ui/po/breezydesktop.pot b/ui/po/breezydesktop.pot index 5c771a7..eee5b35 100644 --- a/ui/po/breezydesktop.pot +++ b/ui/po/breezydesktop.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-02 10:59-0800\n" +"POT-Creation-Date: 2025-03-04 15:46-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -215,7 +215,7 @@ msgid "" msgstr "" #: src/gtk/connected-device.ui:66 -msgid "Zoom on focus mode" +msgid "Zoom on focus" msgstr "" #: src/gtk/connected-device.ui:67 diff --git a/ui/po/de.po b/ui/po/de.po index 0e71d63..cd83ce2 100644 --- a/ui/po/de.po +++ b/ui/po/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-02 10:59-0800\n" +"POT-Creation-Date: 2025-03-04 15:46-0800\n" "PO-Revision-Date: 2024-08-02 20:54-0700\n" "Last-Translator: \n" "Language-Team: German \n" @@ -221,7 +221,7 @@ msgid "" msgstr "" #: src/gtk/connected-device.ui:66 -msgid "Zoom on focus mode" +msgid "Zoom on focus" msgstr "" #: src/gtk/connected-device.ui:67 diff --git a/ui/po/es.po b/ui/po/es.po index 6d892c7..8a2c22f 100644 --- a/ui/po/es.po +++ b/ui/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-02 10:59-0800\n" +"POT-Creation-Date: 2025-03-04 15:46-0800\n" "PO-Revision-Date: 2024-08-02 20:55-0700\n" "Last-Translator: \n" "Language-Team: Spanish \n" @@ -220,7 +220,7 @@ msgid "" msgstr "" #: src/gtk/connected-device.ui:66 -msgid "Zoom on focus mode" +msgid "Zoom on focus" msgstr "" #: src/gtk/connected-device.ui:67 diff --git a/ui/po/fr.po b/ui/po/fr.po index 7ce2253..7c11ab6 100644 --- a/ui/po/fr.po +++ b/ui/po/fr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-02 10:59-0800\n" +"POT-Creation-Date: 2025-03-04 15:46-0800\n" "PO-Revision-Date: 2024-08-02 20:54-0700\n" "Last-Translator: \n" "Language-Team: French \n" @@ -223,7 +223,7 @@ msgid "" msgstr "" #: src/gtk/connected-device.ui:66 -msgid "Zoom on focus mode" +msgid "Zoom on focus" msgstr "" #: src/gtk/connected-device.ui:67 diff --git a/ui/po/it.po b/ui/po/it.po index cf3ee74..88e228e 100644 --- a/ui/po/it.po +++ b/ui/po/it.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-02 10:59-0800\n" +"POT-Creation-Date: 2025-03-04 15:46-0800\n" "PO-Revision-Date: 2024-08-02 21:14-0700\n" "Last-Translator: \n" "Language-Team: Italian \n" @@ -221,7 +221,7 @@ msgid "" msgstr "" #: src/gtk/connected-device.ui:66 -msgid "Zoom on focus mode" +msgid "Zoom on focus" msgstr "" #: src/gtk/connected-device.ui:67 diff --git a/ui/po/ja.po b/ui/po/ja.po index 2a4ca05..881de6d 100644 --- a/ui/po/ja.po +++ b/ui/po/ja.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-02 10:59-0800\n" +"POT-Creation-Date: 2025-03-04 15:46-0800\n" "PO-Revision-Date: 2024-08-02 20:55-0700\n" "Last-Translator: \n" "Language-Team: Japanese \n" @@ -223,7 +223,7 @@ msgid "" msgstr "" #: src/gtk/connected-device.ui:66 -msgid "Zoom on focus mode" +msgid "Zoom on focus" msgstr "" #: src/gtk/connected-device.ui:67 diff --git a/ui/po/pl.po b/ui/po/pl.po index 69c3bf7..01fffa2 100644 --- a/ui/po/pl.po +++ b/ui/po/pl.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-02 10:59-0800\n" +"POT-Creation-Date: 2025-03-04 15:46-0800\n" "PO-Revision-Date: 2024-08-16 10:26-0700\n" "Last-Translator: \n" "Language-Team: Polish \n" @@ -216,7 +216,7 @@ msgid "" msgstr "" #: src/gtk/connected-device.ui:66 -msgid "Zoom on focus mode" +msgid "Zoom on focus" msgstr "" #: src/gtk/connected-device.ui:67 diff --git a/ui/po/pt_BR.po b/ui/po/pt_BR.po index 47ab104..73a097c 100644 --- a/ui/po/pt_BR.po +++ b/ui/po/pt_BR.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-02 10:59-0800\n" +"POT-Creation-Date: 2025-03-04 15:46-0800\n" "PO-Revision-Date: 2024-08-19 09:39-0700\n" "Last-Translator: \n" "Language-Team: Brazilian Portuguese \n" "Language-Team: Russian \n" @@ -221,7 +221,7 @@ msgid "" msgstr "" #: src/gtk/connected-device.ui:66 -msgid "Zoom on focus mode" +msgid "Zoom on focus" msgstr "" #: src/gtk/connected-device.ui:67 diff --git a/ui/po/sv.po b/ui/po/sv.po index e077a79..6ad3a67 100644 --- a/ui/po/sv.po +++ b/ui/po/sv.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-02 10:59-0800\n" +"POT-Creation-Date: 2025-03-04 15:46-0800\n" "PO-Revision-Date: 2024-08-16 10:31-0700\n" "Last-Translator: \n" "Language-Team: Swedish \n" @@ -221,7 +221,7 @@ msgid "" msgstr "" #: src/gtk/connected-device.ui:66 -msgid "Zoom on focus mode" +msgid "Zoom on focus" msgstr "" #: src/gtk/connected-device.ui:67 diff --git a/ui/po/uk_UA.po b/ui/po/uk_UA.po index d1c25f6..b4c4cb8 100644 --- a/ui/po/uk_UA.po +++ b/ui/po/uk_UA.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-02 10:59-0800\n" +"POT-Creation-Date: 2025-03-04 15:46-0800\n" "PO-Revision-Date: 2024-08-17 10:08-0700\n" "Last-Translator: \n" "Language-Team: Ukrainian \n" @@ -220,7 +220,7 @@ msgid "" msgstr "" #: src/gtk/connected-device.ui:66 -msgid "Zoom on focus mode" +msgid "Zoom on focus" msgstr "" #: src/gtk/connected-device.ui:67 diff --git a/ui/po/zh_CN.po b/ui/po/zh_CN.po index 29d6041..c8d6f31 100644 --- a/ui/po/zh_CN.po +++ b/ui/po/zh_CN.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-02 10:59-0800\n" +"POT-Creation-Date: 2025-03-04 15:46-0800\n" "PO-Revision-Date: 2024-08-02 20:55-0700\n" "Last-Translator: \n" "Language-Team: Chinese (simplified) \n" @@ -218,7 +218,7 @@ msgid "" msgstr "" #: src/gtk/connected-device.ui:66 -msgid "Zoom on focus mode" +msgid "Zoom on focus" msgstr "" #: src/gtk/connected-device.ui:67 diff --git a/ui/src/gtk/connected-device.ui b/ui/src/gtk/connected-device.ui index dfb20c2..9b8d176 100644 --- a/ui/src/gtk/connected-device.ui +++ b/ui/src/gtk/connected-device.ui @@ -63,7 +63,7 @@ - Zoom on focus mode + Zoom on focus Automatically move a display closer when you look at it. Set your preferred focused and unfocused distances in the Adjustments section.