Merge branch 'multimonitor' into main
This commit is contained in:
commit
7bd026c9c3
|
|
@ -7,6 +7,7 @@ if [ -z "$XDG_DATA_HOME" ]; then
|
|||
XDG_DATA_HOME="$USER_HOME/.local/share"
|
||||
fi
|
||||
DATA_DIR="$XDG_DATA_HOME/breezy_gnome"
|
||||
mkdir -p $DATA_DIR
|
||||
|
||||
# if $XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com exists
|
||||
extension_path="$XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
const { Clutter, GLib, GObject } = imports.gi;
|
||||
|
||||
export const CustomEffect = GObject.registerClass({
|
||||
Properties: {
|
||||
'fov-degrees': GObject.ParamSpec.double(
|
||||
'fov-degrees',
|
||||
'FOV Degrees',
|
||||
'Diagonal field-of-view in degrees',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
1.0,
|
||||
179.0,
|
||||
60.0
|
||||
)
|
||||
}
|
||||
}, class Customffect extends Clutter.ShaderEffect {
|
||||
_init(params = {}) {
|
||||
super._init(params);
|
||||
|
||||
this.fov_degrees = params['fov-degrees'] || 60.0;
|
||||
this.connect('notify::fov-degrees', this._updateMatrices.bind(this));
|
||||
|
||||
// Set up the vertex shader
|
||||
this.set_shader_source(Clutter.ShaderType.VERTEX, `
|
||||
uniform mat4 viewMatrix;
|
||||
uniform mat4 projectionMatrix;
|
||||
uniform vec4 quaternion;
|
||||
|
||||
vec3 applyQuaternionToVector(vec3 v, vec4 q) {
|
||||
return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// First apply the view matrix to position the vertex in camera space
|
||||
vec4 viewPosition = viewMatrix * vec4(gl_Vertex.xyz, 1.0);
|
||||
// Then apply the quaternion rotation
|
||||
vec3 transformedPosition = applyQuaternionToVector(viewPosition.xyz, quaternion);
|
||||
// Finally apply the projection matrix
|
||||
gl_Position = projectionMatrix * vec4(transformedPosition, 1.0);
|
||||
gl_TexCoord[0] = gl_MultiTexCoord0;
|
||||
}
|
||||
`);
|
||||
|
||||
// Initialize with the current matrices
|
||||
this._updateMatrices();
|
||||
}
|
||||
|
||||
_updateMatrices() {
|
||||
let aspect = this.get_parent().width / this.get_parent().height;
|
||||
let fov = this.fov_degrees * Math.PI / 180.0;
|
||||
let near = 0.1;
|
||||
let far = 100.0;
|
||||
let top = Math.tan(fov / 2.0) * near;
|
||||
let bottom = -top;
|
||||
let right = top * aspect;
|
||||
let left = -right;
|
||||
|
||||
let projectionMatrix = GLib.Matrix.init_frustum(left, right, bottom, top, near, far);
|
||||
let viewMatrix = GLib.Matrix.init_identity();
|
||||
|
||||
// Calculate the appropriate Z-distance based on FOV
|
||||
let distance = -1.0 / Math.tan(fov / 2.0);
|
||||
viewMatrix = viewMatrix.translate(0, 0, distance);
|
||||
|
||||
this.set_shader_uniform_value('projectionMatrix', new Clutter.ShaderValue({matrix: projectionMatrix}));
|
||||
this.set_shader_uniform_value('viewMatrix', new Clutter.ShaderValue({matrix: viewMatrix}));
|
||||
}
|
||||
|
||||
set_quaternion(quat) {
|
||||
this.set_shader_uniform_value('quaternion', new Clutter.ShaderValue({vector4: quat}));
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import Gio from 'gi://Gio';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import Globals from './globals.js';
|
||||
import {
|
||||
dataViewEnd,
|
||||
dataViewUint8,
|
||||
dataViewBigUint,
|
||||
dataViewUint32Array,
|
||||
dataViewUint8Array,
|
||||
dataViewFloat,
|
||||
dataViewFloatArray,
|
||||
BOOL_SIZE,
|
||||
FLOAT_SIZE,
|
||||
UINT_SIZE,
|
||||
UINT8_SIZE
|
||||
} from "./ipc.js";
|
||||
import { isValidKeepAlive, getEpochSec, toSec } from "./time.js";
|
||||
|
||||
const IPC_FILE_PATH = "/dev/shm/breezy_desktop_imu";
|
||||
const KEEPALIVE_REFRESH_INTERVAL_SEC = 1;
|
||||
|
||||
// the driver should be using the same data layout version
|
||||
const DATA_LAYOUT_VERSION = 3;
|
||||
|
||||
// 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);
|
||||
|
||||
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 DeviceDataStream = GObject.registerClass({
|
||||
Properties: {
|
||||
'supported-device-connected': GObject.ParamSpec.boolean(
|
||||
'supported-device-connected',
|
||||
'Supported device connected',
|
||||
'Whether a supported device is connected',
|
||||
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
|
||||
),
|
||||
'imu-snapshots': GObject.ParamSpec.jsobject(
|
||||
'imu-snapshots',
|
||||
'IMU Snapshots',
|
||||
'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
|
||||
GObject.ParamFlags.READWRITE
|
||||
)
|
||||
}
|
||||
}, class DeviceDataStream extends GObject.Object {
|
||||
constructor(params = {}) {
|
||||
super(params);
|
||||
this.supported_device_connected = false;
|
||||
this._ipc_file = Gio.file_new_for_path(IPC_FILE_PATH);
|
||||
this._running = false;
|
||||
this._device_data = null;
|
||||
}
|
||||
|
||||
start() {
|
||||
this._running = true;
|
||||
this._poll();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._running = false;
|
||||
}
|
||||
|
||||
// polling is just intended to keep supported_device_connected current, anything needing up-to-date imu data should
|
||||
// trigger a refresh with the default flag
|
||||
_poll() {
|
||||
if (this._running) {
|
||||
this.refresh_data(true);
|
||||
setTimeout(this._poll.bind(this), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the data from the IPC file. if keepalive_only is true, we'll only check and update supported_device_connected if it
|
||||
// hasn't been checked within KEEPALIVE_REFRESH_INTERVAL_SEC.
|
||||
refresh_data(keepalive_only = false) {
|
||||
if (!this._device_data?.imuData || !keepalive_only || getEpochSec() - this._device_data.imuDateMs > KEEPALIVE_REFRESH_INTERVAL_SEC) {
|
||||
let data = this._ipc_file.load_contents(null);
|
||||
if (data[0]) {
|
||||
let buffer = new Uint8Array(data[1]).buffer;
|
||||
let dataView = new DataView(buffer);
|
||||
if (dataView.byteLength === DATA_VIEW_LENGTH) {
|
||||
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 version = dataViewUint8(dataView, VERSION);
|
||||
const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive;
|
||||
const sbsEnabled = dataViewUint8(dataView, SBS_ENABLED) !== 0;
|
||||
|
||||
// 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;
|
||||
|
||||
if (!this._device_data) {
|
||||
this._device_data = {
|
||||
version,
|
||||
enabled,
|
||||
imuResetState,
|
||||
displayRes: dataViewUint32Array(dataView, DISPLAY_RES),
|
||||
sbsEnabled,
|
||||
displayFov: dataViewFloat(dataView, DISPLAY_FOV),
|
||||
lookAheadCfg: dataViewFloatArray(dataView, LOOK_AHEAD_CFG),
|
||||
};
|
||||
}
|
||||
|
||||
let success = keepalive_only;
|
||||
let attempts = 0;
|
||||
while (!success && attempts < 3) {
|
||||
if (dataView.byteLength === DATA_VIEW_LENGTH) {
|
||||
if (checkParityByte(dataView)) {
|
||||
this._device_data.imuData = imuData;
|
||||
this._device_data.imuDateMs = imuDateMs;
|
||||
this.imu_snapshots = {
|
||||
imu_data: imuData,
|
||||
timestamp_ms: imuDateMs
|
||||
};
|
||||
success = true;
|
||||
}
|
||||
} else if (dataView.byteLength !== 0) {
|
||||
Globals.logger.log(`[ERROR] Invalid dataView.byteLength: ${dataView.byteLength} !== ${DATA_VIEW_LENGTH}`)
|
||||
}
|
||||
|
||||
if (!success && ++attempts < 3) {
|
||||
data = this._ipc_file.load_contents(null);
|
||||
if (data[0]) {
|
||||
buffer = new Uint8Array(data[1]).buffer;
|
||||
dataView = new DataView(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// update the supported device connected property if the state changes, trigger "notify::" events
|
||||
if (this.supported_device_connected !== validKeepalive) this.supported_device_connected = validKeepalive;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.supported_device_connected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,21 +1,23 @@
|
|||
import Clutter from 'gi://Clutter'
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import Meta from 'gi://Meta';
|
||||
import Shell from 'gi://Shell';
|
||||
|
||||
import { CursorManager } from './cursormanager.js';
|
||||
import { DeviceDataStream } from './devicedatastream.js';
|
||||
import Globals from './globals.js';
|
||||
import { Logger } from './logger.js';
|
||||
import { MonitorManager } from './monitormanager.js';
|
||||
import { Overlay } from './overlay.js';
|
||||
import { isValidKeepAlive } from './time.js';
|
||||
import { IPC_FILE_PATH, XREffect } from './xrEffect.js';
|
||||
import { VirtualMonitorsActor } from './virtualmonitorsactor.js';
|
||||
|
||||
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
const NESTED_MONITOR_PRODUCT = 'MetaMonitor';
|
||||
const VIRTUAL_MONITOR_PRODUCT = 'Virtual remote monitor';
|
||||
const SUPPORTED_MONITOR_PRODUCTS = [
|
||||
'VITURE',
|
||||
'nreal air',
|
||||
|
|
@ -42,7 +44,7 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
// Set/destroyed by enable/disable
|
||||
this._cursor_manager = null;
|
||||
this._monitor_manager = null;
|
||||
this._xr_effect = null;
|
||||
this._overlay_content = null;
|
||||
this._overlay = null;
|
||||
this._target_monitor = null;
|
||||
this._is_effect_running = false;
|
||||
|
|
@ -51,7 +53,7 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
this._follow_threshold_connection = null;
|
||||
this._widescreen_mode_settings_connection = null;
|
||||
this._widescreen_mode_effect_state_connection = null;
|
||||
this._supported_device_detected_connected = null;
|
||||
this._supported_device_detected_connection = null;
|
||||
this._start_binding = null;
|
||||
this._end_binding = null;
|
||||
this._curved_display_binding = null;
|
||||
|
|
@ -62,6 +64,8 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
this._headset_as_primary_binding = null;
|
||||
this._actor_added_connection = null;
|
||||
this._actor_removed_connection = null;
|
||||
this._data_stream_connection = null;
|
||||
this._stage_redraw_connection = null;
|
||||
|
||||
if (!Globals.logger) {
|
||||
Globals.logger = new Logger({
|
||||
|
|
@ -70,6 +74,10 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
});
|
||||
Globals.logger.logVersion();
|
||||
}
|
||||
|
||||
if (!Globals.data_stream) {
|
||||
Globals.data_stream = new DeviceDataStream();
|
||||
}
|
||||
}
|
||||
|
||||
enable() {
|
||||
|
|
@ -79,6 +87,8 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
Globals.extension_dir = this.path;
|
||||
this.settings.bind('debug', Globals.logger, 'debug', Gio.SettingsBindFlags.DEFAULT);
|
||||
|
||||
Globals.data_stream.start();
|
||||
|
||||
this._monitor_manager = new MonitorManager({
|
||||
use_optimal_monitor_config: this.settings.get_boolean('use-optimal-monitor-config'),
|
||||
headset_as_primary: this.settings.get_boolean('headset-as-primary'),
|
||||
|
|
@ -119,7 +129,7 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
if (this._check_driver_running() && target_monitor) {
|
||||
if (Globals.data_stream.supported_device_connected && 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.
|
||||
|
|
@ -130,6 +140,7 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
this._running_poller_id = undefined;
|
||||
return GLib.SOURCE_REMOVE;
|
||||
} else {
|
||||
Globals.logger.log_debug(`BreezyDesktopExtension _poll_for_ready - device connected: ${Globals.data_stream.supported_device_connected}, target_monitor: ${!!target_monitor}`);
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -140,19 +151,42 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
}).bind(this));
|
||||
}
|
||||
|
||||
_find_virtual_monitors() {
|
||||
try {
|
||||
Globals.logger.log_debug('BreezyDesktopExtension _find_virtual_monitors');
|
||||
const virtual_monitors = this._monitor_manager.getMonitorPropertiesList()?.filter(
|
||||
monitor => monitor && monitor.product === VIRTUAL_MONITOR_PRODUCT);
|
||||
if (virtual_monitors.length > 0) {
|
||||
Globals.logger.log(`Found ${virtual_monitors.length} virtual monitors`);
|
||||
return virtual_monitors.map(monitor => {
|
||||
return this._monitor_manager.getMonitors()[monitor.index];
|
||||
});
|
||||
}
|
||||
|
||||
Globals.logger.log_debug('BreezyDesktopExtension _find_virtual_monitors - No virtual monitors found');
|
||||
} catch (e) {
|
||||
Globals.logger.log(`[ERROR] BreezyDesktopExtension _find_virtual_monitors ${e.message}\n${e.stack}`)
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
_find_supported_monitor() {
|
||||
if (!this._monitor_manager.getMonitorPropertiesList()) return null;
|
||||
|
||||
try {
|
||||
Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor');
|
||||
const target_monitor = this._monitor_manager.getMonitorPropertiesList()?.find(
|
||||
monitor => SUPPORTED_MONITOR_PRODUCTS.includes(monitor.product) ||
|
||||
this.settings.get_string('custom-monitor-product') === monitor.product);
|
||||
monitor => monitor && (SUPPORTED_MONITOR_PRODUCTS.includes(monitor.product) ||
|
||||
this.settings.get_string('custom-monitor-product') === monitor.product));
|
||||
if (target_monitor !== undefined) {
|
||||
Globals.logger.log(`Identified supported monitor: ${target_monitor.product} on ${target_monitor.connector}`);
|
||||
return {
|
||||
monitor: this._monitor_manager.getMonitors()[target_monitor.index],
|
||||
connector: target_monitor.connector,
|
||||
refreshRate: target_monitor.refreshRate,
|
||||
is_dummy: target_monitor.product === NESTED_MONITOR_PRODUCT
|
||||
is_dummy: target_monitor.product === NESTED_MONITOR_PRODUCT,
|
||||
is_virtual: target_monitor.product === VIRTUAL_MONITOR_PRODUCT
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +234,7 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
if (target_monitor && this._running_poller_id === undefined) {
|
||||
this._target_monitor = target_monitor;
|
||||
|
||||
if (this._check_driver_running()) {
|
||||
if (Globals.data_stream.supported_device_connected) {
|
||||
// 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)) {
|
||||
|
|
@ -222,21 +256,6 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
}
|
||||
}
|
||||
|
||||
_check_driver_running() {
|
||||
try {
|
||||
if (!Globals.ipc_file) Globals.ipc_file = Gio.file_new_for_path(IPC_FILE_PATH);
|
||||
if (Globals.ipc_file.query_exists(null)) {
|
||||
const file_info = Globals.ipc_file.query_info(Gio.FILE_ATTRIBUTE_TIME_MODIFIED, Gio.FileQueryInfoFlags.NONE, null);
|
||||
const file_modified_time = file_info.get_attribute_uint64(Gio.FILE_ATTRIBUTE_TIME_MODIFIED);
|
||||
return isValidKeepAlive(file_modified_time);
|
||||
}
|
||||
} catch (e) {
|
||||
Globals.logger.log(`[ERROR] BreezyDesktopExtension _check_driver_running ${e.message}\n${e.stack}`);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_needs_widescreen_monitor_update() {
|
||||
Globals.logger.log_debug('BreezyDesktopExtension _needs_widescreen_monitor_update');
|
||||
const state = this._read_state();
|
||||
|
|
@ -275,46 +294,39 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
clutterContainer ? 'actor-removed' : 'child-removed',
|
||||
this._handle_sibling_update.bind(this),
|
||||
);
|
||||
|
||||
this._xr_effect = new XREffect({
|
||||
target_monitor: targetMonitor,
|
||||
target_framerate: refreshRate,
|
||||
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'),
|
||||
look_ahead_override: this.settings.get_int('look-ahead-override'),
|
||||
disable_anti_aliasing: this.settings.get_boolean('disable-anti-aliasing')
|
||||
});
|
||||
|
||||
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);
|
||||
// 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._supported_device_detected_connected = this._xr_effect.connect('notify::supported-device-detected', this._handle_supported_device_change.bind(this));
|
||||
// this._widescreen_mode_effect_state_connection = this._xr_effect.connect('notify::widescreen-mode-state', this._update_widescreen_mode_from_state.bind(this));
|
||||
// this._supported_device_detected_connection = this._xr_effect.connect('notify::supported-device-detected', this._handle_supported_device_change.bind(this));
|
||||
this._overlay_content.renderMonitors();
|
||||
|
||||
this._distance_binding = this.settings.bind('display-distance', this._xr_effect, '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._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._xr_effect, 'toggle-display-distance-start', Gio.SettingsBindFlags.DEFAULT)
|
||||
this._end_binding = this.settings.bind('toggle-display-distance-end', this._xr_effect, 'toggle-display-distance-end', Gio.SettingsBindFlags.DEFAULT)
|
||||
this._curved_display_binding = this.settings.bind('curved-display', this._xr_effect, 'curved-display', Gio.SettingsBindFlags.DEFAULT)
|
||||
this._display_size_binding = this.settings.bind('display-size', this._xr_effect, 'display-size', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._look_ahead_override_binding = this.settings.bind('look-ahead-override', this._xr_effect, 'look-ahead-override', Gio.SettingsBindFlags.DEFAULT);
|
||||
this._disable_anti_aliasing_binding = this.settings.bind('disable-anti-aliasing', this._xr_effect, 'disable-anti-aliasing', Gio.SettingsBindFlags.DEFAULT);
|
||||
// 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._curved_display_binding = this.settings.bind('curved-display', this._xr_effect, 'curved-display', Gio.SettingsBindFlags.DEFAULT)
|
||||
// this._display_size_binding = this.settings.bind('display-size', this._xr_effect, 'display-size', Gio.SettingsBindFlags.DEFAULT);
|
||||
// this._look_ahead_override_binding = this.settings.bind('look-ahead-override', this._xr_effect, 'look-ahead-override', Gio.SettingsBindFlags.DEFAULT);
|
||||
// this._disable_anti_aliasing_binding = this.settings.bind('disable-anti-aliasing', this._xr_effect, 'disable-anti-aliasing', Gio.SettingsBindFlags.DEFAULT);
|
||||
|
||||
this._overlay.mainActor().add_effect_with_name('xr-desktop', this._xr_effect);
|
||||
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._xr_effect._change_distance.bind(this._xr_effect));
|
||||
this._add_settings_keybinding('toggle-display-distance-shortcut', this._overlay_content._change_distance.bind(this._overlay_content));
|
||||
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}`);
|
||||
Globals.logger.log(`[ERROR] BreezyDesktopExtension _effect_enable ${e.message}\n${e.stack}`);
|
||||
this._effect_disable();
|
||||
}
|
||||
|
|
@ -443,12 +455,12 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
}
|
||||
|
||||
_update_widescreen_mode_from_settings(settings, event) {
|
||||
const value = settings.get_boolean('widescreen-mode');
|
||||
Globals.logger.log_debug(`BreezyDesktopExtension _update_widescreen_mode_from_settings ${value}`);
|
||||
if (value !== undefined && value !== this._xr_effect.widescreen_mode_state) {
|
||||
this._request_sbs_mode_change(value);
|
||||
} else
|
||||
Globals.logger.log_debug('effect.widescreen_mode_state already matched setting');
|
||||
// const value = settings.get_boolean('widescreen-mode');
|
||||
// Globals.logger.log_debug(`BreezyDesktopExtension _update_widescreen_mode_from_settings ${value}`);
|
||||
// if (value !== undefined && value !== this._xr_effect.widescreen_mode_state) {
|
||||
// this._request_sbs_mode_change(value);
|
||||
// } else
|
||||
// Globals.logger.log_debug('effect.widescreen_mode_state already matched setting');
|
||||
}
|
||||
|
||||
_update_widescreen_mode_from_state(effect, _pspec) {
|
||||
|
|
@ -540,6 +552,11 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
Main.wm.removeKeybinding('toggle-display-distance-shortcut');
|
||||
Main.wm.removeKeybinding('toggle-follow-shortcut');
|
||||
Meta.enable_unredirect_for_display(global.display);
|
||||
|
||||
if (this._stage_redraw_connection) {
|
||||
global.stage.disconnect(this._stage_redraw_connection);
|
||||
this._stage_redraw_connection = null;
|
||||
}
|
||||
|
||||
if (this._actor_added_connection) {
|
||||
global.stage.disconnect(this._actor_added_connection);
|
||||
|
|
@ -557,6 +574,10 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
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;
|
||||
|
|
@ -589,6 +610,24 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
this.settings.unbind(this._disable_anti_aliasing_binding);
|
||||
this._disable_anti_aliasing_binding = null;
|
||||
}
|
||||
if (this._overlay) {
|
||||
if (this._overlay_content) {
|
||||
// if (this._widescreen_mode_effect_state_connection) {
|
||||
// this._xr_effect.disconnect(this._widescreen_mode_effect_state_connection);
|
||||
// this._widescreen_mode_effect_state_connection = null;
|
||||
// }
|
||||
// if (this._supported_device_detected_connection) {
|
||||
// this._xr_effect.disconnect(this._supported_device_detected_connection);
|
||||
// this._supported_device_detected_connection = null;
|
||||
// }
|
||||
this._overlay_content.destroy();
|
||||
this._overlay_content = null;
|
||||
}
|
||||
global.stage.remove_child(this._overlay);
|
||||
this._overlay.destroy();
|
||||
this._overlay = null;
|
||||
|
||||
}
|
||||
if (this._xr_effect) {
|
||||
if (this._widescreen_mode_effect_state_connection) {
|
||||
this._xr_effect.disconnect(this._widescreen_mode_effect_state_connection);
|
||||
|
|
@ -624,8 +663,11 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
disable() {
|
||||
try {
|
||||
Globals.logger.log_debug('BreezyDesktopExtension disable');
|
||||
Globals.data_stream.stop();
|
||||
|
||||
this._effect_disable();
|
||||
this._target_monitor = null;
|
||||
|
||||
if (this._monitor_manager) {
|
||||
if (this._optimal_monitor_config_binding) {
|
||||
this.settings.unbind(this._optimal_monitor_config_binding);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const Globals = {
|
||||
logger: null,
|
||||
ipc_file: null, // Gio.File instance, file exists if set
|
||||
extension_dir: null // string path
|
||||
extension_dir: null, // string path
|
||||
data_stream: null, // DeviceDataStream instance
|
||||
}
|
||||
export default Globals;
|
||||
|
|
@ -35,7 +35,7 @@ function getDisplayConfigProxy(extPath) {
|
|||
xml = new TextDecoder().decode(bytes);
|
||||
}
|
||||
} catch (e) {
|
||||
Globals.logger.log('ERROR: failed to load DisplayConfig interface XML');
|
||||
Globals.logger.log('[ERROR] failed to load DisplayConfig interface XML');
|
||||
throw e;
|
||||
}
|
||||
cachedDisplayConfigProxy = Gio.DBusProxy.makeProxyWrapper(xml);
|
||||
|
|
@ -54,33 +54,28 @@ export function newDisplayConfig(extPath, callback) {
|
|||
}
|
||||
|
||||
function getMonitorConfig(displayConfigProxy, callback) {
|
||||
displayConfigProxy.GetResourcesRemote((result, error) => {
|
||||
displayConfigProxy.GetCurrentStateRemote((result, error) => {
|
||||
if (error) {
|
||||
callback(null, `GetResourcesRemote failed: ${error}`);
|
||||
callback(null, `GetCurrentState failed: ${error}`);
|
||||
} else {
|
||||
const monitors = [];
|
||||
for (let i = 0; i < result[2].length; i++) {
|
||||
const output = result[2][i];
|
||||
if (output.length <= 7) {
|
||||
callback(null, 'Cannot get DisplayConfig: No properties on output #' + i);
|
||||
return;
|
||||
}
|
||||
const props = output[7];
|
||||
const displayName = props['display-name'].get_string()[0];
|
||||
const connectorName = output[4];
|
||||
if (!displayName || displayName == '') {
|
||||
const displayName = 'Monitor on output ' + connectorName;
|
||||
}
|
||||
const vendor = props['vendor'].get_string()[0];
|
||||
const product = props['product'].get_string()[0];
|
||||
const serial = props['serial'].get_string()[0];
|
||||
Globals.logger.log_debug(`monitormanager.js getMonitorConfig GetCurrentState result: ${JSON.stringify(result)}`);
|
||||
|
||||
const allMonitors = [];
|
||||
const [serial, monitors, logicalMonitors, properties] = result;
|
||||
for (let monitor of monitors) {
|
||||
const [details, modes, monProperties] = monitor;
|
||||
const [connector, vendor, product, monitorSerial] = details;
|
||||
const displayName = monProperties['display-name'].get_string()[0];
|
||||
|
||||
// grab refresh rate from the modes array
|
||||
const refreshRate = result[3][i][4];
|
||||
|
||||
monitors.push([displayName, connectorName, vendor, product, serial, refreshRate]);
|
||||
for (let mode of modes) {
|
||||
const [modeId, width, height, refreshRate, preferredScale, supportedScales, modeProperites] = mode;
|
||||
const isCurrent = !!modeProperites['is-current'];
|
||||
if (isCurrent) {
|
||||
allMonitors.push([displayName, connector, vendor, product, serial, refreshRate]);
|
||||
}
|
||||
}
|
||||
}
|
||||
callback(monitors, null);
|
||||
callback(allMonitors, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -287,6 +282,7 @@ export const MonitorManager = GObject.registerClass({
|
|||
// help prevent certain actions from taking place multiple times in the event of rapid monitor updates
|
||||
this._asyncRequestsInFlight = 0;
|
||||
this._configCheckRequestsCount = 0;
|
||||
this._enabled = false;
|
||||
}
|
||||
|
||||
enable() {
|
||||
|
|
@ -300,12 +296,14 @@ export const MonitorManager = GObject.registerClass({
|
|||
}).bind(this));
|
||||
|
||||
this._monitorsChangedConnection = Main.layoutManager.connect('monitors-changed', this._on_monitors_change.bind(this));
|
||||
this._enabled = true;
|
||||
}
|
||||
|
||||
disable() {
|
||||
Globals.logger.log_debug('MonitorManager disable');
|
||||
Main.layoutManager.disconnect(this._monitorsChangedConnection);
|
||||
|
||||
this._enabled = false;
|
||||
this._monitorsChangedConnection = null;
|
||||
this._displayConfigProxy = null;
|
||||
this._monitorProperties = null;
|
||||
|
|
@ -380,6 +378,8 @@ export const MonitorManager = GObject.registerClass({
|
|||
}
|
||||
|
||||
_on_monitors_change() {
|
||||
if (!this._enabled) return;
|
||||
|
||||
Globals.logger.log_debug('MonitorManager _on_monitors_change');
|
||||
if (this._displayConfigProxy == null) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,788 @@
|
|||
import Clutter from 'gi://Clutter'
|
||||
import Cogl from 'gi://Cogl';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import Shell from 'gi://Shell';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
import Globals from './globals.js';
|
||||
|
||||
function applyQuaternionToVector(vector, quaternion) {
|
||||
const t = [
|
||||
2.0 * (quaternion[1] * vector[2] - quaternion[2] * vector[1]),
|
||||
2.0 * (quaternion[2] * vector[0] - quaternion[0] * vector[2]),
|
||||
2.0 * (quaternion[0] * vector[1] - quaternion[1] * vector[0])
|
||||
];
|
||||
return [
|
||||
vector[0] + quaternion[3] * t[0] + quaternion[1] * t[2] - quaternion[2] * t[1],
|
||||
vector[1] + quaternion[3] * t[1] + quaternion[2] * t[0] - quaternion[0] * t[2],
|
||||
vector[2] + quaternion[3] * t[2] + quaternion[0] * t[1] - quaternion[1] * t[0]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the vector in the array that's closest to the quaternion rotation
|
||||
*
|
||||
* @param {number[]} quaternion - Reference quaternion [x, y, z, w]
|
||||
* @param {number[][]} vectors - Array of vectors [x, y, z] to search from
|
||||
* @returns {number} Index of the closest vector, if it surpasses the previous closest index by a certain margin, otherwise the previous index
|
||||
*/
|
||||
function findClosestVector(quaternion, vectors, previousClosestIndex) {
|
||||
|
||||
const lookVector = [1.0, 0.0, 0.0]; // NWU vector pointing to the center of the screen
|
||||
const rotatedLookVector = applyQuaternionToVector(lookVector, quaternion);
|
||||
// Globals.logger.log(`\t\t\tRotated look vector: ${rotatedLookVector}`);
|
||||
|
||||
let closestIndex = -1;
|
||||
let closestDistance = Infinity;
|
||||
let previousDistance = Infinity;
|
||||
|
||||
// find the vector closest to the rotated look vector
|
||||
vectors.forEach((vector, index) => {
|
||||
const distance = Math.acos(
|
||||
Math.min(1.0, Math.max(-1.0, vector[0] * rotatedLookVector[0] + vector[1] * rotatedLookVector[1] + vector[2] * rotatedLookVector[2]))
|
||||
);
|
||||
|
||||
if (previousClosestIndex === index) {
|
||||
previousDistance = distance;
|
||||
}
|
||||
|
||||
// Globals.logger.log(`\t\t\tMonitor ${index} distance: ${distance}`);
|
||||
if (distance < closestDistance) {
|
||||
closestIndex = index;
|
||||
closestDistance = distance;
|
||||
}
|
||||
});
|
||||
|
||||
// Globals.logger.log(`\t\t\tClosest monitor: ${closestIndex}, distance: ${closestDistance}`);
|
||||
|
||||
// only switch if the closest monitor is greater than the previous closest by 25%
|
||||
if (previousClosestIndex !== undefined && closestIndex !== previousClosestIndex && closestDistance * 1.25 > previousDistance) {
|
||||
return previousClosestIndex;
|
||||
}
|
||||
|
||||
return closestIndex;
|
||||
}
|
||||
|
||||
function degreesToRadians(degrees) {
|
||||
return degrees * Math.PI / 180.0;
|
||||
}
|
||||
|
||||
function radiansToDegrees(radians) {
|
||||
return radians * 180.0 / Math.PI;
|
||||
}
|
||||
|
||||
/***
|
||||
* @returns {Object} - containing `start`, `center`, and `end` radians for rotating the given monitor
|
||||
*/
|
||||
function monitorWrap(cachedMonitorWrap, radiusPixels, monitorBeginPixel, monitorLengthPixels) {
|
||||
let closestWrap = cachedMonitorWrap.reduce((previous, current) => {
|
||||
return (!previous || Math.abs(current.pixel - monitorBeginPixel) < Math.abs(previous.pixel - monitorBeginPixel)) ? current : previous;
|
||||
}, undefined);
|
||||
|
||||
if (closestWrap.pixel !== monitorBeginPixel) {
|
||||
// there's a gap between the cached wrap value and this one
|
||||
const gapPixels = monitorBeginPixel - closestWrap.pixel;
|
||||
const gapHalfRadians = Math.asin(gapPixels / 2 / radiusPixels);
|
||||
const gapRadians = gapHalfRadians * 2;
|
||||
|
||||
// update the closestWrap value and cache it
|
||||
closestWrap = { pixel: monitorBeginPixel, radians: closestWrap.radians + gapRadians };
|
||||
cachedMonitorWrap.push(closestWrap);
|
||||
}
|
||||
|
||||
const monitorHalfRadians = Math.asin(monitorLengthPixels / 2 / radiusPixels);
|
||||
const centerRadians = closestWrap.radians + monitorHalfRadians;
|
||||
const endRadians = centerRadians + monitorHalfRadians;
|
||||
|
||||
// since we're computing the end values for this monitor, cache them too in case they line up with a future monitor
|
||||
cachedMonitorWrap.push({ pixel: monitorBeginPixel + monitorLengthPixels, radians: endRadians });
|
||||
return {
|
||||
begin: closestWrap.radians,
|
||||
center: centerRadians,
|
||||
end: endRadians
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[]} 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) {
|
||||
const aspect = fovDetails.widthPixels / fovDetails.heightPixels;
|
||||
const fovVerticalRadians = degreesToRadians(fovDetails.fovDegrees / Math.sqrt(1 + aspect * aspect));
|
||||
|
||||
// distance needed for the FOV-sized monitor to fill up the screen
|
||||
const centerRadius = fovDetails.heightPixels / 2 / Math.sin(fovVerticalRadians / 2);
|
||||
|
||||
const monitorPlacements = [];
|
||||
const cachedMonitorWrap = [];
|
||||
|
||||
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);
|
||||
|
||||
cachedMonitorWrap.push({ pixel: 0, radians: -fovHorizontalRadians / 2 });
|
||||
monitorDetailsList.forEach(monitorDetails => {
|
||||
const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorDetails.x, monitorDetails.width);
|
||||
const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.width / 2, 2))
|
||||
|
||||
monitorPlacements.push({
|
||||
topLeftNoRotate: [
|
||||
monitorCenterRadius,
|
||||
-(monitorDetails.width - fovDetails.widthPixels) / 2,
|
||||
-monitorDetails.y
|
||||
],
|
||||
center: [
|
||||
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
|
||||
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
|
||||
|
||||
// west is opposite where radius is the hypotenuse, using monitorWrapDetails.center as the radians
|
||||
-monitorCenterRadius * Math.sin(monitorWrapDetails.center),
|
||||
|
||||
// up is flat when wrapping horizontally
|
||||
-(monitorDetails.y + monitorDetails.height / 2 - fovDetails.heightPixels / 2)
|
||||
],
|
||||
rotationAngleRadians: -monitorWrapDetails.center
|
||||
});
|
||||
});
|
||||
} else if (monitorWrappingScheme === 'vertical') {
|
||||
// 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);
|
||||
|
||||
cachedMonitorWrap.push({ pixel: 0, radians: -fovVerticalRadians / 2 });
|
||||
monitorDetailsList.forEach(monitorDetails => {
|
||||
const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorDetails.y, monitorDetails.height);
|
||||
const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.height / 2, 2)) ;
|
||||
|
||||
monitorPlacements.push({
|
||||
topLeftNoRotate: [
|
||||
monitorCenterRadius,
|
||||
-monitorDetails.x,
|
||||
-(monitorDetails.height - fovDetails.heightPixels) / 2
|
||||
],
|
||||
center: [
|
||||
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
|
||||
monitorCenterRadius * Math.cos(monitorWrapDetails.center),
|
||||
|
||||
// west is flat when wrapping vertically
|
||||
-(monitorDetails.x + monitorDetails.width / 2 - fovDetails.widthPixels / 2),
|
||||
|
||||
// up is opposite where radius is the hypotenuse, using monitorWrapDetails.center as the radians
|
||||
-monitorCenterRadius * Math.sin(monitorWrapDetails.center)
|
||||
],
|
||||
rotationAngleRadians: -monitorWrapDetails.center
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// monitors make a flat wall in front of us, no wrapping
|
||||
monitorDetailsList.forEach(monitorDetails => {
|
||||
monitorPlacements.push({
|
||||
topLeftNoRotate: [
|
||||
centerRadius,
|
||||
-monitorDetails.x,
|
||||
-monitorDetails.y
|
||||
],
|
||||
center: [
|
||||
centerRadius,
|
||||
-(monitorDetails.x + monitorDetails.width / 2 - fovDetails.widthPixels / 2),
|
||||
-(monitorDetails.y + monitorDetails.height / 2 - fovDetails.heightPixels / 2)
|
||||
],
|
||||
rotationAngleRadians: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
Globals.logger.log_debug(`\t\t\tCached monitor wrap: ${JSON.stringify(cachedMonitorWrap)}`);
|
||||
|
||||
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, override) {
|
||||
// how stale the imu data is
|
||||
const dataAge = Date.now() - imuDateMs;
|
||||
|
||||
// if (override === -1)
|
||||
// return lookAheadCfg[0] + dataAge;
|
||||
|
||||
return 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
|
||||
),
|
||||
'imu-snapshots': GObject.ParamSpec.jsobject(
|
||||
'imu-snapshots',
|
||||
'IMU Snapshots',
|
||||
'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
|
||||
GObject.ParamFlags.READWRITE
|
||||
),
|
||||
'fov-degrees': GObject.ParamSpec.double(
|
||||
'fov-degrees',
|
||||
'FOV Degrees',
|
||||
'Field of view in degrees',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
30.0, 100.0, 46.0
|
||||
),
|
||||
'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
|
||||
),
|
||||
'monitor-wrapping-scheme': GObject.ParamSpec.string(
|
||||
'monitor-wrapping-scheme',
|
||||
'Monitor Wrapping Scheme',
|
||||
'How the monitors are wrapped around the viewport',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
'horizontal', ['horizontal', 'vertical', 'none']
|
||||
),
|
||||
'monitor-wrapping-rotation-radians': GObject.ParamSpec.double(
|
||||
'monitor-wrapping-rotation-radians',
|
||||
'Monitor Wrapping Rotation Radians',
|
||||
'Rotation of the monitor wrapping around the viewport',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
-360.0, 360.0, 0.0
|
||||
),
|
||||
'focused-monitor-index': GObject.ParamSpec.int(
|
||||
'focused-monitor-index',
|
||||
'Focused Monitor Index',
|
||||
'Index of the monitor that is currently focused',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0, 100, 0
|
||||
),
|
||||
'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
|
||||
),
|
||||
'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
|
||||
)
|
||||
}
|
||||
}, class VirtualMonitorEffect extends Shell.GLSLEffect {
|
||||
constructor(params = {}) {
|
||||
super(params);
|
||||
|
||||
this._current_display_distance = this._is_focused() ? this.display_distance : this.display_distance_default;
|
||||
|
||||
this.connect('notify::display-distance', this._update_display_distance.bind(this));
|
||||
this.connect('notify::focused-monitor-index', this._update_display_distance.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();
|
||||
}
|
||||
|
||||
const mid_distance = (this.display_distance_default + desired_distance) / 2;
|
||||
|
||||
// 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 timeline_ms = this._distance_ease_focus ? 500 : 150;
|
||||
|
||||
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 < 0.5) return;
|
||||
|
||||
// treat the second half of the timeline as its own full progression
|
||||
progress = (progress - 0.5) * 2;
|
||||
|
||||
// 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 +
|
||||
progress * (this._distance_ease_target - this._distance_ease_start);
|
||||
}).bind(this));
|
||||
|
||||
this._distance_ease_timeline.start();
|
||||
}
|
||||
|
||||
perspective(fovDiagonalRadians, aspect, near, far) {
|
||||
// compute horizontal fov given diagonal fov and aspect ratio
|
||||
const h = Math.sqrt(aspect * aspect + 1);
|
||||
|
||||
const fovRadians = fovDiagonalRadians / h * aspect;
|
||||
console.log(`fovRadians: ${fovRadians}`);
|
||||
|
||||
const f = 1.0 / Math.tan(fovRadians / 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 mat4 u_imu_data;
|
||||
uniform float u_look_ahead_ms;
|
||||
uniform mat4 u_projection_matrix;
|
||||
uniform vec3 u_display_position;
|
||||
uniform float u_rotation_x_radians;
|
||||
uniform float u_rotation_y_radians;
|
||||
uniform vec2 u_display_resolution;
|
||||
|
||||
// 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;
|
||||
|
||||
vec4 quatConjugate(vec4 q) {
|
||||
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);
|
||||
}
|
||||
|
||||
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 t) {
|
||||
return position + velocity * t;
|
||||
}
|
||||
`;
|
||||
|
||||
const main = `
|
||||
vec4 world_pos = cogl_position_in;
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
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;
|
||||
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);
|
||||
world_pos = vec4(applyLookAhead(rotated_vector_t0, velocity_t0, u_look_ahead_ms), 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;
|
||||
|
||||
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 projection_matrix = this.perspective(
|
||||
this.fov_degrees * Math.PI / 180.0,
|
||||
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_rotation_x_radians"), 1, [this.monitor_wrapping_scheme === 'vertical' ? this.monitor_wrapping_rotation_radians : 0.0]);
|
||||
this.set_uniform_float(this.get_uniform_location("u_rotation_y_radians"), 1, [this.monitor_wrapping_scheme === 'horizontal' ? this.monitor_wrapping_rotation_radians : 0.0]);
|
||||
this.set_uniform_float(this.get_uniform_location("u_display_resolution"), 2, [this.get_actor().width, this.get_actor().height]);
|
||||
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._initialized = true;
|
||||
}
|
||||
|
||||
this.set_uniform_float(this.get_uniform_location('u_look_ahead_ms'), 1, [lookAheadMS(this.imu_snapshots.timestamp_ms, 0)]);
|
||||
this.set_uniform_float(this.get_uniform_location("u_display_position"), 3, [this.display_position[0], this.display_position[1], this._current_display_distance * this.display_position[2]]);
|
||||
this.set_uniform_matrix(this.get_uniform_location("u_imu_data"), false, 4, this.imu_snapshots.imu_data);
|
||||
|
||||
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({
|
||||
Properties: {
|
||||
'monitors': GObject.ParamSpec.jsobject(
|
||||
'monitors',
|
||||
'Monitors',
|
||||
'Array of monitor indexes',
|
||||
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
|
||||
),
|
||||
'fov-degrees': GObject.ParamSpec.double(
|
||||
'fov-degrees',
|
||||
'FOV Degrees',
|
||||
'Field of view in degrees',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
30.0, 100.0, 46.0
|
||||
),
|
||||
'focused-monitor-index': GObject.ParamSpec.int(
|
||||
'focused-monitor-index',
|
||||
'Focused Monitor Index',
|
||||
'Index of the monitor that is currently focused',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0, 100, 0
|
||||
),
|
||||
'display-size': GObject.ParamSpec.double(
|
||||
'display-size',
|
||||
'Display size',
|
||||
'Size of the display',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0.2,
|
||||
2.5,
|
||||
1.0
|
||||
),
|
||||
'display-distance': GObject.ParamSpec.double(
|
||||
'display-distance',
|
||||
'Display Distance',
|
||||
'Distance of the display from the camera',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
0.2,
|
||||
2.5,
|
||||
1.05
|
||||
),
|
||||
'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
|
||||
),
|
||||
'target-framerate': GObject.ParamSpec.double(
|
||||
'target-framerate',
|
||||
'Target Framerate',
|
||||
'Target framerate for the virtual monitors',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
1.0, 120.0, 60.0
|
||||
)
|
||||
}
|
||||
}, class VirtualMonitorsActor extends Clutter.Actor {
|
||||
constructor(params = {}) {
|
||||
super(params);
|
||||
|
||||
this.width = this.target_monitor.width;
|
||||
this.height = this.target_monitor.height;
|
||||
this._frametime_ms = Math.floor(1000 / (this.target_framerate ?? 60.0));
|
||||
this._all_monitors = [
|
||||
this.target_monitor,
|
||||
...this.monitors
|
||||
];
|
||||
}
|
||||
|
||||
renderMonitors() {
|
||||
this._monitorPlacements = monitorsToPlacements(
|
||||
{
|
||||
fovDegrees: this.fov_degrees,
|
||||
widthPixels: this.width,
|
||||
heightPixels: this.height
|
||||
},
|
||||
this._all_monitors.map(monitor => ({
|
||||
x: monitor.x,
|
||||
y: monitor.y,
|
||||
width: monitor.width,
|
||||
height: monitor.height
|
||||
})),
|
||||
'horizontal'
|
||||
);
|
||||
|
||||
// normalize the center vectors
|
||||
this._monitorsAsNormalizedVectors = this._monitorPlacements.map(monitorVectors => {
|
||||
const vector = monitorVectors.center;
|
||||
const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
|
||||
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
||||
});
|
||||
const monitors = this._all_monitors;
|
||||
const minMonitorX = Math.min(...monitors.map(monitor => monitor.x));
|
||||
const maxMonitorX = Math.max(...monitors.map(monitor => monitor.x + monitor.width));
|
||||
const minMonitorY = Math.min(...monitors.map(monitor => monitor.y));
|
||||
const maxMonitorY = Math.max(...monitors.map(monitor => monitor.y + monitor.height));
|
||||
|
||||
const displayWidth = global.stage.width;
|
||||
const displayHeight = global.stage.height;
|
||||
const actorToDisplayRatios = [
|
||||
displayWidth / this.width,
|
||||
displayHeight / this.height
|
||||
];
|
||||
|
||||
// how far this viewport actor's center is from the center of the whole stage
|
||||
const actorMidX = this.target_monitor.x + this.width / 2;
|
||||
const actorMidY = this.target_monitor.y + this.height / 2;
|
||||
const actorToDisplayOffsets = [
|
||||
(displayWidth / 2 - (actorMidX - global.stage.x)) * 2 / this.width,
|
||||
(displayHeight / 2 - (actorMidY - global.stage.y)) * 2 / this.height
|
||||
];
|
||||
|
||||
Globals.logger.log_debug(`\t\t\tActor to display ratios: ${actorToDisplayRatios}, offsets: ${actorToDisplayOffsets}`);
|
||||
|
||||
monitors.forEach(((monitor, index) => {
|
||||
// if (index === 0) return;
|
||||
Globals.logger.log(`\t\t\tMonitor ${index}: ${monitor.x}, ${monitor.y}, ${monitor.width}, ${monitor.height}`);
|
||||
|
||||
// this is in NWU coordinates
|
||||
const noRotationVector = this._monitorPlacements[index].topLeftNoRotate;
|
||||
Globals.logger.log_debug(`\t\t\tMonitor ${index} vectors: ${JSON.stringify(this._monitorPlacements[index])}`);
|
||||
|
||||
// actor coordinates are east-up-south
|
||||
const containerActor = new Clutter.Actor({
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
reactive: false,
|
||||
});
|
||||
|
||||
// Create a clone of the stage content for this monitor
|
||||
const monitorClone = new Clutter.Clone({
|
||||
source: Main.layoutManager.uiGroup,
|
||||
reactive: false,
|
||||
x: -monitor.x,
|
||||
y: -monitor.y
|
||||
});
|
||||
monitorClone.set_clip(monitor.x, monitor.y, monitor.width, monitor.height);
|
||||
|
||||
// Add the monitor actor to the scene
|
||||
containerActor.add_child(monitorClone);
|
||||
const effect = new VirtualMonitorEffect({
|
||||
imu_snapshots: this.imu_snapshots,
|
||||
fov_degrees: this.fov_degrees,
|
||||
monitor_index: index,
|
||||
display_position: [-noRotationVector[1], -noRotationVector[2], -noRotationVector[0]],
|
||||
display_distance: this.display_distance,
|
||||
display_distance_default: Math.max(this.toggle_display_distance_start, this.toggle_display_distance_end),
|
||||
monitor_wrapping_scheme: 'horizontal',
|
||||
monitor_wrapping_rotation_radians: this._monitorPlacements[index].rotationAngleRadians,
|
||||
actor_to_display_ratios: actorToDisplayRatios,
|
||||
actor_to_display_offsets: actorToDisplayOffsets
|
||||
});
|
||||
containerActor.add_effect_with_name('viewport-effect', effect);
|
||||
this.add_child(containerActor);
|
||||
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);
|
||||
|
||||
// in addition to rendering distance properly in the shader, the parent actor determines overlap based on child ordering
|
||||
effect.connect('notify::is-closest', ((actor, _pspec) => {
|
||||
if (actor.is_closest) this.set_child_above_sibling(containerActor, null);
|
||||
}).bind(this));
|
||||
}).bind(this));
|
||||
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, (() => {
|
||||
if (this.imu_snapshots) {
|
||||
const closestMonitorIndex = findClosestVector(
|
||||
this.imu_snapshots.imu_data.splice(0, 4),
|
||||
this._monitorsAsNormalizedVectors, this.closestMonitorIndex
|
||||
);
|
||||
|
||||
// only switch if the closest monitor is greater than the previous closest by 25%
|
||||
if (closestMonitorIndex !== -1 && (this.focused_monitor_index === undefined || this.focused_monitor_index !== closestMonitorIndex)) {
|
||||
Globals.logger.log(`Switching to monitor ${closestMonitorIndex}`);
|
||||
this.focused_monitor_index = closestMonitorIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
}).bind(this));
|
||||
|
||||
this._redraw_timeline = Clutter.Timeline.new_for_actor(this, 1000);
|
||||
this._redraw_timeline.connect('new-frame', (() => {
|
||||
// let's try to cap the forced redraw rate
|
||||
if (this._last_redraw !== undefined && Date.now() - this._last_redraw < this._frametime_ms) return;
|
||||
|
||||
Globals.data_stream.refresh_data();
|
||||
this.imu_snapshots = Globals.data_stream.imu_snapshots;
|
||||
this.queue_redraw();
|
||||
this._last_redraw = Date.now();
|
||||
}).bind(this));
|
||||
this._redraw_timeline.set_repeat_count(-1);
|
||||
this._redraw_timeline.start();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
_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() {
|
||||
this.display_distance = this._is_display_distance_at_end ?
|
||||
this.toggle_display_distance_start : this.toggle_display_distance_end;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._redraw_timeline) {
|
||||
this._redraw_timeline.stop();
|
||||
this._redraw_timeline = null;
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
});
|
||||
|
|
@ -67,6 +67,7 @@ const shaderUniformLocations = {
|
|||
'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
|
||||
|
|
@ -132,6 +133,7 @@ function setIntermittentUniformVariables() {
|
|||
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);
|
||||
|
||||
|
|
@ -166,13 +168,23 @@ function setIntermittentUniformVariables() {
|
|||
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 / this.target_monitor.width;
|
||||
const trimHeightPercent = 3.0 / this.target_monitor.height;
|
||||
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);
|
||||
|
|
@ -184,9 +196,10 @@ function setIntermittentUniformVariables() {
|
|||
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);
|
||||
setSingleFloat(this, 'curved_display', this.curved_display ? 1.0 : 0.0);
|
||||
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);
|
||||
}
|
||||
|
|
@ -209,7 +222,8 @@ function setIntermittentUniformVariables() {
|
|||
setSingleFloat(this, 'sideview_display_size', 1.0);
|
||||
|
||||
this.set_uniform_float(shaderUniformLocations['display_resolution'], 2, displayRes);
|
||||
this.set_uniform_float(shaderUniformLocations['source_to_display_ratio'], 2, [this.target_monitor.width/displayRes[0], this.target_monitor.height/displayRes[1]]);
|
||||
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}`);
|
||||
}
|
||||
|
|
@ -253,6 +267,12 @@ export const XREffect = GObject.registerClass({
|
|||
'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',
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@ from .license import BREEZY_GNOME_FEATURES
|
|||
from .settingsmanager import SettingsManager
|
||||
from .shortcutdialog import bind_shortcut_settings
|
||||
from .statemanager import StateManager
|
||||
from .virtualdisplay import VirtualMonitor
|
||||
from .xrdriveripc import XRDriverIPC
|
||||
import gettext
|
||||
import logging
|
||||
|
||||
_ = gettext.gettext
|
||||
logger = logging.getLogger('breezy_ui')
|
||||
|
||||
@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/connected-device.ui')
|
||||
class ConnectedDevice(Gtk.Box):
|
||||
|
|
@ -29,6 +32,7 @@ class ConnectedDevice(Gtk.Box):
|
|||
widescreen_mode_switch = Gtk.Template.Child()
|
||||
widescreen_mode_row = Gtk.Template.Child()
|
||||
curved_display_switch = Gtk.Template.Child()
|
||||
add_virtual_display_button = Gtk.Template.Child()
|
||||
set_toggle_display_distance_start_button = Gtk.Template.Child()
|
||||
set_toggle_display_distance_end_button = Gtk.Template.Child()
|
||||
reassign_toggle_xr_effect_shortcut_button = Gtk.Template.Child()
|
||||
|
|
@ -59,6 +63,7 @@ class ConnectedDevice(Gtk.Box):
|
|||
self.follow_mode_switch,
|
||||
self.follow_threshold_scale,
|
||||
self.curved_display_switch,
|
||||
# self.add_virtual_display_button,
|
||||
self.set_toggle_display_distance_start_button,
|
||||
self.set_toggle_display_distance_end_button,
|
||||
self.movement_look_ahead_scale
|
||||
|
|
@ -92,6 +97,7 @@ class ConnectedDevice(Gtk.Box):
|
|||
self.set_toggle_display_distance_start_button,
|
||||
self.set_toggle_display_distance_end_button
|
||||
])
|
||||
self.add_virtual_display_button.connect('clicked', self.on_add_virtual_display)
|
||||
|
||||
self.state_manager = StateManager.get_instance()
|
||||
self.state_manager.bind_property('follow-mode', self.follow_mode_switch, 'active', GObject.BindingFlags.DEFAULT)
|
||||
|
|
@ -172,6 +178,12 @@ class ConnectedDevice(Gtk.Box):
|
|||
for widget in widgets:
|
||||
widget.connect('clicked', lambda *args, widget=widget: on_set_display_distance_toggle(widget))
|
||||
reload_display_distance_toggle_button(widget)
|
||||
|
||||
def on_add_virtual_display(self, widget):
|
||||
VirtualMonitor(1920, 1080, self.on_virtual_display_ready).create()
|
||||
|
||||
def on_virtual_display_ready(self):
|
||||
logger.info("Virtual display ready")
|
||||
|
||||
def _on_widget_destroy(self, widget):
|
||||
self.state_manager.unbind_property('follow-mode', self.follow_mode_switch, 'active')
|
||||
|
|
|
|||
|
|
@ -84,6 +84,19 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="yes"><!-- adjustment slider -->Virtual monitors</property>
|
||||
<property name="valign">2</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="add_virtual_display_button">
|
||||
<property name="name">add-virtual-display</property>
|
||||
<property name="valign">3</property>
|
||||
<property name="label" translatable="yes">Add</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ breezydesktop_sources = [
|
|||
'shortcutdialog.py',
|
||||
'statemanager.py',
|
||||
'time.py',
|
||||
'virtualdisplay.py',
|
||||
'verify.py',
|
||||
'window.py'
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import signal
|
||||
import pydbus
|
||||
import gi
|
||||
gi.require_version('Gst', '1.0')
|
||||
from gi.repository import GLib, GObject, Gst
|
||||
|
||||
logger = logging.getLogger('breezy_ui')
|
||||
|
||||
screen_cast_iface = 'org.gnome.Mutter.ScreenCast'
|
||||
screen_cast_session_iface = 'org.gnome.Mutter.ScreenCast.Session'
|
||||
screen_cast_stream_iface = 'org.gnome.Mutter.ScreenCast.Session'
|
||||
gst_pipeline_format = "pipewiresrc path=%u ! video/x-raw,max-framerate=60/1,width=%d,height=%d ! fakesink sync=false"
|
||||
|
||||
|
||||
def _screen_cast_session():
|
||||
bus = pydbus.SessionBus()
|
||||
screen_cast = bus.get(screen_cast_iface, '/org/gnome/Mutter/ScreenCast')
|
||||
session_path = screen_cast.CreateSession([])
|
||||
logger.info("session path: %s" % session_path)
|
||||
screen_cast_session = bus.get(screen_cast_iface, session_path)
|
||||
|
||||
return screen_cast_session
|
||||
|
||||
class VirtualMonitor:
|
||||
def __init__(self, width, height, on_ready_cb):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.on_ready_cb = on_ready_cb
|
||||
|
||||
Gst.init(None)
|
||||
|
||||
def create(self):
|
||||
session = _screen_cast_session()
|
||||
stream_path = session.RecordVirtual({
|
||||
'is-platform': GLib.Variant.new_boolean(True),
|
||||
})
|
||||
logger.info("stream path: %s" % stream_path)
|
||||
bus = pydbus.SessionBus()
|
||||
self.stream = bus.get(screen_cast_iface, stream_path)
|
||||
|
||||
self.stream.onPipeWireStreamAdded = self._on_pipewire_stream_added
|
||||
|
||||
session.Start()
|
||||
|
||||
def terminate(self):
|
||||
if self.stream is not None:
|
||||
self.stream.Stop()
|
||||
|
||||
if self.pipeline is not None:
|
||||
self.pipeline.send_event(Gst.Event.new_eos())
|
||||
self.pipeline.set_state(Gst.State.NULL)
|
||||
|
||||
def _on_message(self, bus, message):
|
||||
type = message.type
|
||||
logger.info("message type: %s" % type)
|
||||
if type == Gst.MessageType.EOS or type == Gst.MessageType.ERROR:
|
||||
self.terminate()
|
||||
|
||||
def _on_pipewire_stream_added(self, node_id):
|
||||
logger.info("pipe wire stream added: %u" % node_id)
|
||||
|
||||
self.pipeline = Gst.parse_launch(gst_pipeline_format % (node_id, self.width, self.height))
|
||||
self.pipeline.set_state(Gst.State.PLAYING)
|
||||
self.pipeline.get_bus().connect('message', self._on_message)
|
||||
self.pipeline.set_state(Gst.State.PAUSED)
|
||||
|
||||
self.on_ready_cb()
|
||||
Loading…
Reference in New Issue