Add support for smooth follow of a single display

This commit is contained in:
wheaney 2025-03-04 15:46:47 -08:00
parent 66c035bd14
commit 7a91b2e39b
18 changed files with 265 additions and 71 deletions

View File

@ -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;
}

View File

@ -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');
}

View File

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

View File

@ -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;

@ -1 +1 @@
Subproject commit a274fc23c385b3f039fe6baba0a138fe31c7ad35
Subproject commit d6b02234553afe9bea2df9e70c5376bd0526a8a9

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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

View File

@ -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: <wayne@xronlinux.com>\n"
"Language-Team: German <translation-team-de@lists.sourceforge.net>\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

View File

@ -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: <wayne@xronlinux.com>\n"
"Language-Team: Spanish <es@tp.org.es>\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

View File

@ -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: <wayne@xronlinux.com>\n"
"Language-Team: French <traduc@traduc.org>\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

View File

@ -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: <wayne@xronlinux.com>\n"
"Language-Team: Italian <tp@lists.linux.it>\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

View File

@ -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: <wayne@xronlinux.com>\n"
"Language-Team: Japanese <translation-team-ja@lists.sourceforge.net>\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

View File

@ -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: <wayne@xronlinux.com>\n"
"Language-Team: Polish <translation-team-pl@lists.sourceforge.net>\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

View File

@ -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: <wayne@xronlinux.com>\n"
"Language-Team: Brazilian Portuguese <ldpbr-"
@ -222,7 +222,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

View File

@ -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 09:39-0700\n"
"Last-Translator: <wayne@xronlinux.com>\n"
"Language-Team: Russian <gnu@d07.ru>\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

View File

@ -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: <wayne@xronlinux.com>\n"
"Language-Team: Swedish <tp-sv@listor.tp-sv.se>\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

View File

@ -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: <wayne@xronlinux.com>\n"
"Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\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

View File

@ -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: <wayne@xronlinux.com>\n"
"Language-Team: Chinese (simplified) <i18n-zh@googlegroups.com>\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

View File

@ -63,7 +63,7 @@
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes"><!-- feature switch -->Zoom on focus mode</property>
<property name="title" translatable="yes"><!-- feature switch -->Zoom on focus</property>
<property name="subtitle" translatable="yes"><!--
-->Automatically move a display closer when you look at it.
<!-- -->Set your preferred focused and unfocused distances in the Adjustments section.