Reorg and cleanup
This commit is contained in:
parent
395db4b53f
commit
d7ebba9eeb
|
|
@ -1,4 +1,3 @@
|
|||
import Clutter from 'gi://Clutter'
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import Meta from 'gi://Meta';
|
||||
|
|
@ -10,7 +9,7 @@ import { DeviceDataStream } from './devicedatastream.js';
|
|||
import Globals from './globals.js';
|
||||
import { Logger } from './logger.js';
|
||||
import { MonitorManager } from './monitormanager.js';
|
||||
import { VirtualMonitorsActor } from './virtualmonitorsactor.js';
|
||||
import { VirtualDisplaysActor } from './virtualdisplaysactor.js';
|
||||
|
||||
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
|
@ -43,36 +42,16 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
// Set/destroyed by enable/disable
|
||||
this._cursor_manager = null;
|
||||
this._monitor_manager = null;
|
||||
this._overlay_content = null;
|
||||
this._overlay = null;
|
||||
this._virtual_displays_actor = null;
|
||||
this._virtual_displays_overlay = null;
|
||||
this._target_monitor = null;
|
||||
this._is_effect_running = false;
|
||||
this._distance_binding = null;
|
||||
this._show_banner_binding = null;
|
||||
this._effect_settings_bindings = [];
|
||||
this._data_stream_bindings = [];
|
||||
this._show_banner_connection = null;
|
||||
this._custom_banner_enabled_binding = null;
|
||||
this._monitor_wrapping_scheme_binding = null;
|
||||
this._viewport_offset_x_binding = null;
|
||||
this._viewport_offset_y_binding = null;
|
||||
this._monitor_spacing_binding = null;
|
||||
this._distance_connection = null;
|
||||
this._follow_threshold_connection = null;
|
||||
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;
|
||||
this._display_size_binding = null;
|
||||
this._look_ahead_override_binding = null;
|
||||
this._disable_anti_aliasing_binding = null;
|
||||
this._framerate_cap_binding = null;
|
||||
this._optimal_monitor_config_binding = null;
|
||||
this._headset_as_primary_binding = null;
|
||||
this._actor_added_connection = null;
|
||||
this._actor_removed_connection = null;
|
||||
this._data_stream_connection = null;
|
||||
|
||||
if (!Globals.logger) {
|
||||
Globals.logger = new Logger({
|
||||
|
|
@ -94,7 +73,6 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
|
||||
try {
|
||||
Globals.extension_dir = this.path;
|
||||
this.settings.bind('debug', Globals.logger, 'debug', Gio.SettingsBindFlags.DEFAULT);
|
||||
|
||||
Globals.data_stream.start();
|
||||
|
||||
|
|
@ -107,12 +85,11 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
this._monitor_manager.setChangeHook(this._handle_monitor_change.bind(this));
|
||||
this._monitor_manager.enable();
|
||||
|
||||
this._optimal_monitor_config_binding = this.settings.bind('use-optimal-monitor-config',
|
||||
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.settings.bind('debug', Globals.logger, 'debug', Gio.SettingsBindFlags.DEFAULT);
|
||||
this.settings.bind('use-optimal-monitor-config',this._monitor_manager, 'use-optimal-monitor-config', Gio.SettingsBindFlags.DEFAULT);
|
||||
this.settings.bind('headset-as-primary', this._monitor_manager, 'headset-as-primary', Gio.SettingsBindFlags.DEFAULT);
|
||||
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));
|
||||
|
||||
|
|
@ -251,22 +228,22 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
const refreshRate = targetMonitor.refreshRate ?? 60;
|
||||
|
||||
// use rgba(255, 4, 144, 1) for chroma key background
|
||||
this._overlay = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);', clip_to_allocation: true });
|
||||
this._overlay.opacity = 255;
|
||||
this._overlay.set_position(targetMonitor.x, targetMonitor.y);
|
||||
this._overlay.set_size(targetMonitor.width, targetMonitor.height);
|
||||
this._virtual_displays_overlay = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);', clip_to_allocation: true });
|
||||
this._virtual_displays_overlay.opacity = 255;
|
||||
this._virtual_displays_overlay.set_position(targetMonitor.x, targetMonitor.y);
|
||||
this._virtual_displays_overlay.set_size(targetMonitor.width, targetMonitor.height);
|
||||
|
||||
// const textureSourceActor = Main.layoutManager.uiGroup;
|
||||
Globals.data_stream.refresh_data();
|
||||
this._overlay_content = new VirtualMonitorsActor({
|
||||
this._virtual_displays_actor = new VirtualDisplaysActor({
|
||||
width: targetMonitor.width,
|
||||
height: targetMonitor.height,
|
||||
target_monitor: targetMonitor,
|
||||
virtual_monitors: virtualMonitors,
|
||||
monitor_wrapping_scheme: this.settings.get_string('monitor-wrapping-scheme'),
|
||||
monitor_spacing: this.settings.get_int('monitor-spacing'),
|
||||
viewport_offset_x: this.settings.get_double('viewport-offset-x'),
|
||||
viewport_offset_y: this.settings.get_double('viewport-offset-y'),
|
||||
target_monitor: targetMonitor,
|
||||
display_distance: this.settings.get_double('display-distance'),
|
||||
toggle_display_distance_start: this.settings.get_double('toggle-display-distance-start'),
|
||||
toggle_display_distance_end: this.settings.get_double('toggle-display-distance-end'),
|
||||
|
|
@ -276,13 +253,13 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
custom_banner_enabled: Globals.data_stream.custom_banner_enabled
|
||||
});
|
||||
|
||||
this._overlay.set_child(this._overlay_content);
|
||||
this._overlay_content.renderMonitors();
|
||||
this._virtual_displays_overlay.set_child(this._virtual_displays_actor);
|
||||
this._virtual_displays_actor.renderMonitors();
|
||||
|
||||
Shell.util_set_hidden_from_pick(this._overlay, true);
|
||||
global.stage.add_child(this._overlay);
|
||||
Shell.util_set_hidden_from_pick(this._virtual_displays_overlay, true);
|
||||
global.stage.add_child(this._virtual_displays_overlay);
|
||||
|
||||
const cursor_manager_monitor_objs = this._overlay_content.monitor_actors.map(monitor => {
|
||||
const cursor_manager_monitor_objs = this._virtual_displays_actor.monitor_actors.map(monitor => {
|
||||
return {
|
||||
monitor: monitor.monitorDetails,
|
||||
actor: monitor.containerActor
|
||||
|
|
@ -294,41 +271,42 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
|
||||
this._update_follow_threshold(this.settings);
|
||||
|
||||
// this gets triggered before _effect_enable if in fast-sbs-mode-switching mode
|
||||
// if (!this.settings.get_boolean('fast-sbs-mode-switching'))
|
||||
// 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._show_banner_binding = Globals.data_stream.bind_property('show-banner', this._overlay_content, 'show-banner', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._data_stream_bindings = [
|
||||
'show-banner',
|
||||
'custom-banner-enabled'
|
||||
].map(data_stream_key =>
|
||||
Globals.data_stream.bind_property(data_stream_key, this._virtual_displays_actor, data_stream_key, Gio.SettingsBindFlags.DEFAULT)
|
||||
);
|
||||
|
||||
this._show_banner_connection = Globals.data_stream.connect('notify::show-banner', this._handle_show_banner_update.bind(this));
|
||||
this._was_show_banner = Globals.data_stream.show_banner;
|
||||
if (!this._was_show_banner) this._recenter_display();
|
||||
|
||||
this._custom_banner_enabled_binding = Globals.data_stream.bind_property('custom-banner-enabled', this._overlay_content, 'custom-banner-enabled', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._effect_settings_bindings = [
|
||||
'monitor-wrapping-scheme',
|
||||
'viewport-offset-x',
|
||||
'viewport-offset-y',
|
||||
'monitor-spacing',
|
||||
'display-distance',
|
||||
'toggle-display-distance-start',
|
||||
'toggle-display-distance-end',
|
||||
'display-size',
|
||||
'framerate-cap',
|
||||
'look-ahead-override',
|
||||
'disable-anti-aliasing'
|
||||
]
|
||||
this._effect_settings_bindings.forEach(settings_key =>
|
||||
this.settings.bind(settings_key, this._virtual_displays_actor, settings_key, Gio.SettingsBindFlags.DEFAULT)
|
||||
);
|
||||
|
||||
this._monitor_wrapping_scheme_binding = this.settings.bind('monitor-wrapping-scheme', this._overlay_content, 'monitor-wrapping-scheme', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._viewport_offset_x_binding = this.settings.bind('viewport-offset-x', this._overlay_content, 'viewport-offset-x', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._viewport_offset_y_binding = this.settings.bind('viewport-offset-y', this._overlay_content, 'viewport-offset-y', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._monitor_spacing_binding = this.settings.bind('monitor-spacing', this._overlay_content, 'monitor-spacing', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._distance_binding = this.settings.bind('display-distance', this._overlay_content, 'display-distance', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._distance_connection = this.settings.connect('changed::display-distance', this._update_display_distance.bind(this));
|
||||
this._follow_threshold_connection = this.settings.connect('changed::follow-threshold', this._update_follow_threshold.bind(this));
|
||||
|
||||
// this._widescreen_mode_settings_connection = this.settings.connect('changed::widescreen-mode', this._update_widescreen_mode_from_settings.bind(this))
|
||||
this._start_binding = this.settings.bind('toggle-display-distance-start', this._overlay_content, 'toggle-display-distance-start', Gio.SettingsBindFlags.DEFAULT)
|
||||
this._end_binding = this.settings.bind('toggle-display-distance-end', this._overlay_content, 'toggle-display-distance-end', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._display_size_binding = this.settings.bind('display-size', this._overlay_content, 'display-size', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._framerate_cap_binding = this.settings.bind('framerate-cap', this._overlay_content, 'framerate-cap', Gio.SettingsBindFlags.DEFAULT);
|
||||
// this._curved_display_binding = this.settings.bind('curved-display', this._xr_effect, 'curved-display', Gio.SettingsBindFlags.DEFAULT)
|
||||
this._look_ahead_override_binding = this.settings.bind('look-ahead-override', this._overlay_content, 'look-ahead-override', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._disable_anti_aliasing_binding = this.settings.bind('disable-anti-aliasing', this._overlay_content, 'disable-anti-aliasing', Gio.SettingsBindFlags.DEFAULT);
|
||||
|
||||
Meta.disable_unredirect_for_display(global.display);
|
||||
|
||||
this._add_settings_keybinding('toggle-xr-effect-shortcut', this._toggle_xr_effect.bind(this));
|
||||
this._add_settings_keybinding('recenter-display-shortcut', this._recenter_display.bind(this));
|
||||
this._add_settings_keybinding('toggle-display-distance-shortcut', this._overlay_content._change_distance.bind(this._overlay_content));
|
||||
this._add_settings_keybinding('toggle-display-distance-shortcut', this._virtual_displays_actor._change_distance.bind(this._virtual_displays_actor));
|
||||
this._add_settings_keybinding('toggle-follow-shortcut', this._toggle_follow_mode.bind(this));
|
||||
} catch (e) {
|
||||
Globals.logger.log(`[ERROR] BreezyDesktopExtension _effect_enable ${e.message}\n${e.stack}`);
|
||||
|
|
@ -557,100 +535,35 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
Main.wm.removeKeybinding('toggle-follow-shortcut');
|
||||
Meta.enable_unredirect_for_display(global.display);
|
||||
|
||||
if (this._actor_added_connection) {
|
||||
global.stage.disconnect(this._actor_added_connection);
|
||||
this._actor_added_connection = null;
|
||||
}
|
||||
if (this._actor_removed_connection) {
|
||||
global.stage.disconnect(this._actor_removed_connection);
|
||||
this._actor_removed_connection = null;
|
||||
}
|
||||
if (this._distance_binding) {
|
||||
this.settings.unbind(this._distance_binding);
|
||||
this._distance_binding = null;
|
||||
}
|
||||
if (this._monitor_spacing_binding) {
|
||||
this.settings.unbind(this._monitor_spacing_binding);
|
||||
this._monitor_spacing_binding = null;
|
||||
}
|
||||
if (this._viewport_offset_x_binding) {
|
||||
this.settings.unbind(this._viewport_offset_x_binding);
|
||||
this._viewport_offset_x_binding = null;
|
||||
}
|
||||
if (this._viewport_offset_y_binding) {
|
||||
this.settings.unbind(this._viewport_offset_y_binding);
|
||||
this._viewport_offset_y_binding = null;
|
||||
}
|
||||
if (this._monitor_wrapping_scheme_binding) {
|
||||
this.settings.unbind(this._monitor_wrapping_scheme_binding);
|
||||
this._monitor_wrapping_scheme_binding = null;
|
||||
}
|
||||
if (this._show_banner_binding) {
|
||||
this._show_banner_binding.unbind();
|
||||
this._show_banner_binding = null;
|
||||
for (let settings_key of this._effect_settings_bindings) {
|
||||
Gio.Settings.unbind(this.settings, settings_key);
|
||||
}
|
||||
this._effect_settings_bindings = [];
|
||||
this._data_stream_bindings.forEach(binding => binding.unbind());
|
||||
this._data_stream_bindings = [];
|
||||
|
||||
if (this._show_banner_connection) {
|
||||
Globals.data_stream.disconnect(this._show_banner_connection);
|
||||
this._show_banner_connection = null;
|
||||
}
|
||||
if (this._custom_banner_enabled_binding) {
|
||||
this._custom_banner_enabled_binding.unbind();
|
||||
this._custom_banner_enabled_binding = null;
|
||||
}
|
||||
if (this._distance_connection) {
|
||||
this.settings.disconnect(this._distance_connection);
|
||||
this._distance_connection = null;
|
||||
}
|
||||
if (this._data_stream_connection) {
|
||||
this._data_stream_connection.unbind();
|
||||
this._data_stream_connection = null;
|
||||
}
|
||||
if (this._follow_threshold_connection) {
|
||||
this.settings.disconnect(this._follow_threshold_connection);
|
||||
this._follow_threshold_connection = null;
|
||||
}
|
||||
if (this._widescreen_mode_settings_connection) {
|
||||
this.settings.disconnect(this._widescreen_mode_settings_connection);
|
||||
this._widescreen_mode_settings_connection = null;
|
||||
}
|
||||
if (this._start_binding) {
|
||||
this.settings.unbind(this._start_binding);
|
||||
this._start_binding = null;
|
||||
}
|
||||
if (this._end_binding) {
|
||||
this.settings.unbind(this._end_binding);
|
||||
this._end_binding = null;
|
||||
}
|
||||
if (this._curved_display_binding) {
|
||||
this.settings.unbind(this._curved_display_binding);
|
||||
this._curved_display_binding = null;
|
||||
}
|
||||
if (this._display_size_binding) {
|
||||
this.settings.unbind(this._display_size_binding);
|
||||
this._display_size_binding = null;
|
||||
}
|
||||
if (this._look_ahead_override_binding) {
|
||||
this.settings.unbind(this._look_ahead_override_binding);
|
||||
this._look_ahead_override_binding = null;
|
||||
}
|
||||
if (this._disable_anti_aliasing_binding) {
|
||||
this.settings.unbind(this._disable_anti_aliasing_binding);
|
||||
this._disable_anti_aliasing_binding = null;
|
||||
}
|
||||
if (this._framerate_cap_binding) {
|
||||
this.settings.unbind(this._framerate_cap_binding);
|
||||
this._framerate_cap_binding = null;
|
||||
}
|
||||
if (this._overlay) {
|
||||
if (this._overlay_content) {
|
||||
this._overlay.remove_child(this._overlay_content);
|
||||
this._overlay_content.destroy();
|
||||
this._overlay_content = null;
|
||||
if (this._virtual_displays_overlay) {
|
||||
if (this._virtual_displays_actor) {
|
||||
this._virtual_displays_overlay.remove_child(this._virtual_displays_actor);
|
||||
this._virtual_displays_actor.destroy();
|
||||
this._virtual_displays_actor = null;
|
||||
}
|
||||
|
||||
global.stage.remove_child(this._overlay);
|
||||
this._overlay.destroy();
|
||||
this._overlay = null;
|
||||
global.stage.remove_child(this._virtual_displays_overlay);
|
||||
this._virtual_displays_overlay.destroy();
|
||||
this._virtual_displays_overlay = null;
|
||||
}
|
||||
if (this._cursor_manager) {
|
||||
this._cursor_manager.disable();
|
||||
|
|
@ -680,21 +593,12 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
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;
|
||||
}
|
||||
Gio.Settings.unbind(this.settings, 'debug');
|
||||
Gio.Settings.unbind(this.settings, 'use-optimal-monitor-config');
|
||||
Gio.Settings.unbind(this.settings, 'headset-as-primary');
|
||||
Gio.Settings.unbind(this.settings, 'debug-no-device');
|
||||
|
||||
if (this._monitor_manager) {
|
||||
if (this._optimal_monitor_config_binding) {
|
||||
this.settings.unbind(this._optimal_monitor_config_binding);
|
||||
this._optimal_monitor_config_binding = null
|
||||
}
|
||||
if (this._headset_as_primary_binding) {
|
||||
this.settings.unbind(this._headset_as_primary_binding);
|
||||
this._headset_as_primary_binding = null;
|
||||
}
|
||||
|
||||
this._monitor_manager.disable();
|
||||
this._monitor_manager = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import Clutter from 'gi://Clutter';
|
||||
import Cogl from 'gi://Cogl';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import Meta from 'gi://Meta';
|
||||
|
||||
const DEFAULT_BACKGROUND_COLOR = Clutter.Color?.from_pixel(0x2e3436ff) || new Cogl.Color({red: 40, green: 40, blue: 40, alpha: 255});
|
||||
|
||||
let _systemBackground;
|
||||
|
||||
export const SystemBackground = GObject.registerClass({
|
||||
Signals: {'loaded': {}},
|
||||
}, class SystemBackground extends Meta.BackgroundActor {
|
||||
_init() {
|
||||
if (_systemBackground == null) {
|
||||
_systemBackground = new Meta.Background({meta_display: global.display});
|
||||
_systemBackground.set_color(DEFAULT_BACKGROUND_COLOR);
|
||||
}
|
||||
|
||||
super._init({
|
||||
meta_display: global.display,
|
||||
monitor: 0,
|
||||
});
|
||||
this.content.background = _systemBackground;
|
||||
|
||||
let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||
this.emit('loaded');
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
GLib.Source.set_name_by_id(id, '[gnome-shell] SystemBackground.loaded');
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
import Clutter from 'gi://Clutter'
|
||||
import Cogl from 'gi://Cogl';
|
||||
import GObject from 'gi://GObject';
|
||||
import Shell from 'gi://Shell';
|
||||
|
||||
import Globals from './globals.js';
|
||||
|
||||
// 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
|
||||
const dataAge = Date.now() - imuDateMs;
|
||||
|
||||
return (override === -1 ? lookAheadCfg[0] : override) + dataAge;
|
||||
}
|
||||
|
||||
export const VirtualDisplayEffect = GObject.registerClass({
|
||||
Properties: {
|
||||
'monitor-index': GObject.ParamSpec.int(
|
||||
'monitor-index',
|
||||
'Monitor Index',
|
||||
'Index of the monitor that this effect is applied to',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0, 100, 0
|
||||
),
|
||||
'monitor-placements': GObject.ParamSpec.jsobject(
|
||||
'monitor-placements',
|
||||
'Monitor Placements',
|
||||
'Target and virtual monitor placement details, as relevant to rendering',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'target-monitor': GObject.ParamSpec.jsobject(
|
||||
'target-monitor',
|
||||
'Target Monitor',
|
||||
'Details about the monitor being used as a viewport',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'imu-snapshots': GObject.ParamSpec.jsobject(
|
||||
'imu-snapshots',
|
||||
'IMU Snapshots',
|
||||
'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'width': GObject.ParamSpec.int(
|
||||
'width',
|
||||
'Width',
|
||||
'Width of the viewport',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
1, 10000, 1920
|
||||
),
|
||||
'height': GObject.ParamSpec.int(
|
||||
'height',
|
||||
'Height',
|
||||
'Height of the viewport',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
1, 10000, 1080
|
||||
),
|
||||
'focused-monitor-index': GObject.ParamSpec.int(
|
||||
'focused-monitor-index',
|
||||
'Focused Monitor Index',
|
||||
'Index of the monitor that is currently focused',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
-1, 100, -1
|
||||
),
|
||||
'display-zoom-on-focus': GObject.ParamSpec.boolean(
|
||||
'display-zoom-on-focus',
|
||||
'Display zoom on focus',
|
||||
'Automatically move a display closer when it becomes focused.',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
true
|
||||
),
|
||||
'display-distance': GObject.ParamSpec.double(
|
||||
'display-distance',
|
||||
'Display Distance',
|
||||
'Distance of the display from the camera',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0.0,
|
||||
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',
|
||||
'Distance to use when not explicitly set, or when reset',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0.2,
|
||||
2.5,
|
||||
1.0
|
||||
),
|
||||
'show-banner': GObject.ParamSpec.boolean(
|
||||
'show-banner',
|
||||
'Show banner',
|
||||
'Whether the banner should be displayed',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
false
|
||||
),
|
||||
'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',
|
||||
'Ratios to convert actor coordinates to display coordinates',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'actor-to-display-offsets': GObject.ParamSpec.jsobject(
|
||||
'actor-to-display-offsets',
|
||||
'Actor to Display Offsets',
|
||||
'Offsets to convert actor coordinates to display coordinates',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'is-closest': GObject.ParamSpec.boolean(
|
||||
'is-closest',
|
||||
'Is Closest',
|
||||
'Whether this monitor is the closest to the camera',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
false
|
||||
),
|
||||
'disable-anti-aliasing': GObject.ParamSpec.boolean(
|
||||
'disable-anti-aliasing',
|
||||
'Disable anti-aliasing',
|
||||
'Disable anti-aliasing for the effect',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
false
|
||||
),
|
||||
'look-ahead-override': GObject.ParamSpec.int(
|
||||
'look-ahead-override',
|
||||
'Look ahead override',
|
||||
'Override the look ahead value',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
-1,
|
||||
45,
|
||||
-1
|
||||
),
|
||||
}
|
||||
}, class VirtualDisplayEffect extends Shell.GLSLEffect {
|
||||
constructor(params = {}) {
|
||||
super(params);
|
||||
|
||||
this._current_display_distance = this._is_focused() ? this.display_distance : this.display_distance_default;
|
||||
this.no_distance_ease = 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));
|
||||
}
|
||||
|
||||
_is_focused() {
|
||||
return this.focused_monitor_index === this.monitor_index;
|
||||
}
|
||||
|
||||
_update_display_distance() {
|
||||
const desired_distance = this._is_focused() ? this.display_distance : this.display_distance_default;
|
||||
if (this._distance_ease_timeline?.is_playing()) {
|
||||
// we're already easing towards the desired distance, do nothing
|
||||
if (this._distance_ease_target === desired_distance) return;
|
||||
|
||||
this._distance_ease_timeline.stop();
|
||||
}
|
||||
|
||||
if (this.no_distance_ease) {
|
||||
this._current_display_distance = desired_distance;
|
||||
this._update_display_position_uniforms();
|
||||
this.no_distance_ease = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// if we're the focused display, we'll double the timeline and wait for the first half to let other
|
||||
// displays ease out first
|
||||
this._distance_ease_focus = this._is_focused();
|
||||
const ease_out_timeline_ms = 150;
|
||||
const pause_ms = 50;
|
||||
const ease_in_timeline_ms = 500; // includes ease out and pause
|
||||
const ease_in_begin_pct = (ease_out_timeline_ms + pause_ms) / ease_in_timeline_ms;
|
||||
const timeline_ms = this._distance_ease_focus ?
|
||||
ease_in_timeline_ms :
|
||||
ease_out_timeline_ms;
|
||||
|
||||
this._distance_ease_start = this._current_display_distance;
|
||||
this._distance_ease_timeline = Clutter.Timeline.new_for_actor(this.get_actor(), timeline_ms);
|
||||
|
||||
this._distance_ease_target = desired_distance;
|
||||
this._distance_ease_timeline.connect('new-frame', (() => {
|
||||
let progress = this._distance_ease_timeline.get_progress();
|
||||
if (this._distance_ease_focus) {
|
||||
// if we're the focused display, wait for the first half of the timeline to pass
|
||||
if (progress < ease_in_begin_pct) return;
|
||||
|
||||
// treat the second half of the timeline as its own full progression
|
||||
progress = (progress - ease_in_begin_pct) / (1 - ease_in_begin_pct);
|
||||
|
||||
// put this display in front as it starts to easy in
|
||||
this.is_closest = true;
|
||||
} else {
|
||||
this.is_closest = false;
|
||||
}
|
||||
|
||||
this._current_display_distance = this._distance_ease_start +
|
||||
(1 - Math.cos(progress * Math.PI)) / 2 * (this._distance_ease_target - this._distance_ease_start);
|
||||
this._update_display_position_uniforms();
|
||||
}).bind(this));
|
||||
|
||||
this._distance_ease_timeline.start();
|
||||
}
|
||||
|
||||
_update_display_position_uniforms() {
|
||||
// this is in NWU coordinates
|
||||
const monitorPlacement = this.monitor_placements[this.monitor_index];
|
||||
// Globals.logger.log_debug(`\t\t\tMonitor ${this.monitor_index} vectors: ${JSON.stringify(monitorPlacement)}`);
|
||||
|
||||
// use the center vector with the distance applied to determine how much to move each coordinate, so they all move uniformly
|
||||
const inverseAppliedDistance = 1.0 - this._current_display_distance / this.display_distance_default;
|
||||
const distanceDelta = monitorPlacement.centerNoRotate.map(coord => coord * inverseAppliedDistance);
|
||||
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 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]);
|
||||
}
|
||||
|
||||
_handle_banner_update() {
|
||||
this.set_uniform_float(this.get_uniform_location("u_show_banner"), 1, [this.show_banner ? 1.0 : 0.0]);
|
||||
}
|
||||
|
||||
perspective(fovVerticalRadians, aspect, near, far) {
|
||||
const fovHorizontalRadians = fovVerticalRadians * aspect;
|
||||
const f = 1.0 / Math.tan(fovHorizontalRadians / 2.0);
|
||||
const range = far - near;
|
||||
|
||||
return [
|
||||
f / aspect, 0, 0, 0,
|
||||
0, f, 0, 0,
|
||||
0, 0, - (far + near) / range, -1,
|
||||
0, 0, - (2.0 * near * far) / range, 0
|
||||
];
|
||||
}
|
||||
|
||||
vfunc_build_pipeline() {
|
||||
const declarations = `
|
||||
uniform bool u_show_banner;
|
||||
uniform mat4 u_imu_data;
|
||||
uniform float u_look_ahead_ms;
|
||||
uniform vec4 u_look_ahead_cfg;
|
||||
uniform mat4 u_projection_matrix;
|
||||
uniform float u_fov_vertical_radians;
|
||||
uniform vec3 u_display_position;
|
||||
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;
|
||||
uniform vec2 u_actor_to_display_offsets;
|
||||
|
||||
// discovered through trial and error, no idea the significance
|
||||
float cogl_position_mystery_factor = 29.09 * 2;
|
||||
|
||||
float look_ahead_ms_cap = 45.0;
|
||||
|
||||
vec4 quatConjugate(vec4 q) {
|
||||
return vec4(-q.xyz, q.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) {
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
return vec4(v.x, v.y * c - v.z * s, v.y * s + v.z * c, v.w);
|
||||
}
|
||||
|
||||
vec4 applyYRotationToVector(vec4 v, float angle) {
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
return vec4(v.x * c + v.z * s, v.y, v.z * c - v.x * s, v.w);
|
||||
}
|
||||
|
||||
vec4 nwuToESU(vec4 v) {
|
||||
return vec4(-v.y, v.z, -v.x, v.w);
|
||||
}
|
||||
|
||||
// returns the rate of change between the two vectors, in same time units as delta_time
|
||||
// e.g. if delta_time is in ms, then the rate of change is "per ms"
|
||||
vec3 rateOfChange(vec3 v1, vec3 v2, float delta_time) {
|
||||
return (v1-v2) / delta_time;
|
||||
}
|
||||
|
||||
// attempt to figure out where the current position should be based on previous position and velocity.
|
||||
// velocity and time values should use the same time units (secs, ms, etc...)
|
||||
vec3 applyLookAhead(vec3 position, vec3 velocity, float look_ahead_ms) {
|
||||
return position + velocity * look_ahead_ms;
|
||||
}
|
||||
|
||||
// project the vector onto a flat surface, return it's vertical position relative to the vertical fov, where 0.0 is
|
||||
// the top and 1.0 is the bottom. vectors that project outside the vertical range of the display will have values
|
||||
// outside this range, but capped
|
||||
float vectorToScanline(float fovVerticalRadians, vec3 v) {
|
||||
return clamp(1.0 - (-v.y / (tan(fovVerticalRadians / 2.0) * v.z) + 1.0) / 2.0, -1.5, 2.5);
|
||||
}
|
||||
`;
|
||||
|
||||
const main = `
|
||||
vec4 world_pos = cogl_position_in;
|
||||
|
||||
if (!u_show_banner) {
|
||||
float aspect_ratio = u_display_resolution.x / u_display_resolution.y;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
world_pos.y -= u_actor_to_display_offsets.y * cogl_position_height / 2;
|
||||
|
||||
world_pos.z *= aspect_ratio / u_actor_to_display_ratios.y;
|
||||
world_pos = applyXRotationToVector(world_pos, u_rotation_x_radians);
|
||||
world_pos = applyYRotationToVector(world_pos, u_rotation_y_radians);
|
||||
|
||||
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);
|
||||
|
||||
// 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 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);
|
||||
|
||||
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;
|
||||
|
||||
world_pos = u_projection_matrix * world_pos;
|
||||
|
||||
// if the perspective includes more than just our viewport actor, move the vertices back to just the area we can see.
|
||||
// this needs to be done after the projection matrix multiplication so it will be projected as if centered in our vision
|
||||
world_pos.x -= (u_actor_to_display_offsets.x / u_actor_to_display_ratios.x) * world_pos.w;
|
||||
world_pos.y += (u_actor_to_display_offsets.y / u_actor_to_display_ratios.y) * world_pos.w;
|
||||
} else {
|
||||
world_pos = cogl_modelview_matrix * world_pos;
|
||||
world_pos = cogl_projection_matrix * world_pos;
|
||||
}
|
||||
|
||||
cogl_position_out = world_pos;
|
||||
cogl_tex_coord_out[0] = cogl_tex_coord_in;
|
||||
`
|
||||
|
||||
this.add_glsl_snippet(Shell.SnippetHook.VERTEX, declarations, main, false);
|
||||
}
|
||||
|
||||
vfunc_paint_target(node, paintContext) {
|
||||
if (!this._initialized) {
|
||||
const aspect = this.get_actor().width / this.get_actor().height;
|
||||
const fovDiagonalRadians = Globals.data_stream.device_data.displayFov * Math.PI / 180.0;
|
||||
const diagToVertRatio = Math.sqrt(aspect * aspect + 1);
|
||||
const fovVerticalRadians = fovDiagonalRadians / diagToVertRatio;
|
||||
const projection_matrix = this.perspective(
|
||||
fovVerticalRadians,
|
||||
aspect,
|
||||
0.0001,
|
||||
1000.0
|
||||
);
|
||||
this.set_uniform_matrix(this.get_uniform_location("u_projection_matrix"), false, 4, projection_matrix);
|
||||
this.set_uniform_float(this.get_uniform_location("u_fov_vertical_radians"), 1, [fovVerticalRadians]);
|
||||
this.set_uniform_float(this.get_uniform_location("u_display_resolution"), 2, [this.target_monitor.width, this.target_monitor.height]);
|
||||
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._handle_banner_update();
|
||||
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);
|
||||
|
||||
if (!this.disable_anti_aliasing) {
|
||||
// improves sampling quality for smooth text and edges
|
||||
this.get_pipeline().set_layer_filters(
|
||||
0,
|
||||
Cogl.PipelineFilter.LINEAR_MIPMAP_LINEAR,
|
||||
Cogl.PipelineFilter.LINEAR
|
||||
);
|
||||
}
|
||||
|
||||
super.vfunc_paint_target(node, paintContext);
|
||||
}
|
||||
});
|
||||
|
|
@ -6,6 +6,9 @@ import GObject from 'gi://GObject';
|
|||
import Mtk from 'gi://Mtk';
|
||||
import Shell from 'gi://Shell';
|
||||
import St from 'gi://St';
|
||||
|
||||
import { VirtualDisplayEffect } from './virtualdisplayeffect.js';
|
||||
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
import Globals from './globals.js';
|
||||
|
|
@ -323,443 +326,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch
|
|||
return monitorPlacements;
|
||||
}
|
||||
|
||||
function monitorVectorToRotationAngle(vector, monitorWrappingScheme) {
|
||||
if (monitorWrappingScheme === 'horizontal') {
|
||||
// monitors wrap around us horizontally
|
||||
return {
|
||||
angle: Math.atan2(vector[1], vector[0]),
|
||||
axis: Clutter.RotateAxis.Y_AXIS
|
||||
};
|
||||
} else if (monitorWrappingScheme === 'vertical') {
|
||||
// monitors wrap around us vertically
|
||||
return {
|
||||
angle: Math.atan2(vector[2], vector[0]),
|
||||
axis: Clutter.RotateAxis.X_AXIS
|
||||
}
|
||||
} else {
|
||||
// no rotation
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
const dataAge = Date.now() - imuDateMs;
|
||||
|
||||
return (override === -1 ? lookAheadCfg[0] : override) + dataAge;
|
||||
}
|
||||
|
||||
export const VirtualMonitorEffect = GObject.registerClass({
|
||||
Properties: {
|
||||
'monitor-index': GObject.ParamSpec.int(
|
||||
'monitor-index',
|
||||
'Monitor Index',
|
||||
'Index of the monitor that this effect is applied to',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0, 100, 0
|
||||
),
|
||||
'monitor-placements': GObject.ParamSpec.jsobject(
|
||||
'monitor-placements',
|
||||
'Monitor Placements',
|
||||
'Target and virtual monitor placement details, as relevant to rendering',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'target-monitor': GObject.ParamSpec.jsobject(
|
||||
'target-monitor',
|
||||
'Target Monitor',
|
||||
'Details about the monitor being used as a viewport',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'imu-snapshots': GObject.ParamSpec.jsobject(
|
||||
'imu-snapshots',
|
||||
'IMU Snapshots',
|
||||
'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'width': GObject.ParamSpec.int(
|
||||
'width',
|
||||
'Width',
|
||||
'Width of the viewport',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
1, 10000, 1920
|
||||
),
|
||||
'height': GObject.ParamSpec.int(
|
||||
'height',
|
||||
'Height',
|
||||
'Height of the viewport',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
1, 10000, 1080
|
||||
),
|
||||
'focused-monitor-index': GObject.ParamSpec.int(
|
||||
'focused-monitor-index',
|
||||
'Focused Monitor Index',
|
||||
'Index of the monitor that is currently focused',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
-1, 100, -1
|
||||
),
|
||||
'display-zoom-on-focus': GObject.ParamSpec.boolean(
|
||||
'display-zoom-on-focus',
|
||||
'Display zoom on focus',
|
||||
'Automatically move a display closer when it becomes focused.',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
true
|
||||
),
|
||||
'display-distance': GObject.ParamSpec.double(
|
||||
'display-distance',
|
||||
'Display Distance',
|
||||
'Distance of the display from the camera',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0.0,
|
||||
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',
|
||||
'Distance to use when not explicitly set, or when reset',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0.2,
|
||||
2.5,
|
||||
1.0
|
||||
),
|
||||
'show-banner': GObject.ParamSpec.boolean(
|
||||
'show-banner',
|
||||
'Show banner',
|
||||
'Whether the banner should be displayed',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
false
|
||||
),
|
||||
'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',
|
||||
'Ratios to convert actor coordinates to display coordinates',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'actor-to-display-offsets': GObject.ParamSpec.jsobject(
|
||||
'actor-to-display-offsets',
|
||||
'Actor to Display Offsets',
|
||||
'Offsets to convert actor coordinates to display coordinates',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'is-closest': GObject.ParamSpec.boolean(
|
||||
'is-closest',
|
||||
'Is Closest',
|
||||
'Whether this monitor is the closest to the camera',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
false
|
||||
),
|
||||
'disable-anti-aliasing': GObject.ParamSpec.boolean(
|
||||
'disable-anti-aliasing',
|
||||
'Disable anti-aliasing',
|
||||
'Disable anti-aliasing for the effect',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
false
|
||||
),
|
||||
'look-ahead-override': GObject.ParamSpec.int(
|
||||
'look-ahead-override',
|
||||
'Look ahead override',
|
||||
'Override the look ahead value',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
-1,
|
||||
45,
|
||||
-1
|
||||
),
|
||||
}
|
||||
}, class VirtualMonitorEffect extends Shell.GLSLEffect {
|
||||
constructor(params = {}) {
|
||||
super(params);
|
||||
|
||||
this._current_display_distance = this._is_focused() ? this.display_distance : this.display_distance_default;
|
||||
this.no_distance_ease = 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));
|
||||
}
|
||||
|
||||
_is_focused() {
|
||||
return this.focused_monitor_index === this.monitor_index;
|
||||
}
|
||||
|
||||
_update_display_distance() {
|
||||
const desired_distance = this._is_focused() ? this.display_distance : this.display_distance_default;
|
||||
if (this._distance_ease_timeline?.is_playing()) {
|
||||
// we're already easing towards the desired distance, do nothing
|
||||
if (this._distance_ease_target === desired_distance) return;
|
||||
|
||||
this._distance_ease_timeline.stop();
|
||||
}
|
||||
|
||||
if (this.no_distance_ease) {
|
||||
this._current_display_distance = desired_distance;
|
||||
this._update_display_position_uniforms();
|
||||
this.no_distance_ease = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// if we're the focused display, we'll double the timeline and wait for the first half to let other
|
||||
// displays ease out first
|
||||
this._distance_ease_focus = this._is_focused();
|
||||
const ease_out_timeline_ms = 150;
|
||||
const pause_ms = 50;
|
||||
const ease_in_timeline_ms = 500; // includes ease out and pause
|
||||
const ease_in_begin_pct = (ease_out_timeline_ms + pause_ms) / ease_in_timeline_ms;
|
||||
const timeline_ms = this._distance_ease_focus ?
|
||||
ease_in_timeline_ms :
|
||||
ease_out_timeline_ms;
|
||||
|
||||
this._distance_ease_start = this._current_display_distance;
|
||||
this._distance_ease_timeline = Clutter.Timeline.new_for_actor(this.get_actor(), timeline_ms);
|
||||
|
||||
this._distance_ease_target = desired_distance;
|
||||
this._distance_ease_timeline.connect('new-frame', (() => {
|
||||
let progress = this._distance_ease_timeline.get_progress();
|
||||
if (this._distance_ease_focus) {
|
||||
// if we're the focused display, wait for the first half of the timeline to pass
|
||||
if (progress < ease_in_begin_pct) return;
|
||||
|
||||
// treat the second half of the timeline as its own full progression
|
||||
progress = (progress - ease_in_begin_pct) / (1 - ease_in_begin_pct);
|
||||
|
||||
// put this display in front as it starts to easy in
|
||||
this.is_closest = true;
|
||||
} else {
|
||||
this.is_closest = false;
|
||||
}
|
||||
|
||||
this._current_display_distance = this._distance_ease_start +
|
||||
(1 - Math.cos(progress * Math.PI)) / 2 * (this._distance_ease_target - this._distance_ease_start);
|
||||
this._update_display_position_uniforms();
|
||||
}).bind(this));
|
||||
|
||||
this._distance_ease_timeline.start();
|
||||
}
|
||||
|
||||
_update_display_position_uniforms() {
|
||||
// this is in NWU coordinates
|
||||
const monitorPlacement = this.monitor_placements[this.monitor_index];
|
||||
// Globals.logger.log_debug(`\t\t\tMonitor ${this.monitor_index} vectors: ${JSON.stringify(monitorPlacement)}`);
|
||||
|
||||
// use the center vector with the distance applied to determine how much to move each coordinate, so they all move uniformly
|
||||
const inverseAppliedDistance = 1.0 - this._current_display_distance / this.display_distance_default;
|
||||
const distanceDelta = monitorPlacement.centerNoRotate.map(coord => coord * inverseAppliedDistance);
|
||||
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 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]);
|
||||
}
|
||||
|
||||
_handle_banner_update() {
|
||||
this.set_uniform_float(this.get_uniform_location("u_show_banner"), 1, [this.show_banner ? 1.0 : 0.0]);
|
||||
}
|
||||
|
||||
perspective(fovVerticalRadians, aspect, near, far) {
|
||||
const fovHorizontalRadians = fovVerticalRadians * aspect;
|
||||
const f = 1.0 / Math.tan(fovHorizontalRadians / 2.0);
|
||||
const range = far - near;
|
||||
|
||||
return [
|
||||
f / aspect, 0, 0, 0,
|
||||
0, f, 0, 0,
|
||||
0, 0, - (far + near) / range, -1,
|
||||
0, 0, - (2.0 * near * far) / range, 0
|
||||
];
|
||||
}
|
||||
|
||||
vfunc_build_pipeline() {
|
||||
const declarations = `
|
||||
uniform bool u_show_banner;
|
||||
uniform mat4 u_imu_data;
|
||||
uniform float u_look_ahead_ms;
|
||||
uniform vec4 u_look_ahead_cfg;
|
||||
uniform mat4 u_projection_matrix;
|
||||
uniform float u_fov_vertical_radians;
|
||||
uniform vec3 u_display_position;
|
||||
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;
|
||||
uniform vec2 u_actor_to_display_offsets;
|
||||
|
||||
// discovered through trial and error, no idea the significance
|
||||
float cogl_position_mystery_factor = 29.09 * 2;
|
||||
|
||||
float look_ahead_ms_cap = 45.0;
|
||||
|
||||
vec4 quatConjugate(vec4 q) {
|
||||
return vec4(-q.xyz, q.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) {
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
return vec4(v.x, v.y * c - v.z * s, v.y * s + v.z * c, v.w);
|
||||
}
|
||||
|
||||
vec4 applyYRotationToVector(vec4 v, float angle) {
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
return vec4(v.x * c + v.z * s, v.y, v.z * c - v.x * s, v.w);
|
||||
}
|
||||
|
||||
vec4 nwuToESU(vec4 v) {
|
||||
return vec4(-v.y, v.z, -v.x, v.w);
|
||||
}
|
||||
|
||||
// returns the rate of change between the two vectors, in same time units as delta_time
|
||||
// e.g. if delta_time is in ms, then the rate of change is "per ms"
|
||||
vec3 rateOfChange(vec3 v1, vec3 v2, float delta_time) {
|
||||
return (v1-v2) / delta_time;
|
||||
}
|
||||
|
||||
// attempt to figure out where the current position should be based on previous position and velocity.
|
||||
// velocity and time values should use the same time units (secs, ms, etc...)
|
||||
vec3 applyLookAhead(vec3 position, vec3 velocity, float look_ahead_ms) {
|
||||
return position + velocity * look_ahead_ms;
|
||||
}
|
||||
|
||||
// project the vector onto a flat surface, return it's vertical position relative to the vertical fov, where 0.0 is
|
||||
// the top and 1.0 is the bottom. vectors that project outside the vertical range of the display will have values
|
||||
// outside this range, but capped
|
||||
float vectorToScanline(float fovVerticalRadians, vec3 v) {
|
||||
return clamp(1.0 - (-v.y / (tan(fovVerticalRadians / 2.0) * v.z) + 1.0) / 2.0, -1.5, 2.5);
|
||||
}
|
||||
`;
|
||||
|
||||
const main = `
|
||||
vec4 world_pos = cogl_position_in;
|
||||
|
||||
if (!u_show_banner) {
|
||||
float aspect_ratio = u_display_resolution.x / u_display_resolution.y;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
world_pos.y -= u_actor_to_display_offsets.y * cogl_position_height / 2;
|
||||
|
||||
world_pos.z *= aspect_ratio / u_actor_to_display_ratios.y;
|
||||
world_pos = applyXRotationToVector(world_pos, u_rotation_x_radians);
|
||||
world_pos = applyYRotationToVector(world_pos, u_rotation_y_radians);
|
||||
|
||||
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);
|
||||
|
||||
// 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 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);
|
||||
|
||||
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;
|
||||
|
||||
world_pos = u_projection_matrix * world_pos;
|
||||
|
||||
// if the perspective includes more than just our viewport actor, move the vertices back to just the area we can see.
|
||||
// this needs to be done after the projection matrix multiplication so it will be projected as if centered in our vision
|
||||
world_pos.x -= (u_actor_to_display_offsets.x / u_actor_to_display_ratios.x) * world_pos.w;
|
||||
world_pos.y += (u_actor_to_display_offsets.y / u_actor_to_display_ratios.y) * world_pos.w;
|
||||
} else {
|
||||
world_pos = cogl_modelview_matrix * world_pos;
|
||||
world_pos = cogl_projection_matrix * world_pos;
|
||||
}
|
||||
|
||||
cogl_position_out = world_pos;
|
||||
cogl_tex_coord_out[0] = cogl_tex_coord_in;
|
||||
`
|
||||
|
||||
this.add_glsl_snippet(Shell.SnippetHook.VERTEX, declarations, main, false);
|
||||
}
|
||||
|
||||
vfunc_paint_target(node, paintContext) {
|
||||
if (!this._initialized) {
|
||||
const aspect = this.get_actor().width / this.get_actor().height;
|
||||
const fovDiagonalRadians = Globals.data_stream.device_data.displayFov * Math.PI / 180.0;
|
||||
const diagToVertRatio = Math.sqrt(aspect * aspect + 1);
|
||||
const fovVerticalRadians = fovDiagonalRadians / diagToVertRatio;
|
||||
const projection_matrix = this.perspective(
|
||||
fovVerticalRadians,
|
||||
aspect,
|
||||
0.0001,
|
||||
1000.0
|
||||
);
|
||||
this.set_uniform_matrix(this.get_uniform_location("u_projection_matrix"), false, 4, projection_matrix);
|
||||
this.set_uniform_float(this.get_uniform_location("u_fov_vertical_radians"), 1, [fovVerticalRadians]);
|
||||
this.set_uniform_float(this.get_uniform_location("u_display_resolution"), 2, [this.target_monitor.width, this.target_monitor.height]);
|
||||
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._handle_banner_update();
|
||||
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);
|
||||
|
||||
if (!this.disable_anti_aliasing) {
|
||||
// improves sampling quality for smooth text and edges
|
||||
this.get_pipeline().set_layer_filters(
|
||||
0,
|
||||
Cogl.PipelineFilter.LINEAR_MIPMAP_LINEAR,
|
||||
Cogl.PipelineFilter.LINEAR
|
||||
);
|
||||
}
|
||||
|
||||
super.vfunc_paint_target(node, paintContext);
|
||||
}
|
||||
});
|
||||
|
||||
export const VirtualMonitorsActor = GObject.registerClass({
|
||||
export const VirtualDisplaysActor = GObject.registerClass({
|
||||
Properties: {
|
||||
'target-monitor': GObject.ParamSpec.jsobject(
|
||||
'target-monitor',
|
||||
|
|
@ -913,7 +480,7 @@ export const VirtualMonitorsActor = GObject.registerClass({
|
|||
false
|
||||
)
|
||||
}
|
||||
}, class VirtualMonitorsActor extends Clutter.Actor {
|
||||
}, class VirtualDisplaysActor extends Clutter.Actor {
|
||||
constructor(params = {}) {
|
||||
super(params);
|
||||
|
||||
|
|
@ -1017,7 +584,7 @@ export const VirtualMonitorsActor = GObject.registerClass({
|
|||
|
||||
// Add the monitor actor to the scene
|
||||
containerActor.add_child(monitorClone);
|
||||
const effect = new VirtualMonitorEffect({
|
||||
const effect = new VirtualDisplayEffect({
|
||||
focused_monitor_index: this.focused_monitor_index,
|
||||
imu_snapshots: this.imu_snapshots,
|
||||
monitor_index: index,
|
||||
|
|
@ -1,473 +0,0 @@
|
|||
import Clutter from 'gi://Clutter';
|
||||
import Cogl from 'gi://Cogl';
|
||||
import GdkPixbuf from 'gi://GdkPixbuf';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import Shell from 'gi://Shell';
|
||||
|
||||
import Globals from './globals.js';
|
||||
|
||||
import {
|
||||
dataViewEnd,
|
||||
dataViewUint8,
|
||||
dataViewBigUint,
|
||||
dataViewUint32Array,
|
||||
dataViewUint8Array,
|
||||
dataViewFloat,
|
||||
dataViewFloatArray,
|
||||
BOOL_SIZE,
|
||||
DATA_VIEW_INFO_COUNT_INDEX,
|
||||
DATA_VIEW_INFO_OFFSET_INDEX,
|
||||
FLOAT_SIZE,
|
||||
UINT_SIZE,
|
||||
UINT8_SIZE
|
||||
} from "./ipc.js";
|
||||
import { degreeToRadian } from "./math.js";
|
||||
import { getShaderSource } from "./shader.js";
|
||||
import { isValidKeepAlive, toSec } from "./time.js";
|
||||
|
||||
export const IPC_FILE_PATH = "/dev/shm/breezy_desktop_imu";
|
||||
|
||||
// the driver should be using the same data layout version
|
||||
const DATA_LAYOUT_VERSION = 3;
|
||||
|
||||
// DataView info: [offset, size, count]
|
||||
const VERSION = [0, UINT8_SIZE, 1];
|
||||
const ENABLED = [dataViewEnd(VERSION), BOOL_SIZE, 1];
|
||||
const LOOK_AHEAD_CFG = [dataViewEnd(ENABLED), FLOAT_SIZE, 4];
|
||||
const DISPLAY_RES = [dataViewEnd(LOOK_AHEAD_CFG), UINT_SIZE, 2];
|
||||
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 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);
|
||||
|
||||
// cached after first retrieval
|
||||
const shaderUniformLocations = {
|
||||
'virtual_display_enabled': null,
|
||||
'show_banner': null,
|
||||
'imu_quat_data': null,
|
||||
'look_ahead_cfg': null,
|
||||
'look_ahead_ms': null,
|
||||
'trim_percent': null,
|
||||
'display_size': null,
|
||||
'display_north_offset': null,
|
||||
'lens_vector': null,
|
||||
'lens_vector_r': null, // only used if sbs_enabled is true
|
||||
'texcoord_x_limits': null, // index 0: min; index 1: max
|
||||
'texcoord_x_limits_r': null, // only used if sbs_enabled is true
|
||||
'sbs_enabled': null,
|
||||
'custom_banner_enabled': null,
|
||||
'half_fov_z_rads': null,
|
||||
'half_fov_y_rads': null,
|
||||
'fov_half_widths': null,
|
||||
'fov_widths': null,
|
||||
'display_resolution': null,
|
||||
'source_to_display_ratio': null,
|
||||
'texcoord_visible_area': null,
|
||||
'curved_display': null,
|
||||
|
||||
// only used by the reshade integration, but needs to be set to a default value by this effect
|
||||
'frametime': null,
|
||||
'sideview_enabled': null,
|
||||
'sideview_position': null,
|
||||
'sideview_display_size': null
|
||||
};
|
||||
|
||||
function setUniformFloat(effect, locationName, dataViewInfo, value) {
|
||||
effect.set_uniform_float(shaderUniformLocations[locationName], dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX], value);
|
||||
}
|
||||
|
||||
function transferUniformFloat(effect, locationName, dataView, dataViewInfo) {
|
||||
setUniformFloat(effect, locationName, dataViewInfo, dataViewFloatArray(dataView, dataViewInfo));
|
||||
}
|
||||
|
||||
function setSingleFloat(effect, locationName, value) {
|
||||
effect.set_uniform_float(shaderUniformLocations[locationName], 1, [value]);
|
||||
}
|
||||
|
||||
function setUniformMatrix(effect, locationName, components, dataView, dataViewInfo) {
|
||||
const numValues = dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX];
|
||||
if (numValues / components !== components) {
|
||||
throw new Error('Invalid matrix size');
|
||||
}
|
||||
|
||||
const floatArray = [].fill(0, 0, numValues);
|
||||
let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX];
|
||||
for (let i = 0; i < numValues; i++) {
|
||||
// GLSL uses column-major order, so we need to transpose the matrix
|
||||
const row = i % components;
|
||||
const column = Math.floor(i / components);
|
||||
|
||||
floatArray[row * components + column] = dataView.getFloat32(offset, true);
|
||||
offset += FLOAT_SIZE;
|
||||
}
|
||||
effect.set_uniform_matrix(shaderUniformLocations[locationName], true, components, floatArray);
|
||||
}
|
||||
|
||||
function lookAheadMS(dataView) {
|
||||
const lookAheadCfg = dataViewFloatArray(dataView, LOOK_AHEAD_CFG);
|
||||
const imuDateMS = dataViewBigUint(dataView, EPOCH_MS);
|
||||
|
||||
// how stale the imu data is
|
||||
const dataAge = Date.now() - imuDateMS;
|
||||
|
||||
return lookAheadCfg[0] + dataAge;
|
||||
}
|
||||
|
||||
// most uniforms don't change frequently, this function should be called periodically
|
||||
function setIntermittentUniformVariables() {
|
||||
try {
|
||||
const dataView = this._dataView;
|
||||
|
||||
if (dataView.byteLength === DATA_VIEW_LENGTH) {
|
||||
const version = dataViewUint8(dataView, VERSION);
|
||||
const imuDateMs = dataViewBigUint(dataView, EPOCH_MS);
|
||||
const validKeepalive = isValidKeepAlive(toSec(imuDateMs));
|
||||
const imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA);
|
||||
const imuResetState = validKeepalive && imuData[0] === 0.0 && imuData[1] === 0.0 && imuData[2] === 0.0 && imuData[3] === 1.0;
|
||||
const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive;
|
||||
const displayRes = dataViewUint32Array(dataView, DISPLAY_RES);
|
||||
const sbsEnabled = dataViewUint8(dataView, SBS_ENABLED) !== 0;
|
||||
|
||||
const texture_actor = this.get_actor();
|
||||
if (enabled) {
|
||||
const displayFov = dataViewFloat(dataView, DISPLAY_FOV);
|
||||
|
||||
// TODO - drive these values from settings
|
||||
const sbsContent = false;
|
||||
const sbsModeStretched = true;
|
||||
|
||||
// compute these values once, they only change when the XR device changes
|
||||
const displayAspectRatio = displayRes[0] / displayRes[1];
|
||||
const diagToVertRatio = Math.sqrt(Math.pow(displayAspectRatio, 2) + 1);
|
||||
const halfFovZRads = degreeToRadian(displayFov / diagToVertRatio) / 2;
|
||||
const halfFovYRads = halfFovZRads * displayAspectRatio;
|
||||
const fovHalfWidths = [Math.tan(halfFovYRads), Math.tan(halfFovZRads)];
|
||||
const fovWidths = [fovHalfWidths[0] * 2, fovHalfWidths[1] * 2];
|
||||
const lensDistanceRatio = dataViewFloat(dataView, LENS_DISTANCE_RATIO);
|
||||
let lensFromCenter = 0.0;
|
||||
let texcoordXLimits = [0.0, 1.0];
|
||||
let texcoordXLimitsRight = [0.0, 1.0];
|
||||
if (sbsEnabled) {
|
||||
lensFromCenter = lensDistanceRatio / 3.0;
|
||||
if (sbsContent) {
|
||||
texcoordXLimits[1] = 0.5;
|
||||
texcoordXLimitsRight[0] = 0.5;
|
||||
if (!sbsModeStretched) {
|
||||
texcoordXLimits[0] = 0.25;
|
||||
texcoordXLimitsRight[1] = 0.75;
|
||||
}
|
||||
} else if (!sbsModeStretched) {
|
||||
texcoordXLimits[0] = 0.25;
|
||||
texcoordXLimits[1] = 0.75;
|
||||
texcoordXLimitsRight[0] = 0.25;
|
||||
texcoordXLimitsRight[1] = 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
Globals.logger.log(`texture_actor: ${texture_actor.x}, ${texture_actor.y}, ${texture_actor.width}, ${texture_actor.height}`);
|
||||
|
||||
const texcoordVisibleArea = [
|
||||
this.texture_monitor_position.x / texture_actor.width,
|
||||
this.texture_monitor_position.y / texture_actor.height,
|
||||
(this.texture_monitor_position.x + this.target_monitor.width) / texture_actor.width,
|
||||
(this.texture_monitor_position.y + this.target_monitor.height) / texture_actor.height
|
||||
]
|
||||
|
||||
const lensVector = [lensDistanceRatio, lensFromCenter, 0.0];
|
||||
const lensVectorRight = [lensDistanceRatio, -lensFromCenter, 0.0];
|
||||
|
||||
// our overlay doesn't quite cover the full screen texture, which allows us to see some of the real desktop
|
||||
// underneath, so we trim three pixels around the entire edge of the texture
|
||||
const trimWidthPercent = 3.0 / texture_actor.width;
|
||||
const trimHeightPercent = 3.0 / texture_actor.height;
|
||||
|
||||
// all these values are transferred directly, unmodified from the driver
|
||||
transferUniformFloat(this, 'look_ahead_cfg', dataView, LOOK_AHEAD_CFG);
|
||||
transferUniformFloat(this, 'lens_distance_ratio', dataView, LENS_DISTANCE_RATIO);
|
||||
|
||||
// computed values with no dataViewInfo, so we set these manually
|
||||
this.set_uniform_float(shaderUniformLocations['trim_percent'], 2, [trimWidthPercent, trimHeightPercent]);
|
||||
setSingleFloat(this, 'half_fov_z_rads', halfFovZRads);
|
||||
setSingleFloat(this, 'half_fov_y_rads', halfFovYRads);
|
||||
this.set_uniform_float(shaderUniformLocations['fov_half_widths'], 2, fovHalfWidths);
|
||||
this.set_uniform_float(shaderUniformLocations['fov_widths'], 2, fovWidths);
|
||||
this.set_uniform_float(shaderUniformLocations['texcoord_x_limits'], 2, texcoordXLimits);
|
||||
this.set_uniform_float(shaderUniformLocations['texcoord_x_limits_r'], 2, texcoordXLimitsRight);
|
||||
this.set_uniform_float(shaderUniformLocations['texcoord_visible_area'], 4, texcoordVisibleArea);
|
||||
setSingleFloat(this, 'curved_display', this.curved_display ? 1.0 : 0.0);
|
||||
this.set_uniform_float(shaderUniformLocations['lens_vector'], 3, lensVector);
|
||||
this.set_uniform_float(shaderUniformLocations['lens_vector_r'], 3, lensVectorRight);
|
||||
}
|
||||
|
||||
// update the supported device detected property if the state changes, trigger "notify::" events
|
||||
if (this.supported_device_detected !== validKeepalive) this.supported_device_detected = validKeepalive;
|
||||
|
||||
// update the widescreen property if the state changes while still enabled, trigger "notify::" events
|
||||
if (enabled && this.widescreen_mode_state !== sbsEnabled) this.widescreen_mode_state = sbsEnabled;
|
||||
|
||||
// these variables are always in play, even if enabled is false
|
||||
setSingleFloat(this, 'virtual_display_enabled', enabled ? 1.0 : 0.0);
|
||||
setSingleFloat(this, 'show_banner', imuResetState ? 1.0 : 0.0);
|
||||
setSingleFloat(this, 'sbs_enabled', sbsEnabled ? 1.0 : 0.0);
|
||||
setSingleFloat(this, 'custom_banner_enabled', dataViewUint8(dataView, CUSTOM_BANNER_ENABLED) !== 0 ? 1.0 : 0.0);
|
||||
setSingleFloat(this, 'frametime', 0.0);
|
||||
|
||||
setSingleFloat(this, 'sideview_enabled', 0.0);
|
||||
setSingleFloat(this, 'sideview_position', 0.0);
|
||||
setSingleFloat(this, 'sideview_display_size', 1.0);
|
||||
|
||||
this.set_uniform_float(shaderUniformLocations['display_resolution'], 2, displayRes);
|
||||
Globals.logger.log_debug(`Source resolution ${texture_actor.width}x${texture_actor.height}`);
|
||||
this.set_uniform_float(shaderUniformLocations['source_to_display_ratio'], 2, [texture_actor.width/displayRes[0], texture_actor.height/displayRes[1]]);
|
||||
} else if (dataView.byteLength !== 0) {
|
||||
throw new Error(`Invalid dataView.byteLength: ${dataView.byteLength} !== ${DATA_VIEW_LENGTH}`);
|
||||
}
|
||||
} catch (e) {
|
||||
Globals.logger.log(`[ERROR] xrEffect.js setIntermittentUniformVariables ${e.message}\n${e.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
function checkParityByte(dataView) {
|
||||
const parityByte = dataViewUint8(dataView, IMU_PARITY_BYTE);
|
||||
let parity = 0;
|
||||
const epochUint8 = dataViewUint8Array(dataView, EPOCH_MS);
|
||||
const imuDataUint8 = dataViewUint8Array(dataView, IMU_QUAT_DATA);
|
||||
for (let i = 0; i < epochUint8.length; i++) {
|
||||
parity ^= epochUint8[i];
|
||||
}
|
||||
for (let i = 0; i < imuDataUint8.length; i++) {
|
||||
parity ^= imuDataUint8[i];
|
||||
}
|
||||
return parityByte === parity;
|
||||
}
|
||||
|
||||
export const XREffect = GObject.registerClass({
|
||||
Properties: {
|
||||
'supported-device-detected': GObject.ParamSpec.boolean(
|
||||
'supported-device-detected',
|
||||
'Supported device detected',
|
||||
'Whether a supported device is connected',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
false
|
||||
),
|
||||
'target-monitor': GObject.ParamSpec.jsobject(
|
||||
'target-monitor',
|
||||
'Target Monitor',
|
||||
'Geometry of the target monitor for this effect',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'target-framerate': GObject.ParamSpec.uint(
|
||||
'target-framerate',
|
||||
'Target Framerate',
|
||||
'Target framerate for this effect',
|
||||
GObject.ParamFlags.READWRITE, 30, 240, 60
|
||||
),
|
||||
'texture-monitor-position': GObject.ParamSpec.jsobject(
|
||||
'texture-monitor-position',
|
||||
'Texture Monitor Position',
|
||||
'Coordinates of the monitor relative to the target actor texture',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'display-distance': GObject.ParamSpec.double(
|
||||
'display-distance',
|
||||
'Display Distance',
|
||||
'How far away the display appears',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0.2,
|
||||
2.5,
|
||||
1.05
|
||||
),
|
||||
'display-size': GObject.ParamSpec.double(
|
||||
'display-size',
|
||||
'Display size',
|
||||
'Size of the display',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0.2,
|
||||
2.5,
|
||||
1.0
|
||||
),
|
||||
'toggle-display-distance-start': GObject.ParamSpec.double(
|
||||
'toggle-display-distance-start',
|
||||
'Display distance start',
|
||||
'Start distance when using the "change distance" shortcut.',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0.2,
|
||||
2.5,
|
||||
1.05
|
||||
),
|
||||
'toggle-display-distance-end': GObject.ParamSpec.double(
|
||||
'toggle-display-distance-end',
|
||||
'Display distance end',
|
||||
'End distance when using the "change distance" shortcut.',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0.2,
|
||||
2.5,
|
||||
1.05
|
||||
),
|
||||
'curved-display': GObject.ParamSpec.boolean(
|
||||
'curved-display',
|
||||
'Curved Display',
|
||||
'Whether the display is curved',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
false
|
||||
),
|
||||
'widescreen-mode-state': GObject.ParamSpec.boolean(
|
||||
'widescreen-mode-state',
|
||||
'Widescreen mode state',
|
||||
'The state of widescreen mode from the perspective of the driver',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
false
|
||||
),
|
||||
'look-ahead-override': GObject.ParamSpec.int(
|
||||
'look-ahead-override',
|
||||
'Look ahead override',
|
||||
'Override the look ahead value',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
-1,
|
||||
45,
|
||||
-1
|
||||
),
|
||||
'disable-anti-aliasing': GObject.ParamSpec.boolean(
|
||||
'disable-anti-aliasing',
|
||||
'Disable anti-aliasing',
|
||||
'Disable anti-aliasing for the effect',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
false
|
||||
)
|
||||
}
|
||||
}, class XREffect extends Shell.GLSLEffect {
|
||||
constructor(params = {}) {
|
||||
super(params);
|
||||
|
||||
this._distance_ease_timeline = null;
|
||||
this.connect('notify::toggle-display-distance-start', this._handle_display_distance_properties_change.bind(this));
|
||||
this.connect('notify::toggle-display-distance-end', this._handle_display_distance_properties_change.bind(this));
|
||||
this.connect('notify::display-distance', this._handle_display_distance_properties_change.bind(this));
|
||||
this._handle_display_distance_properties_change();
|
||||
|
||||
const calibrating = GdkPixbuf.Pixbuf.new_from_file(`${Globals.extension_dir}/textures/calibrating.png`);
|
||||
this.calibratingImage = new Clutter.Image();
|
||||
this.calibratingImage.set_data(calibrating.get_pixels(), Cogl.PixelFormat.RGB_888,
|
||||
calibrating.width, calibrating.height, calibrating.rowstride);
|
||||
|
||||
const customBanner = GdkPixbuf.Pixbuf.new_from_file(`${Globals.extension_dir}/textures/custom_banner.png`);
|
||||
this.customBannerImage = new Clutter.Image();
|
||||
this.customBannerImage.set_data(customBanner.get_pixels(), Cogl.PixelFormat.RGB_888,
|
||||
customBanner.width, customBanner.height, customBanner.rowstride);
|
||||
}
|
||||
|
||||
_handle_display_distance_properties_change() {
|
||||
const distance_from_end = Math.abs(this.display_distance - this.toggle_display_distance_end);
|
||||
const distance_from_start = Math.abs(this.display_distance - this.toggle_display_distance_start);
|
||||
this._is_display_distance_at_end = distance_from_end < distance_from_start;
|
||||
}
|
||||
|
||||
_change_distance() {
|
||||
if (this._distance_ease_timeline?.is_playing()) this._distance_ease_timeline.stop();
|
||||
|
||||
this._distance_ease_start = this.display_distance;
|
||||
this._distance_ease_timeline = Clutter.Timeline.new_for_actor(this.get_actor(), 250);
|
||||
|
||||
const toggle_display_distance_target = this._is_display_distance_at_end ?
|
||||
this.toggle_display_distance_start : this.toggle_display_distance_end;
|
||||
this._distance_ease_timeline.connect('new-frame', () => {
|
||||
this.display_distance = this._distance_ease_start +
|
||||
this._distance_ease_timeline.get_progress() *
|
||||
(toggle_display_distance_target - this._distance_ease_start);
|
||||
});
|
||||
|
||||
this._distance_ease_timeline.start();
|
||||
}
|
||||
|
||||
vfunc_build_pipeline() {
|
||||
const code = getShaderSource(`${Globals.extension_dir}/Sombrero.frag`);
|
||||
const main = 'PS_Sombrero(virtual_display_enabled, false, source_to_display_ratio, show_banner, cogl_tex_coord_in[0].xy, cogl_color_out);';
|
||||
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, code, main, false);
|
||||
}
|
||||
|
||||
vfunc_paint_target(node, paintContext) {
|
||||
var calibratingImage = this.calibratingImage;
|
||||
var customBannerImage = this.customBannerImage;
|
||||
let data = Globals.ipc_file.load_contents(null);
|
||||
if (data[0]) {
|
||||
let buffer = new Uint8Array(data[1]).buffer;
|
||||
this._dataView = new DataView(buffer);
|
||||
if (!this._initialized) {
|
||||
this.set_uniform_float(this.get_uniform_location('screenTexture'), 1, [0]);
|
||||
|
||||
this.get_pipeline().set_layer_texture(1, calibratingImage.get_texture());
|
||||
this.get_pipeline().set_layer_texture(2, customBannerImage.get_texture());
|
||||
this.get_pipeline().set_uniform_1i(this.get_uniform_location('calibratingTexture'), 1);
|
||||
this.get_pipeline().set_uniform_1i(this.get_uniform_location('customBannerTexture'), 2);
|
||||
|
||||
for (let key in shaderUniformLocations) {
|
||||
shaderUniformLocations[key] = this.get_uniform_location(key);
|
||||
}
|
||||
this.setIntermittentUniformVariables = setIntermittentUniformVariables.bind(this);
|
||||
this.setIntermittentUniformVariables();
|
||||
|
||||
this._redraw_timeline = Clutter.Timeline.new_for_actor(this.get_actor(), 1000);
|
||||
this._redraw_timeline.connect('new-frame', (() => {
|
||||
this.queue_repaint();
|
||||
}).bind(this));
|
||||
this._redraw_timeline.set_repeat_count(-1);
|
||||
this._redraw_timeline.start();
|
||||
|
||||
this._uniforms_timeout_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, (() => {
|
||||
this.setIntermittentUniformVariables();
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
}).bind(this));
|
||||
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
let success = false;
|
||||
let attempts = 0;
|
||||
while (!success && attempts < 2) {
|
||||
if (this._dataView.byteLength === DATA_VIEW_LENGTH) {
|
||||
if (checkParityByte(this._dataView)) {
|
||||
setSingleFloat(this, 'display_north_offset', this.display_distance);
|
||||
setSingleFloat(this, 'look_ahead_ms',
|
||||
this.look_ahead_override === -1 ? lookAheadMS(this._dataView) : this.look_ahead_override);
|
||||
setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA);
|
||||
setSingleFloat(this, 'display_size', this.display_size);
|
||||
success = true;
|
||||
}
|
||||
} else if (this._dataView.byteLength !== 0) {
|
||||
Globals.logger.log(`[ERROR] Invalid dataView.byteLength: ${this._dataView.byteLength} !== ${DATA_VIEW_LENGTH}`)
|
||||
}
|
||||
|
||||
if (!success && ++attempts < 3) {
|
||||
data = Globals.ipc_file.load_contents(null);
|
||||
if (data[0]) {
|
||||
buffer = new Uint8Array(data[1]).buffer;
|
||||
this._dataView = new DataView(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.disable_anti_aliasing) {
|
||||
// improves sampling quality for smooth text and edges
|
||||
this.get_pipeline().set_layer_filters(
|
||||
0,
|
||||
Cogl.PipelineFilter.LINEAR_MIPMAP_LINEAR,
|
||||
Cogl.PipelineFilter.LINEAR
|
||||
);
|
||||
}
|
||||
}
|
||||
super.vfunc_paint_target(node, paintContext);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this._redraw_timeline) {
|
||||
this._redraw_timeline.stop();
|
||||
this._redraw_timeline = null;
|
||||
}
|
||||
if (this._uniforms_timeout_id) GLib.source_remove(this._uniforms_timeout_id);
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue