diff --git a/gnome/src/devicedatastream.js b/gnome/src/devicedatastream.js index e87cbd1..309cdd8 100644 --- a/gnome/src/devicedatastream.js +++ b/gnome/src/devicedatastream.js @@ -176,8 +176,9 @@ export const DeviceDataStream = GObject.registerClass({ imuResetState, displayRes: dataViewUint32Array(dataView, DISPLAY_RES), sbsEnabled, - displayFov: 44.0, // dataViewFloat(dataView, DISPLAY_FOV), + displayFov: dataViewFloat(dataView, DISPLAY_FOV), lookAheadCfg: dataViewFloatArray(dataView, LOOK_AHEAD_CFG), + lensDistanceRatio: dataViewFloat(dataView, LENS_DISTANCE_RATIO) }; } else if (keepalive_only) { this.device_data = { @@ -216,14 +217,11 @@ export const DeviceDataStream = GObject.registerClass({ } } - if (success) { - // update the supported device connected property if the state changes, trigger "notify::" events - this.breezy_desktop_actually_running = enabled && validKeepalive; - if (this.breezy_desktop_running !== this.breezy_desktop_actually_running) this.breezy_desktop_running = this.breezy_desktop_actually_running; - } + this.breezy_desktop_actually_running = success && enabled && validKeepalive; + } else { + this.breezy_desktop_actually_running = false; } } else { - this.breezy_desktop_running = false; this.breezy_desktop_actually_running = false; } } @@ -237,7 +235,8 @@ export const DeviceDataStream = GObject.registerClass({ displayRes: [1920.0, 1080.0], sbsEnabled: false, displayFov: 46.0, - lookAheadCfg: [0.0, 0.0, 0.0, 0.0] + lookAheadCfg: [0.0, 0.0, 0.0, 0.0], + lensDistanceRatio: 0.05 } } this.was_debug_no_device = true; @@ -261,7 +260,9 @@ export const DeviceDataStream = GObject.registerClass({ }; } this.breezy_desktop_running = true; - return; + } 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; } } }); \ No newline at end of file diff --git a/gnome/src/extension.js b/gnome/src/extension.js index a6f5365..d3b7cb6 100644 --- a/gnome/src/extension.js +++ b/gnome/src/extension.js @@ -57,6 +57,7 @@ export default class BreezyDesktopExtension extends Extension { this._widescreen_mode_settings_connection = null; this._widescreen_mode_effect_state_connection = null; this._breezy_desktop_running_connection = null; + this._debug_no_device_binding = null; this._start_binding = null; this._end_binding = null; this._curved_display_binding = null; @@ -82,7 +83,6 @@ export default class BreezyDesktopExtension extends Extension { Globals.data_stream = new DeviceDataStream({ debug_no_device: this.settings.get_boolean('debug-no-device') }); - this.settings.bind('debug-no-device', Globals.data_stream, 'debug-no-device', Gio.SettingsBindFlags.DEFAULT); } } @@ -108,6 +108,10 @@ export default class BreezyDesktopExtension extends Extension { this._monitor_manager, 'use-optimal-monitor-config', Gio.SettingsBindFlags.DEFAULT); this._headset_as_primary_binding = this.settings.bind('headset-as-primary', this._monitor_manager, 'headset-as-primary', Gio.SettingsBindFlags.DEFAULT); + this._debug_no_device_binding = this.settings.bind('debug-no-device', + Globals.data_stream, 'debug-no-device', Gio.SettingsBindFlags.DEFAULT); + this._breezy_desktop_running_connection = Globals.data_stream.connect('notify::breezy-desktop-running', + this._handle_breezy_desktop_running_change.bind(this)); this._cli_file = Gio.file_new_for_path(XDG_CLI_PATH); if (!this._cli_file.query_exists(null)) { @@ -124,39 +128,6 @@ export default class BreezyDesktopExtension extends Extension { } } - _poll_for_ready() { - Globals.logger.log_debug('BreezyDesktopExtension _poll_for_ready'); - var target_monitor = this._target_monitor; - var is_effect_running = this._is_effect_running; - this._running_poller_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, (() => { - try { - if (is_effect_running) { - this._running_poller_id = undefined; - return GLib.SOURCE_REMOVE; - } - - if (Globals.data_stream.breezy_desktop_running && target_monitor) { - // Don't enable the effect yet if monitor updates are needed. - // _setup will be triggered again since a !ready result means it will trigger monitor changes, - // so we can remove this timeout_add no matter what. - if (this._target_monitor_ready(target_monitor)) { - Globals.logger.log('Breezy enabled, supported monitor connected. Enabling XR effect.'); - this._effect_enable(); - } - this._running_poller_id = undefined; - return GLib.SOURCE_REMOVE; - } else { - Globals.logger.log_debug(`BreezyDesktopExtension _poll_for_ready - breezy enabled: ${Globals.data_stream.breezy_desktop_running}, target_monitor: ${!!target_monitor}`); - return GLib.SOURCE_CONTINUE; - } - } catch (e) { - Globals.logger.log(`[ERROR] BreezyDesktopExtension _poll_for_ready ${e.message}\n${e.stack}`); - this._running_poller_id = undefined; - return GLib.SOURCE_REMOVE; - } - }).bind(this)); - } - _find_virtual_monitors() { try { Globals.logger.log_debug('BreezyDesktopExtension _find_virtual_monitors'); @@ -215,8 +186,7 @@ export default class BreezyDesktopExtension extends Extension { } // Assumes target_monitor is set, and was returned by _find_supported_monitor. - // A false result means we'll expect _handle_monitor_change to be triggered, so active polling - // can be disabled. + // A false result means we'll expect _handle_monitor_change to be triggered _target_monitor_ready(target_monitor) { if (target_monitor.is_dummy) return true; @@ -233,31 +203,23 @@ export default class BreezyDesktopExtension extends Extension { Globals.logger.log('Reset triggered, disabling XR effect'); this._effect_disable(!for_disable); } - const target_monitor = this._find_supported_monitor(); - - // if target_monitor isn't set, do nothing and wait for MonitorManager to call this again - if (target_monitor && this._running_poller_id === undefined) { - this._target_monitor = target_monitor; + this._target_monitor = this._find_supported_monitor(); + if (this._target_monitor) { if (Globals.data_stream.breezy_desktop_running) { // Don't enable the effect yet if monitor updates are needed. // _setup will be triggered again since a !ready result means it will trigger monitor changes - if (this._target_monitor_ready(target_monitor)) { + if (this._target_monitor_ready(this._target_monitor)) { Globals.logger.log('Ready, enabling XR effect'); this._effect_enable(); } else { - Globals.logger.log_debug('BreezyDesktopExtension _setup - breezy desktop enabled but async monitor action needed'); + Globals.logger.log_debug('BreezyDesktopExtension _setup - breezy desktop enabled, but async monitor action needed'); } } else { - Globals.logger.log_debug('BreezyDesktopExtension _setup - breezy desktop not enabled, starting poller'); - this._poll_for_ready(); + Globals.logger.log_debug('BreezyDesktopExtension _setup - Doing nothing, target monitor found, but device stream not being received'); } } else { - if (!target_monitor) { - Globals.logger.log_debug(`BreezyDesktopExtension _setup - Doing nothing, no supported monitor found`); - } else { - Globals.logger.log_debug(`BreezyDesktopExtension _setup - Doing nothing, target monitor found, waiting for poller to pick it up`); - } + Globals.logger.log_debug(`BreezyDesktopExtension _setup - Doing nothing, no supported monitor found, breezy_desktop_running: ${Globals.data_stream.breezy_desktop_running}`); } } @@ -277,7 +239,6 @@ export default class BreezyDesktopExtension extends Extension { _effect_enable() { Globals.logger.log_debug('BreezyDesktopExtension _effect_enable'); - this._running_poller_id = undefined; if (!this._is_effect_running) { this._is_effect_running = true; @@ -333,7 +294,6 @@ export default class BreezyDesktopExtension extends Extension { // this._update_widescreen_mode_from_settings(this.settings); // this._widescreen_mode_effect_state_connection = this._xr_effect.connect('notify::widescreen-mode-state', this._update_widescreen_mode_from_state.bind(this)); - this._breezy_desktop_running_connection = Globals.data_stream.connect('notify::breezy-desktop-running', this._handle_breezy_desktop_running_change.bind(this)); this._overlay_content.renderMonitors(); this._monitor_wrapping_scheme_binding = this.settings.bind('monitor-wrapping-scheme', this._overlay_content, 'monitor-wrapping-scheme', Gio.SettingsBindFlags.DEFAULT); @@ -467,7 +427,7 @@ export default class BreezyDesktopExtension extends Extension { this._sbs_mode_update_timeout = undefined; if (this.settings.get_boolean('fast-sbs-mode-switching')) { - // setup and polling were halted if this is enabled, so we have to re-trigger setup + // setup was halted if this is enabled, so we have to re-trigger it now this._setup(); } @@ -519,13 +479,11 @@ export default class BreezyDesktopExtension extends Extension { } _handle_breezy_desktop_running_change(effect, _pspec) { - const breezy_desktop_running = effect.breezy_desktop_running; - Globals.logger.log_debug(`BreezyDesktopExtension _handle_breezy_desktop_running_change ${breezy_desktop_running}`); + Globals.logger.log_debug(`BreezyDesktopExtension _handle_breezy_desktop_running_change ${effect.breezy_desktop_running}`); - // this will disable the effect and begin polling for a ready state again - if (!breezy_desktop_running && this._is_effect_running) { - Globals.logger.log('Breezy desktop disabled'); - this._setup(true); + if (effect.breezy_desktop_running !== this._is_effect_running) { + if (!effect.breezy_desktop_running) Globals.logger.log('Breezy desktop disabled'); + this._setup(!effect.breezy_desktop_running); } } @@ -576,12 +534,6 @@ export default class BreezyDesktopExtension extends Extension { Globals.logger.log_debug('BreezyDesktopExtension _effect_disable'); this._is_effect_running = false; - if (this._running_poller_id) { - const poller_id = this._running_poller_id; - this._running_poller_id = undefined; - GLib.source_remove(poller_id); - } - Main.wm.removeKeybinding('recenter-display-shortcut'); Main.wm.removeKeybinding('toggle-display-distance-shortcut'); Main.wm.removeKeybinding('toggle-follow-shortcut'); @@ -668,10 +620,6 @@ export default class BreezyDesktopExtension extends Extension { // this._xr_effect.disconnect(this._widescreen_mode_effect_state_connection); // this._widescreen_mode_effect_state_connection = null; // } - if (this._breezy_desktop_running_connection) { - Globals.data_stream.disconnect(this._breezy_desktop_running_connection); - this._breezy_desktop_running_connection = null; - } this._overlay_content.destroy(); this._overlay_content = null; } @@ -700,6 +648,15 @@ export default class BreezyDesktopExtension extends Extension { this._effect_disable(); Globals.data_stream.stop(); this._target_monitor = null; + + if (this._breezy_desktop_running_connection) { + Globals.data_stream.disconnect(this._breezy_desktop_running_connection); + this._breezy_desktop_running_connection = null; + } + if (this._debug_no_device_binding) { + this.settings.unbind(this._debug_no_device_binding); + this._debug_no_device_binding = null; + } if (this._monitor_manager) { if (this._optimal_monitor_config_binding) { diff --git a/gnome/src/virtualmonitorsactor.js b/gnome/src/virtualmonitorsactor.js index 3c250b6..3fd0620 100644 --- a/gnome/src/virtualmonitorsactor.js +++ b/gnome/src/virtualmonitorsactor.js @@ -131,32 +131,25 @@ function monitorWrap(cachedMonitorRadians, radiusPixels, monitorSpacingPixels, m * Convert the given monitor details into NWU vectors describing the center of the fully placed monitor, * and the top-left of the partially placed monitor (minus only a single-axis rotation) * - * @param {Object} fovDetails - contains reference fovDegrees (diagonal), widthPixels, heightPixels + * @param {Object} fovDetails - contains reference widthPixels, heightPixels, horizontal and vertical radians, +* and distance to the center of the screen * @param {Object[]} monitorDetailsList - contains x, y, width, height (coordinates from top-left) * @param {string} monitorWrappingScheme - horizontal, vertical, none * @returns {Object[]} - contains NWU vectors pointing to `topLeftNoRotate` and `center` of each monitor * and a `rotation` angle for the given wrapping scheme */ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingScheme, monitorSpacing) { - const aspect = fovDetails.widthPixels / fovDetails.heightPixels; - const fovVerticalRadiansInitial = degreesToRadians(fovDetails.fovDegrees / Math.sqrt(1 + aspect * aspect)); - const fovVerticalRadians = Math.atan(Math.tan(fovVerticalRadiansInitial) / fovDetails.distanceAdjustment); - - // distance needed for the FOV-sized monitor to fill up the screen - const centerRadius = fovDetails.heightPixels / 2 / Math.sin(fovVerticalRadians / 2); - const monitorPlacements = []; const cachedMonitorRadians = {}; if (monitorWrappingScheme === 'horizontal') { // monitors wrap around us horizontally - const fovHorizontalRadians = fovVerticalRadians * aspect; // distance to a horizontal edge is the hypothenuse of the triangle where the opposite side is half the width of the reference fov screen - const edgeRadius = fovDetails.widthPixels / 2 / Math.sin(fovHorizontalRadians / 2); + const edgeRadius = fovDetails.widthPixels / 2 / Math.sin(fovDetails.horizontalRadians / 2); const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels; - cachedMonitorRadians[0] = -fovHorizontalRadians / 2; + cachedMonitorRadians[0] = -fovDetails.horizontalRadians / 2; monitorDetailsList.forEach(monitorDetails => { const monitorWrapDetails = monitorWrap(cachedMonitorRadians, edgeRadius, monitorSpacingPixels, monitorDetails.x, monitorDetails.width); const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.width / 2, 2)); @@ -202,10 +195,10 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch // monitors wrap around us vertically // distance to a vertical edge is the hypothenuse of the triangle where the opposite side is half the height of the reference fov screen - const edgeRadius = fovDetails.heightPixels / 2 / Math.sin(fovVerticalRadians / 2); + const edgeRadius = fovDetails.heightPixels / 2 / Math.sin(fovDetails.verticalRadians / 2); const monitorSpacingPixels = monitorSpacing * fovDetails.heightPixels; - cachedMonitorRadians[0] = -fovVerticalRadians / 2; + cachedMonitorRadians[0] = -fovDetails.verticalRadians / 2; monitorDetailsList.forEach(monitorDetails => { const monitorWrapDetails = monitorWrap(cachedMonitorRadians, edgeRadius, monitorSpacingPixels, monitorDetails.y, monitorDetails.height); const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.height / 2, 2)); @@ -258,17 +251,17 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch const upCenterPixels = upPixels + monitorDetails.height / 2 - fovDetails.heightPixels / 2; monitorPlacements.push({ topLeftNoRotate: [ - centerRadius, + fovDetails.fullScreenDistance, westPixels, -upPixels ], centerNoRotate: [ - centerRadius, + fovDetails.fullScreenDistance, westCenterPixels, -upCenterPixels ], center: [ - centerRadius, + fovDetails.fullScreenDistance, -westCenterPixels, -upCenterPixels ], @@ -376,6 +369,12 @@ export const VirtualMonitorEffect = GObject.registerClass({ 2.5, 1.0 ), + 'lens-vector': GObject.ParamSpec.jsobject( + 'lens-vector', + 'Lens Vector', + 'Vector representing the offset of the lens from the pivot point', + GObject.ParamFlags.READWRITE + ), 'actor-to-display-ratios': GObject.ParamSpec.jsobject( 'actor-to-display-ratios', 'Actor to Display Ratios', @@ -512,6 +511,7 @@ export const VirtualMonitorEffect = GObject.registerClass({ uniform float u_rotation_x_radians; uniform float u_rotation_y_radians; uniform vec2 u_display_resolution; + uniform vec3 u_lens_vector; // vector positions are relative to the width and height of the entire stage uniform vec2 u_actor_to_display_ratios; @@ -526,10 +526,9 @@ export const VirtualMonitorEffect = GObject.registerClass({ return vec4(-q.xyz, q.w); } - vec4 applyQuaternionToVector(vec4 v, vec4 q) { - vec3 t = 2.0 * cross(q.xyz, v.xyz); - vec3 rotated = v.xyz + q.w * t + cross(q.xyz, t); - return vec4(rotated, v.w); + vec3 applyQuaternionToVector(vec3 v, vec4 q) { + vec3 t = 2.0 * cross(q.xyz, v); + return v + q.w * t + cross(q.xyz, t); } vec4 applyXRotationToVector(vec4 v, float angle) { @@ -575,9 +574,10 @@ export const VirtualMonitorEffect = GObject.registerClass({ float cogl_position_width = cogl_position_mystery_factor * aspect_ratio / u_actor_to_display_ratios.y; float cogl_position_height = cogl_position_width / aspect_ratio; - world_pos.x -= u_display_position.x * cogl_position_width / u_display_resolution.x; - world_pos.y -= u_display_position.y * cogl_position_height / u_display_resolution.y; - world_pos.z = u_display_position.z * cogl_position_mystery_factor / u_display_resolution.x; + vec3 pos_factors = vec3(cogl_position_width / u_display_resolution.x, cogl_position_height / u_display_resolution.y, cogl_position_mystery_factor / u_display_resolution.x); + world_pos.x -= u_display_position.x * pos_factors.x; + world_pos.y -= u_display_position.y * pos_factors.y; + world_pos.z = u_display_position.z * pos_factors.z; // if the perspective includes more than just our viewport actor, move vertices towards the center of the perspective so they'll be properly rotated world_pos.x += u_actor_to_display_offsets.x * cogl_position_width / 2; @@ -587,8 +587,11 @@ export const VirtualMonitorEffect = GObject.registerClass({ world_pos = applyXRotationToVector(world_pos, u_rotation_x_radians); world_pos = applyYRotationToVector(world_pos, u_rotation_y_radians); - vec3 rotated_vector_t0 = applyQuaternionToVector(world_pos, nwuToESU(quatConjugate(u_imu_data[0]))).xyz; - vec3 rotated_vector_t1 = applyQuaternionToVector(world_pos, nwuToESU(quatConjugate(u_imu_data[1]))).xyz; + vec4 quat_t0 = nwuToESU(quatConjugate(u_imu_data[0])); + vec3 adjusted_lens_vector = u_lens_vector * pos_factors; + vec3 complete_vector = world_pos.xyz + adjusted_lens_vector; + vec3 rotated_vector_t0 = applyQuaternionToVector(complete_vector, quat_t0); + vec3 rotated_vector_t1 = applyQuaternionToVector(complete_vector, nwuToESU(quatConjugate(u_imu_data[1]))); float delta_time_t0 = u_imu_data[3][0] - u_imu_data[3][1]; vec3 velocity_t0 = rateOfChange(rotated_vector_t0, rotated_vector_t1, delta_time_t0); @@ -596,7 +599,10 @@ export const VirtualMonitorEffect = GObject.registerClass({ float look_ahead_scanline_ms = 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; - world_pos = vec4(applyLookAhead(rotated_vector_t0, velocity_t0, effective_look_ahead_ms), world_pos.w); + vec3 look_ahead_vector = applyLookAhead(rotated_vector_t0, velocity_t0, effective_look_ahead_ms); + + vec3 rotated_lens_vector = applyQuaternionToVector(adjusted_lens_vector, quat_t0); + world_pos = vec4(look_ahead_vector - rotated_lens_vector, world_pos.w); world_pos.z /= aspect_ratio / u_actor_to_display_ratios.y; world_pos.x *= u_actor_to_display_ratios.y / u_actor_to_display_ratios.x; @@ -634,6 +640,7 @@ export const VirtualMonitorEffect = GObject.registerClass({ this.set_uniform_float(this.get_uniform_location("u_look_ahead_cfg"), 4, Globals.data_stream.device_data.lookAheadCfg); this.set_uniform_float(this.get_uniform_location("u_actor_to_display_ratios"), 2, this.actor_to_display_ratios); this.set_uniform_float(this.get_uniform_location("u_actor_to_display_offsets"), 2, this.actor_to_display_offsets); + this.set_uniform_float(this.get_uniform_location("u_lens_vector"), 3, this.lens_vector); this._update_display_position_uniforms(); this._initialized = true; } @@ -734,6 +741,12 @@ export const VirtualMonitorsActor = GObject.registerClass({ 2.5, 1.05 ), + 'lens-vector': GObject.ParamSpec.jsobject( + 'lens-vector', + 'Lens Vector', + 'Vector representing the offset of the lens from the pivot point', + GObject.ParamFlags.READWRITE + ), 'toggle-display-distance-start': GObject.ParamSpec.double( 'toggle-display-distance-start', 'Display distance start', @@ -782,7 +795,10 @@ export const VirtualMonitorsActor = GObject.registerClass({ this.width = this.target_monitor.width; this.height = this.target_monitor.height; - this._cap_frametime_ms = Math.floor(1000 / this.framerate_cap); + + // add a margin to the cap time so we don't cut off frames that come in close + const frametime_margin = 0.75; + this._cap_frametime_ms = Math.floor(1000 * frametime_margin / this.framerate_cap); this._all_monitors = [ this.target_monitor, @@ -835,7 +851,8 @@ export const VirtualMonitorsActor = GObject.registerClass({ display_distance: this.display_distance, display_distance_default: this._display_distance_default(), actor_to_display_ratios: actorToDisplayRatios, - actor_to_display_offsets: actorToDisplayOffsets + actor_to_display_offsets: actorToDisplayOffsets, + lens_vector: this.lens_vector }); containerActor.add_effect_with_name('viewport-effect', effect); this.add_child(containerActor); @@ -845,6 +862,7 @@ export const VirtualMonitorsActor = GObject.registerClass({ this.bind_property('imu-snapshots', effect, 'imu-snapshots', GObject.BindingFlags.DEFAULT); this.bind_property('focused-monitor-index', effect, 'focused-monitor-index', GObject.BindingFlags.DEFAULT); this.bind_property('display-distance', effect, 'display-distance', GObject.BindingFlags.DEFAULT); + this.bind_property('lens-vector', effect, 'lens-vector', GObject.BindingFlags.DEFAULT); this.bind_property('look-ahead-override', effect, 'look-ahead-override', GObject.BindingFlags.DEFAULT); this.bind_property('disable-anti-aliasing', effect, 'disable-anti-aliasing', GObject.BindingFlags.DEFAULT); @@ -921,12 +939,24 @@ export const VirtualMonitorsActor = GObject.registerClass({ } } + const aspect = this.width / this.height; + const fovVerticalRadiansInitial = degreesToRadians(Globals.data_stream.device_data.displayFov / Math.sqrt(1 + aspect * aspect)); + const fovVerticalRadians = Math.atan(Math.tan(fovVerticalRadiansInitial) / this._display_distance_default()); + + // distance needed for the FOV-sized monitor to fill up the screen + const fullScreenDistance = this.height / 2 / Math.sin(fovVerticalRadians / 2); + + // full screen distance + lens distance + const completeScreenDistance = fullScreenDistance / (1.0 - Globals.data_stream.device_data.lensDistanceRatio); + + this.lens_vector = [0.0, 0.0, -Globals.data_stream.device_data.lensDistanceRatio * completeScreenDistance]; this.monitor_placements = monitorsToPlacements( { - fovDegrees: Globals.data_stream.device_data.displayFov, widthPixels: this.width, heightPixels: this.height, - distanceAdjustment: this._display_distance_default() + verticalRadians: fovVerticalRadians, + horizontalRadians: fovVerticalRadians * aspect, + fullScreenDistance }, // shift all monitors so they center around the target monitor, then adjusted by the offsets