From c713c69e66041751fcc6a42e4a34bfe6f6393103 Mon Sep 17 00:00:00 2001
From: wheaney <42350981+wheaney@users.noreply.github.com>
Date: Mon, 17 Jun 2024 10:28:36 -0700
Subject: [PATCH 01/13] Pull in latest sombrero with look-ahead adjustment
---
modules/sombrero | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/sombrero b/modules/sombrero
index 26ece49..17ebe10 160000
--- a/modules/sombrero
+++ b/modules/sombrero
@@ -1 +1 @@
-Subproject commit 26ece497d36bbe2a7445ea2f4e1cce31c35daa3a
+Subproject commit 17ebe10ad9a006cd6a51d5705d6ceddca4464369
From cf66807f856de38e613e0f379b480253a195acd2 Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Tue, 18 Jun 2024 21:58:22 -0700
Subject: [PATCH 02/13] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 934628d..a48286e 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
This repo contains a collection of tools to enable virtual desktop environments for gaming and productivity on Linux using [supported XR glasses](https://github.com/wheaney/XRLinuxDriver#supported-devices).
-There are two installations at the moment:
+There are two installations at the moment. **Note: Only install one of these at a time, as they invalidate each other's installations. This is only temporary.**
* [Breezy GNOME](#breezy-gnome) for desktop support, primarily in GNOME Linux desktop environments
* [Breezy Vulkan](#breezy-vulkan) primarily for gaming but would work with pretty much any application that uses Vulkan rendering.
From b70cf91b55f21a4e0b3e16896cbd1c0552cfa81e Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Tue, 2 Jul 2024 09:29:06 -0700
Subject: [PATCH 03/13] Update README.md
---
README.md | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index a48286e..0af74fe 100644
--- a/README.md
+++ b/README.md
@@ -30,13 +30,12 @@ A workable solution (with some [QoL improvements needed](#upcoming-features)) is
All controls are provided through the Breezy Desktop application. You can also configure keyboard shortcuts for the most common toggle actions. The Breezy Desktop app doesn't have to be running to use the virtual desktop or the keyboard shortcuts once you've configured everything to your liking.
### Upcoming Features
-1. Port to GNOME 43/44
-2. ARM/AARCH64 build
-3. Multiple virtual monitors + multiple physical monitors
-4. Better nested support (clipboard sync, borderless window, snap window to correct display automatically)
-5. SBS display depth
-6. SBS support for 3D content
-7. Port to other popular desktop environments: Cinnamon, KWin
+1. Widescreen + true display depth w/ SBS
+2. Port to GNOME 43/44
+3. ARM/AARCH64 build
+4. Port to KWin Effect (KDE Plasma support)
+5. Multiple virtual monitors + multiple physical monitors
+6. Supported nested or Distrobox deployment
## Breezy Vulkan
From e69d3cb97d252bf52cb01f6ef6d1e0a3194abe53 Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Wed, 3 Jul 2024 14:26:19 -0700
Subject: [PATCH 04/13] Update README.md
---
README.md | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 0af74fe..66d982f 100644
--- a/README.md
+++ b/README.md
@@ -16,9 +16,10 @@ There are two installations at the moment. **Note: Only install one of these at
Breezy GNOME is a virtual workspace solution for Linux desktops that use the GNOME desktop environment (requires GNOME 45+ on an x86_64 system); see [non-GNOME setup](#non-gnome-setup) if you want to try it without a GNOME desktop environment. It currently supports one virtual monitor and multiple physical monitors, but it will soon support multiple virtual monitors. See [upcoming features](#upcoming-features) for more improvements on the horizon.
### GNOME Setup
-1. Download the Breezy GNOME [setup script](https://github.com/wheaney/breezy-desktop/releases/latest/download/breezy_gnome_setup) and set the execute flag (e.g. from the terminal: `chmod +x ~/Downloads/breezy_gnome_setup`)
-2. Run the setup script (e.g. `~/Downloads/breezy_gnome_setup`)
-3. You'll have an application called `Breezy Desktop` installed. Launch that and follow any instructions. You will need to log out and back in at least once to get the GNOME extension working.
+1. Ensure you have the latest graphics drivers installed for your distro.
+2. Download the Breezy GNOME [setup script](https://github.com/wheaney/breezy-desktop/releases/latest/download/breezy_gnome_setup) and set the execute flag (e.g. from the terminal: `chmod +x ~/Downloads/breezy_gnome_setup`)
+3. Run the setup script (e.g. `~/Downloads/breezy_gnome_setup`)
+4. You'll have an application called `Breezy Desktop` installed. Launch that and follow any instructions. You will need to log out and back in at least once to get the GNOME extension working.
### Non-GNOME Setup
A workable solution (with some [QoL improvements needed](#upcoming-features)) is to use your preferred desktop environment with a GNOME window open in nested mode. To do this:
From 7843d4d38531033b7457603cb912639b05ffc8af Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Mon, 8 Jul 2024 15:02:04 -0700
Subject: [PATCH 05/13] Widescreen/SBS support (#28)
Squashed 28 commits
---
bin/breezy_gnome_setup | 12 +-
gnome/bin/dev/use_local_extension.sh | 11 +-
gnome/src/cursormanager.js | 158 ++++--
gnome/src/extension.js | 283 +++++++--
gnome/src/ipc.js | 12 +-
gnome/src/monitormanager.js | 198 ++++++-
gnome/src/time.js | 4 +
gnome/src/xrEffect.js | 291 +++++++---
modules/XRLinuxDriver | 2 +-
modules/sombrero | 2 +-
.../com.xronlinux.BreezyDesktop.gschema.xml | 63 +++
ui/modules/PyXRLinuxDriverIPC | 2 +-
ui/src/connecteddevice.py | 40 +-
ui/src/gtk/connected-device.ui | 535 ++++++++++++------
ui/src/gtk/license-dialog.ui | 4 +-
ui/src/gtk/window.ui | 4 +
ui/src/licensefeaturerow.py | 8 +-
ui/src/licensetierrow.py | 2 +-
ui/src/main.py | 6 +
ui/src/statemanager.py | 6 +
20 files changed, 1230 insertions(+), 413 deletions(-)
diff --git a/bin/breezy_gnome_setup b/bin/breezy_gnome_setup
index 798b3a7..c3714cd 100755
--- a/bin/breezy_gnome_setup
+++ b/bin/breezy_gnome_setup
@@ -18,21 +18,23 @@ tmp_dir=$(mktemp -d -t breezy-gnome-XXXXXXXXXX)
pushd $tmp_dir > /dev/null
echo "Created temp directory: ${tmp_dir}"
-# if the first argument is "-v" then the second argument is metrics version, and the third argument is binary path
-# otherwise, if the first argument is present, it's the binary path
+binary_download_url="https://github.com/wheaney/breezy-desktop/releases/latest/download/breezyGNOME.tar.gz"
if [ "$1" = "-v" ]
then
metrics_version="$2"
binary_path_arg="$3"
+elif [ "$1" = "--tag" ] && [ -n "$2" ]
+then
+ binary_download_url="https://github.com/wheaney/breezy-desktop/releases/download/$2/breezyGNOME.tar.gz"
else
binary_path_arg="$1"
fi
if [ -z "$binary_path_arg" ]
then
- # download and unzip the latest driver
- echo "Downloading latest release to: ${tmp_dir}/breezyGNOME.tar.gz"
- curl -L -O https://github.com/wheaney/breezy-desktop/releases/latest/download/breezyGNOME.tar.gz
+ # download and unzip the binary
+ echo "Downloading to: ${tmp_dir}/breezyGNOME.tar.gz"
+ curl -L -O $binary_download_url
else
if [[ "$binary_path_arg" = /* ]]; then
abs_path="$binary_path_arg"
diff --git a/gnome/bin/dev/use_local_extension.sh b/gnome/bin/dev/use_local_extension.sh
index 54e9577..f959f13 100755
--- a/gnome/bin/dev/use_local_extension.sh
+++ b/gnome/bin/dev/use_local_extension.sh
@@ -6,6 +6,7 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
if [ -z "$XDG_DATA_HOME" ]; then
XDG_DATA_HOME="$USER_HOME/.local/share"
fi
+DATA_DIR="$XDG_DATA_HOME/breezy_gnome"
# if $XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com exists
extension_path="$XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com"
@@ -17,4 +18,12 @@ fi
# recursively copy the $SCRIPT_DIR/../../src to extension_path, don't preserve symlinks
cp -rL $SCRIPT_DIR/../../src $extension_path
-glib-compile-schemas $extension_path/schemas
\ No newline at end of file
+glib-compile-schemas $extension_path/schemas
+
+pushd $extension_path
+GNOME_MANIFEST_LINE=$(find -L . -type f ! -name "*.compiled" -exec sha256sum {} \; | sort | sha256sum | sed 's/ .*//')
+popd
+
+pushd $DATA_DIR
+echo -e "$GNOME_MANIFEST_LINE breezydesktop@xronlinux.com" > manifest
+popd
diff --git a/gnome/src/cursormanager.js b/gnome/src/cursormanager.js
index 545619f..c2f5e82 100644
--- a/gnome/src/cursormanager.js
+++ b/gnome/src/cursormanager.js
@@ -1,4 +1,5 @@
import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.js';
import { MouseSpriteContent } from './cursor.js';
@@ -6,10 +7,9 @@ import Globals from './globals.js';
// Taken from https://github.com/jkitching/soft-brightness-plus
export class CursorManager {
- constructor(mainActor) {
+ constructor(mainActor, refreshRate) {
this._mainActor = mainActor;
-
- this._changeHookFn = null;
+ this._refreshRate = refreshRate;
// Set/destroyed by _enableCloningMouse/_disableCloningMouse
this._cursorWantedVisible = null;
@@ -26,7 +26,7 @@ export class CursorManager {
this._cursorWatch = null;
this._cursorChangedConnection = null;
this._cursorVisibilityChangedConnection = null;
- this._cursorPositionInvalidatedConnection = null;
+ this._redraw_timeline = null;
}
enable() {
@@ -71,11 +71,7 @@ export class CursorManager {
this._cursorSprite.content = new MouseSpriteContent();
this._cursorActor = new Clutter.Actor();
- if (Clutter.Container === undefined) {
- this._cursorActor.add_child(this._cursorSprite);
- } else {
- this._cursorActor.add_actor(this._cursorSprite);
- }
+ this._cursorActor.add_child(this._cursorSprite);
this._cursorWatcher = PointerWatcher.getPointerWatcher();
this._cursorSeat = Clutter.get_default_backend().get_default_seat();
}
@@ -92,6 +88,11 @@ export class CursorManager {
Meta.CursorTracker.prototype.set_pointer_visible = this._cursorTrackerSetPointerVisible;
this._cursorTracker.set_pointer_visible(this._cursorWantedVisible);
+ if (this._cursorSprite) {
+ this._cursorSprite.content = null;
+ if (this._cursorActor) this._cursorActor.remove_child(this._cursorSprite);
+ }
+
this._cursorWantedVisible = null;
this._cursorTracker = null;
this._cursorTrackerSetPointerVisible = null;
@@ -123,23 +124,32 @@ export class CursorManager {
// prereqs: setup in _enableCloningMouse, _cursorWantedVisible is true
_startCloningMouse() {
Globals.logger.log_debug('CursorManager _startCloningMouse');
- if (this._cursorWatch == null) {
- if (Clutter.Container === undefined) {
- this._mainActor.add_child(this._cursorActor);
- } else {
- this._mainActor.add_actor(this._cursorActor);
- }
- this._cursorChangedConnection = this._cursorTracker.connect('cursor-changed', this._updateMouseSprite.bind(this));
- this._cursorVisibilityChangedConnection = this._cursorTracker.connect('visibility-changed', this._updateMouseSprite.bind(this));
- this._cursorPositionInvalidatedConnection = this._cursorTracker.connect('position-invalidated', this._updateMouseSprite.bind(this));
+ this._mainActor.add_child(this._cursorActor);
+ this._cursorChangedConnection = this._cursorTracker.connect('cursor-changed', this._queueSpriteUpdate.bind(this));
+ this._cursorVisibilityChangedConnection = this._cursorTracker.connect('visibility-changed', this._queueVisibilityUpdate.bind(this));
- const interval = 1000 / 250;
- this._cursorWatch = this._cursorWatcher.addWatch(interval, this._updateMousePosition.bind(this));
+ const interval = 1000.0 / this._refreshRate;
+ this._redraw_timeline = Clutter.Timeline.new_for_actor(this._mainActor, interval * 10);
+ this._redraw_timeline.connect('new-frame', (() => {
+ this.handleNewFrame();
+ }).bind(this));
- const [x, y] = global.get_pointer();
- this._updateMousePosition(x, y);
- this._updateMouseSprite();
- }
+ // Some elements will occasionally appear above the cursor, so we periodically reset the actor stacking.
+ // This could theoretically be fixed "better" by attaching to all events that might affect actor ordering,
+ // but finding a comprehensive list is difficult and not future proof. So this ugly solution helps us
+ // catch everything.
+ this._redraw_timeline.connect('completed', (() => {
+ this._periodicReset();
+ }).bind(this));
+
+ this._redraw_timeline.set_repeat_count(-1);
+ this._redraw_timeline.start();
+
+ this._cursorWatch = this._cursorWatcher.addWatch(interval, this._queuePositionUpdate.bind(this));
+
+ const [x, y] = global.get_pointer();
+ this._queuePositionUpdate(x, y);
+ this._queueSpriteUpdate();
if (this._cursorTracker.set_keep_focus_while_hidden) {
this._cursorTracker.set_keep_focus_while_hidden(true);
@@ -163,27 +173,25 @@ export class CursorManager {
if (this._cursorWatch != null) {
this._cursorWatch.remove();
this._cursorWatch = null;
+ }
+ if (this._cursorChangedConnection) {
this._cursorTracker.disconnect(this._cursorChangedConnection);
this._cursorChangedConnection = null;
+ }
+ if (this._cursorVisibilityChangedConnection) {
this._cursorTracker.disconnect(this._cursorVisibilityChangedConnection);
this._cursorVisibilityChangedConnection = null;
-
- this._cursorTracker.disconnect(this._cursorPositionInvalidatedConnection);
- this._cursorPositionInvalidatedConnection = null;
-
- if (Clutter.Container === undefined) {
- this._mainActor.remove_child(this._cursorActor);
- } else {
- this._mainActor.remove_actor(this._cursorActor);
- }
}
- if (this._cursorTracker.set_keep_focus_while_hidden) {
- this._cursorTracker.set_keep_focus_while_hidden(false);
+ if (this._redraw_timeline) {
+ this._redraw_timeline.stop();
+ this._redraw_timeline = null;
}
+ if (this._cursorActor) this._mainActor.remove_child(this._cursorActor);
+
if (this._cursorUnfocusInhibited) {
Globals.logger.log_debug('uninhibit_unfocus');
this._cursorSeat.uninhibit_unfocus();
@@ -191,31 +199,71 @@ export class CursorManager {
}
}
- _updateMousePosition(x, y) {
- this._cursorActor.set_position(x, y);
+ _queuePositionUpdate(x, y) {
+ this._queued_cursor_position = [x, y];
}
- _updateMouseSprite() {
- const sprite = this._cursorTracker.get_sprite();
- if (sprite) {
- this._cursorSprite.content.texture = sprite;
- this._cursorSprite.show();
- } else {
- this._cursorSprite.hide();
- }
+ _queueSpriteUpdate() {
+ this._queued_sprite_update = true;
+ }
- const [xHot, yHot] = this._cursorTracker.get_hot();
- this._cursorSprite.set({
- translation_x: -xHot,
- translation_y: -yHot,
- });
- this._mainActor.set_child_above_sibling(this._cursorActor, null);
+ _queueVisibilityUpdate() {
+ this._queued_visibility_update = true;
this._cursorTrackerSetPointerVisibleBound(false);
+ this._queueSpriteUpdate();
+ }
- // some other processes are uninhibiting when they shouldn't, so we need to re-inhibit here
- if (!this._cursorSeat.is_unfocus_inhibited() && this._cursorUnfocusInhibited) {
- Globals.logger.log_debug('reinhibiting');
- this._cursorSeat.inhibit_unfocus();
+ // updates the stacking and other attributes that are hard to track and may periodically get out of sync
+ _periodicReset() {
+ this._queue_reset = true;
+ this._queueVisibilityUpdate();
+ }
+
+ handleNewFrame() {
+ let redraw = false;
+ if (this._queued_cursor_position) {
+ const [x, y] = this._queued_cursor_position;
+ this._cursorActor.set_position(x, y);
+ this._queued_cursor_position = null;
+ redraw = true;
}
+
+ if (this._queued_sprite_update) {
+ const sprite = this._cursorTracker.get_sprite();
+ if (sprite) {
+ this._cursorSprite.content.texture = sprite;
+ this._cursorSprite.show();
+ } else {
+ this._cursorSprite.hide();
+ }
+
+ const [xHot, yHot] = this._cursorTracker.get_hot();
+ this._cursorSprite.set({
+ translation_x: -xHot,
+ translation_y: -yHot,
+ });
+ this._queued_sprite_update = false;
+ redraw = true;
+ }
+
+ if (this._queued_visibility_update) {
+ this._queued_visibility_update = false;
+ redraw = true;
+ }
+
+ if (this._queue_reset) {
+ if (this._mainActor.get_last_child() !== this._cursorActor)
+ this._mainActor.set_child_above_sibling(this._cursorActor, null);
+
+ // some other processes are uninhibiting when they shouldn't, so we need to re-inhibit here
+ if (!this._cursorSeat.is_unfocus_inhibited() && this._cursorUnfocusInhibited) {
+ Globals.logger.log_debug('reinhibiting');
+ this._cursorSeat.inhibit_unfocus();
+ }
+ this._queue_reset = false;
+ redraw = true;
+ }
+
+ return redraw;
}
}
\ No newline at end of file
diff --git a/gnome/src/extension.js b/gnome/src/extension.js
index 4f56a51..62cdcac 100644
--- a/gnome/src/extension.js
+++ b/gnome/src/extension.js
@@ -8,7 +8,8 @@ import St from 'gi://St';
import { CursorManager } from './cursormanager.js';
import Globals from './globals.js';
import { Logger } from './logger.js';
-import MonitorManager from './monitormanager.js';
+import { MonitorManager } from './monitormanager.js';
+import { isValidKeepAlive } from './time.js';
import { IPC_FILE_PATH, XREffect } from './xrEffect.js';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
@@ -20,8 +21,7 @@ const SUPPORTED_MONITOR_PRODUCTS = [
'Air',
'Air 2', // guessing this one
'Air 2 Pro',
- 'SmartGlasses', // TCL/RayNeo
- 'MetaMonitor' // nested mode dummy monitor
+ 'SmartGlasses' // TCL/RayNeo
];
export default class BreezyDesktopExtension extends Extension {
@@ -40,8 +40,17 @@ export default class BreezyDesktopExtension extends Extension {
this._distance_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._supported_device_detected_connected = 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._optimal_monitor_config_binding = null;
+ this._headset_as_primary_binding = null;
if (!Globals.logger) {
Globals.logger = new Logger({
@@ -59,13 +68,22 @@ export default class BreezyDesktopExtension extends Extension {
Globals.extension_dir = this.path;
this.settings.bind('debug', Globals.logger, 'debug', Gio.SettingsBindFlags.DEFAULT);
- this._monitor_manager = new MonitorManager(this.path);
- this._monitor_manager.setChangeHook(this._setup.bind(this));
+ 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'),
+ extension_path: this.path
+ });
+ 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._setup();
} catch (e) {
- Globals.logger.log(`ERROR: BreezyDesktopExtension enable ${e.message}`, e.stack);
+ Globals.logger.log(`ERROR: BreezyDesktopExtension enable ${e.message}\n${e.stack}`);
}
}
@@ -74,12 +92,21 @@ export default class BreezyDesktopExtension extends Extension {
var target_monitor = this._target_monitor;
var is_effect_running = this._is_effect_running;
this._running_poller_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, (() => {
- if (is_effect_running) return GLib.SOURCE_REMOVE;
+ if (is_effect_running) {
+ this._running_poller_id = undefined;
+ return GLib.SOURCE_REMOVE;
+ }
const is_driver_running = this._check_driver_running();
if (is_driver_running && target_monitor) {
- Globals.logger.log('Driver is running, supported monitor connected. Enabling XR effect.');
- this._effect_enable();
+ // Don't enable the effect yet if monitor updates are needed.
+ // _setup will be triggered again since a !ready result means it will trigger monitor changes,
+ // so we can remove this timeout_add no matter what.
+ if (this._target_monitor_ready(target_monitor)) {
+ Globals.logger.log('Driver is running, supported monitor connected. Enabling XR effect.');
+ this._effect_enable();
+ }
+ this._running_poller_id = undefined;
return GLib.SOURCE_REMOVE;
} else {
return GLib.SOURCE_CONTINUE;
@@ -93,57 +120,113 @@ export default class BreezyDesktopExtension extends Extension {
const target_monitor = this._monitor_manager.getMonitorPropertiesList()?.find(
monitor => SUPPORTED_MONITOR_PRODUCTS.includes(monitor.product));
if (target_monitor !== undefined) {
+ Globals.logger.log_debug(`BreezyDesktopExtension _find_supported_monitor - Identified supported monitor: ${target_monitor.connector}`);
return {
monitor: this._monitor_manager.getMonitors()[target_monitor.index],
- refreshRate: target_monitor.refreshRate,
+ connector: target_monitor.connector,
+ refreshRate: target_monitor.refreshRate
};
}
if (this.settings.get_boolean('developer-mode')) {
+ Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor - Using dummy monitor');
// allow testing XR devices with just USB, no video needed
return {
monitor: this._monitor_manager.getMonitors()[0],
+ connector: 'dummy',
refreshRate: 60,
+ is_dummy: true
};
}
+ Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor - No supported monitor found');
return null;
} catch (e) {
- Globals.logger.log(`ERROR: BreezyDesktopExtension _find_supported_monitor ${e.message}`, e.stack);
+ Globals.logger.log(`ERROR: BreezyDesktopExtension _find_supported_monitor ${e.message}\n${e.stack}`);
return null;
}
}
+ // Assumes target_monitor is set, and was returned by _find_supported_monitor.
+ // A false result means we'll expect _handle_monitor_change to be triggered, so active polling
+ // can be disabled.
+ _target_monitor_ready(target_monitor) {
+ if (target_monitor.is_dummy) {
+ // do this so it updates the state, if needed, but don't worry about the result since
+ // it won't trigger a monitor change
+ this._needs_widescreen_monitor_update();
+ return true;
+ }
+
+ // widescreen check should always be first since it changes the type of monitor, and we wouldn't
+ // want to update the optimal mode if the monitor configs are going to change again
+ return !this._needs_widescreen_monitor_update() &&
+ !this._monitor_manager.needsOptimalModeCheck(target_monitor.connector);
+ }
+
_setup() {
Globals.logger.log_debug('BreezyDesktopExtension _setup');
if (this._is_effect_running) {
- Globals.logger.log('Monitors changed, disabling XR effect');
- this._effect_disable();
+ Globals.logger.log('Reset triggered, disabling XR effect');
+ this._effect_disable(true);
}
const target_monitor = this._find_supported_monitor();
// if target_monitor isn't set, do nothing and wait for MonitorManager to call this again
if (target_monitor && this._running_poller_id === undefined) {
- this._target_monitor = target_monitor.monitor;
- this._refresh_rate = target_monitor.refreshRate;
+ this._target_monitor = target_monitor;
if (this._check_driver_running()) {
- Globals.logger.log('Ready, enabling XR effect');
- this._effect_enable();
+ // 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)) {
+ Globals.logger.log('Ready, enabling XR effect');
+ this._effect_enable();
+ } else {
+ Globals.logger.log_debug('BreezyDesktopExtension _setup - driver running but async monitor action needed');
+ }
} else {
+ Globals.logger.log_debug('BreezyDesktopExtension _setup - driver not running, starting poller');
this._poll_for_ready();
}
+ } else {
+ if (!target_monitor) {
+ Globals.logger.log_debug(`BreezyDesktopExtension _setup - Doing nothing, no supported monitor found`);
+ } else {
+ Globals.logger.log_debug(`BreezyDesktopExtension _setup - Doing nothing, target monitor found, waiting for poller to pick it up`);
+ }
}
}
_check_driver_running() {
try {
if (!Globals.ipc_file) Globals.ipc_file = Gio.file_new_for_path(IPC_FILE_PATH);
- return Globals.ipc_file.query_exists(null);
+ 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);
+
+ // when the driver is running, the IMU file is updated at least 60x per second, do a strict check
+ return isValidKeepAlive(file_modified_time, true);
+ }
} catch (e) {
- Globals.logger.log(`ERROR: BreezyDesktopExtension _check_driver_running ${e.message}`, e.stack);
- return false;
+ 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(['sbs_mode_enabled']);
+ const sbs_enabled = state['sbs_mode_enabled'] === 'true';
+ const widescreen_setting_enabled = this.settings.get_boolean('widescreen-mode');
+ if (widescreen_setting_enabled !== sbs_enabled) {
+ Globals.logger.log_debug('BreezyDesktopExtension _needs_widescreen_monitor_update - true');
+ this._write_control('sbs_mode', widescreen_setting_enabled ? 'enable' : 'disable');
+ return true;
+ }
+
+ return false;
}
_effect_enable() {
@@ -153,23 +236,21 @@ export default class BreezyDesktopExtension extends Extension {
this._is_effect_running = true;
try {
- this._cursor_manager = new CursorManager(Main.layoutManager.uiGroup);
+ const targetMonitor = this._target_monitor.monitor;
+ const refreshRate = targetMonitor.refreshRate ?? 60;
+ this._cursor_manager = new CursorManager(Main.layoutManager.uiGroup, refreshRate);
this._cursor_manager.enable();
- this._overlay = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);'});
+ this._overlay = new St.Bin();
this._overlay.opacity = 255;
- this._overlay.set_position(this._target_monitor.x, this._target_monitor.y);
- this._overlay.set_size(this._target_monitor.width, this._target_monitor.height);
+ this._overlay.set_position(targetMonitor.x, targetMonitor.y);
+ this._overlay.set_size(targetMonitor.width, targetMonitor.height);
const overlayContent = new Clutter.Actor({clip_to_allocation: true});
const uiClone = new Clutter.Clone({ source: Main.layoutManager.uiGroup, clip_to_allocation: true });
- uiClone.x = -this._target_monitor.x;
- uiClone.y = -this._target_monitor.y;
- if (Clutter.Container === undefined) {
- overlayContent.add_child(uiClone);
- } else {
- overlayContent.add_actor(uiClone);
- }
+ uiClone.x = -targetMonitor.x;
+ uiClone.y = -targetMonitor.y;
+ overlayContent.add_child(uiClone);
this._overlay.set_child(overlayContent);
@@ -177,21 +258,31 @@ export default class BreezyDesktopExtension extends Extension {
Shell.util_set_hidden_from_pick(this._overlay, true);
this._xr_effect = new XREffect({
- target_monitor: this._target_monitor,
- target_framerate: this._refresh_rate ?? 60,
+ 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_display_distance(this.settings);
this._update_follow_threshold(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._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._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._overlay.add_effect_with_name('xr-desktop', this._xr_effect);
Meta.disable_unredirect_for_display(global.display);
@@ -200,7 +291,7 @@ export default class BreezyDesktopExtension extends Extension {
this._add_settings_keybinding('toggle-display-distance-shortcut', this._xr_effect._change_distance.bind(this._xr_effect));
this._add_settings_keybinding('toggle-follow-shortcut', this._toggle_follow_mode.bind(this));
} catch (e) {
- Globals.logger.log(`ERROR: BreezyDesktopExtension _effect_enable ${e.message}`, e.stack);
+ Globals.logger.log(`ERROR: BreezyDesktopExtension _effect_enable ${e.message}\n${e.stack}`);
this._effect_disable();
}
}
@@ -231,11 +322,11 @@ export default class BreezyDesktopExtension extends Extension {
bind_to_function
);
} catch (e) {
- Globals.logger.log(`ERROR: BreezyDesktopExtension _add_settings_keybinding settings binding lambda ${e.message}`, e.stack);
+ Globals.logger.log(`ERROR: BreezyDesktopExtension _add_settings_keybinding settings binding lambda ${e.message}\n${e.stack}`);
}
});
} catch (e) {
- Globals.logger.log(`ERROR: BreezyDesktopExtension _add_settings_keybinding ${e.message}`, e.stack);
+ Globals.logger.log(`ERROR: BreezyDesktopExtension _add_settings_keybinding ${e.message}\n${e.stack}`);
}
}
@@ -246,6 +337,27 @@ export default class BreezyDesktopExtension extends Extension {
stream.close(null);
}
+ _read_state(keys) {
+ const state = {};
+ const file = Gio.file_new_for_path('/dev/shm/xr_driver_state');
+ if (file.query_exists(null)) {
+ const data = file.load_contents(null);
+
+ if (data[0]) {
+ const bytes = new Uint8Array(data[1]);
+ const decoder = new TextDecoder();
+ const contents = decoder.decode(bytes);
+
+ const lines = contents.split('\n');
+ for (const line of lines) {
+ const [k, v] = line.split('=');
+ if (keys.includes(k)) state[k] = v;
+ }
+ }
+ }
+ return state;
+ }
+
_update_display_distance(settings, event) {
const value = settings.get_double('display-distance');
Globals.logger.log_debug(`BreezyDesktopExtension _update_display_distance ${value}`);
@@ -258,6 +370,40 @@ export default class BreezyDesktopExtension extends Extension {
if (value !== undefined) this._write_control('breezy_desktop_follow_threshold', value);
}
+ _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._write_control('sbs_mode', value ? 'enable' : 'disable');
+ else
+ Globals.logger.log_debug('effect.widescreen_mode_state already matched setting');
+ }
+
+ _update_widescreen_mode_from_state(effect, _pspec) {
+ const value = effect.widescreen_mode_state;
+ Globals.logger.log_debug(`BreezyDesktopExtension _update_widescreen_mode_from_state ${value}`);
+ if (value !== this.settings.get_boolean('widescreen-mode'))
+ this.settings.set_boolean('widescreen-mode', value);
+ else
+ Globals.logger.log_debug('settings.widescreen-mode already matched state');
+ }
+
+ _handle_monitor_change() {
+ Globals.logger.log('Monitor change detected');
+ this._setup();
+ }
+
+ _handle_supported_device_change(effect, _pspec) {
+ const value = effect.supported_device_detected;
+ Globals.logger.log_debug(`BreezyDesktopExtension _handle_supported_device_change ${value}`);
+
+ // this will disable the effect and begin polling for a ready state again
+ if (!value && this._is_effect_running) {
+ Globals.logger.log('Supported device disconnected');
+ this._setup();
+ }
+ }
+
_recenter_display() {
Globals.logger.log_debug('BreezyDesktopExtension _recenter_display');
this._write_control('recenter_screen', 'true');
@@ -268,12 +414,16 @@ export default class BreezyDesktopExtension extends Extension {
this._write_control('toggle_breezy_desktop_smooth_follow', 'true');
}
- _effect_disable() {
+ // for_setup should be true if our intention is to immediately re-enable the extension
+ _effect_disable(for_setup = false) {
try {
Globals.logger.log_debug('BreezyDesktopExtension _effect_disable');
this._is_effect_running = false;
- if (this._running_poller_id) GLib.source_remove(this._running_poller_id);
+ if (this._running_poller_id) {
+ GLib.source_remove(this._running_poller_id);
+ this._running_poller_id = undefined;
+ }
Main.wm.removeKeybinding('recenter-display-shortcut');
Main.wm.removeKeybinding('toggle-display-distance-shortcut');
@@ -281,12 +431,13 @@ export default class BreezyDesktopExtension extends Extension {
Meta.enable_unredirect_for_display(global.display);
if (this._overlay) {
- global.stage.remove_child(this._overlay);
+ if (this._xr_effect) this._xr_effect.cleanup();
this._overlay.remove_effect_by_name('xr-desktop');
+
+ global.stage.remove_child(this._overlay);
this._overlay.destroy();
this._overlay = null;
}
-
if (this._distance_binding) {
this.settings.unbind(this._distance_binding);
this._distance_binding = null;
@@ -299,6 +450,10 @@ export default class BreezyDesktopExtension extends Extension {
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;
@@ -307,17 +462,46 @@ export default class BreezyDesktopExtension extends Extension {
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._xr_effect) {
- this._xr_effect.cleanup();
+ 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_connected) {
+ this._xr_effect.disconnect(this._supported_device_detected_connected);
+ this._supported_device_detected_connected = null;
+ }
this._xr_effect = null;
}
-
if (this._cursor_manager) {
this._cursor_manager.disable();
this._cursor_manager = null;
}
+
+ // this should always be done at the end of this function after the widescreen settings binding is removed,
+ // so it doesn't reset the setting to false
+ if (!for_setup && this.settings.get_boolean('widescreen-mode')) {
+ Globals.logger.log('Disabling SBS mode due to disabling effect');
+ this._write_control('sbs_mode', 'disable');
+ }
} catch (e) {
- Globals.logger.log(`ERROR: BreezyDesktopExtension _effect_disable ${e.message}`, e.stack);
+ Globals.logger.log(`ERROR: BreezyDesktopExtension _effect_disable ${e.message}\n${e.stack}`);
}
}
@@ -327,11 +511,20 @@ export default class BreezyDesktopExtension extends Extension {
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);
+ 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;
}
} catch (e) {
- Globals.logger.log(`ERROR: BreezyDesktopExtension disable ${e.message}`, e.stack);
+ Globals.logger.log(`ERROR: BreezyDesktopExtension disable ${e.message}\n${e.stack}`);
}
}
}
diff --git a/gnome/src/ipc.js b/gnome/src/ipc.js
index 77d8fa1..a729368 100644
--- a/gnome/src/ipc.js
+++ b/gnome/src/ipc.js
@@ -24,7 +24,7 @@ export function dataViewBigUint(dataView, dataViewInfo) {
return Number(dataView.getBigUint64(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true));
}
-export function dataViewUintArray(dataView, dataViewInfo) {
+export function dataViewUint32Array(dataView, dataViewInfo) {
const uintArray = []
let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX];
for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) {
@@ -34,6 +34,16 @@ export function dataViewUintArray(dataView, dataViewInfo) {
return uintArray;
}
+export function dataViewUint8Array(dataView, dataViewInfo) {
+ const uintArray = []
+ let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX];
+ for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_SIZE_INDEX] * dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) {
+ uintArray.push(dataView.getUint8(offset));
+ offset += UINT8_SIZE;
+ }
+ return uintArray;
+}
+
export function dataViewFloat(dataView, dataViewInfo) {
return dataView.getFloat32(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true);
}
diff --git a/gnome/src/monitormanager.js b/gnome/src/monitormanager.js
index 0eacea5..8126ecf 100644
--- a/gnome/src/monitormanager.js
+++ b/gnome/src/monitormanager.js
@@ -17,6 +17,7 @@
// along with this program. If not, see .
import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
@@ -52,10 +53,10 @@ export function newDisplayConfig(extPath, callback) {
);
}
-export function getMonitorConfig(displayConfigProxy, callback) {
- displayConfigProxy.GetResourcesRemote((result) => {
- if (result.length <= 2) {
- callback(null, 'Cannot get DisplayConfig: No outputs in GetResources()');
+function getMonitorConfig(displayConfigProxy, callback) {
+ displayConfigProxy.GetResourcesRemote((result, error) => {
+ if (error) {
+ callback(null, `GetResourcesRemote failed: ${error}`);
} else {
const monitors = [];
for (let i = 0; i < result[2].length; i++) {
@@ -84,32 +85,163 @@ export function getMonitorConfig(displayConfigProxy, callback) {
});
}
+// triggers callback with true result if an an async monitor config change was triggered, false if no config change needed
+function performOptimalModeCheck(displayConfigProxy, connectorName, headsetAsPrimary, callback) {
+ Globals.logger.log_debug(`monitormanager.js performOptimalModeCheck for ${connectorName}`);
+ displayConfigProxy.GetCurrentStateRemote((result, error) => {
+ if (error) {
+ callback(null, `GetCurrentState failed: ${error}`);
+ } else {
+ Globals.logger.log_debug(`monitormanager.js performOptimalModeCheck GetCurrentState result: ${JSON.stringify(result)}`);
+ const [serial, monitors, logicalMonitors, properties] = result;
+
+ // iterate over all monitors at least once, collecting the best fit mode for our monitor, and mode information
+ // for each monitor
+ let ourMonitor = undefined;
+ let monitorToModeIdMap = {};
+ let bestFitMode = undefined;
+ for (let monitor of monitors) {
+ const [details, modes, monProperties] = monitor;
+ const [connector, vendor, product, monitorSerial] = details;
+ const isOurMonitor = connector == connectorName;
+ if (isOurMonitor) ourMonitor = monitor;
+
+ for (let mode of modes) {
+ const [modeId, width, height, refreshRate, preferredScale, supportedScales, modeProperites] = mode;
+ const isCurrent = !!modeProperites['is-current'];
+ if (isCurrent) monitorToModeIdMap[connector] = modeId;
+
+ if (isOurMonitor && (!bestFitMode || (
+ width >= bestFitMode.width &&
+ height >= bestFitMode.height &&
+ refreshRate >= bestFitMode.refreshRate))) {
+ // find the scale that is closest to 1.0
+ const bestScale = supportedScales.reduce((prev, curr) => {
+ return Math.abs(curr - 1.0) < Math.abs(prev - 1.0) ? curr : prev;
+ });
+
+ bestFitMode = {
+ modeId,
+ width,
+ height,
+ refreshRate,
+ bestScale,
+ isCurrent
+ };
+ }
+ }
+ }
+
+ if (!!ourMonitor) {
+ let anyMonitorsChanged = false;
+ if (!!bestFitMode) {
+ // map from original logical monitors schema to a(iiduba(ssa{sv})) for ApplyMonitorsConfig call
+ const updatedLogicalMonitors = logicalMonitors.map((logicalMonitor) => {
+ const [x, y, scale, transform, primary, monitors, logMonProperties] = logicalMonitor;
+ const hasOurMonitor = !!monitors.some((monitor) => monitor[0] === connectorName);
+ anyMonitorsChanged |= hasOurMonitor && bestFitMode.bestScale !== scale;
+
+ // there can only be one primary monitor, so we need to set all other monitors to not primary and glasses to primary,
+ // if headsetAsPrimary is true
+ anyMonitorsChanged |= headsetAsPrimary && ((hasOurMonitor && !primary) || (!hasOurMonitor && primary));
+ return [
+ x,
+ y,
+ hasOurMonitor ? bestFitMode.bestScale : scale,
+ transform,
+ headsetAsPrimary ? hasOurMonitor : primary,
+ monitors.map((monitor) => {
+ const monitorConnector = monitor[0];
+ const isOurMonitor = monitorConnector === connectorName;
+ anyMonitorsChanged |= isOurMonitor && !bestFitMode.isCurrent;
+ return [
+ monitorConnector,
+ isOurMonitor ? bestFitMode.modeId : monitorToModeIdMap[monitorConnector],
+ {} // properties
+ ];
+ })
+ ];
+ });
+
+ // if our monitor is already properly configured, we can skip the ApplyMonitorsConfig call
+ if (anyMonitorsChanged) {
+ Globals.logger.log_debug(`monitormanager.js performOptimalModeCheck updatedLogicalMonitors: ${JSON.stringify(updatedLogicalMonitors)}`);
+ displayConfigProxy.ApplyMonitorsConfigRemote(
+ serial,
+ 1, // "temporary" config -- "permanent" might be pointless since we always do this check
+ updatedLogicalMonitors,
+ {}, // properties
+ (_result, error) => {
+ if (error) {
+ callback(null, `ApplyMonitorsConfig failed: ${error}`);
+ } else {
+ callback(true, null);
+ }
+ }
+ );
+ }
+ }
+ if (!anyMonitorsChanged) callback(false, null);
+ } else {
+ callback(null, `Monitor ${connectorName} not found in GetCurrentState result`);
+ }
+ }
+ });
+}
+
// Monitor change handling
-export default class MonitorManager {
- constructor(extPath) {
- this._extPath = extPath;
+export const MonitorManager = GObject.registerClass({
+ Properties: {
+ 'use-optimal-monitor-config': GObject.ParamSpec.boolean(
+ 'use-optimal-monitor-config',
+ 'Use optimal monitor configuration',
+ 'Automatically set the optimal monitor configuration upon connection',
+ GObject.ParamFlags.READWRITE,
+ true
+ ),
+ 'headset-as-primary': GObject.ParamSpec.boolean(
+ 'headset-as-primary',
+ 'Use headset as primary monitor',
+ 'Automatically set the headset as the primary display upon connection',
+ GObject.ParamFlags.READWRITE,
+ true
+ ),
+ 'extension-path': GObject.ParamSpec.string(
+ 'extension-path',
+ 'Extension path',
+ 'Path to the extension directory',
+ GObject.ParamFlags.READWRITE,
+ ''
+ )
+ }
+}, class MonitorManager extends GObject.Object {
+ constructor(params = {}) {
+ super(params);
this._monitorsChangedConnection = null;
this._displayConfigProxy = null;
this._backendManager = null;
this._monitorProperties = null;
this._changeHookFn = null;
+ this._needsConfigCheck = this.use_optimal_monitor_config;
}
enable() {
+ Globals.logger.log_debug('MonitorManager enable');
this._backendManager = global.backend.get_monitor_manager();
- newDisplayConfig(this._extPath, (proxy, error) => {
+ newDisplayConfig(this.extension_path, ((proxy, error) => {
if (error) {
return;
}
this._displayConfigProxy = proxy;
this._on_monitors_change();
- });
+ }).bind(this));
this._monitorsChangedConnection = Main.layoutManager.connect('monitors-changed', this._on_monitors_change.bind(this));
}
disable() {
+ Globals.logger.log_debug('MonitorManager disable');
Main.layoutManager.disconnect(this._monitorsChangedConnection);
this._monitorsChangedConnection = null;
@@ -123,10 +255,6 @@ export default class MonitorManager {
this._changeHookFn = fn;
}
- setPostCallback(callback) {
- this._postCallback = callback;
- }
-
getMonitors() {
return Main.layoutManager.monitors;
}
@@ -135,11 +263,45 @@ export default class MonitorManager {
return this._monitorProperties;
}
+ // returns true if a check is needed, caller should wait for the next change hook call
+ needsOptimalModeCheck(monitorConnector) {
+ Globals.logger.log_debug(`MonitorManager checkOptimalMode: ${monitorConnector}`);
+ if (this._displayConfigProxy == null) {
+ Globals.logger.log('MonitorManager checkOptimalMode: _displayConfigProxy not set!');
+ return false;
+ }
+
+ if (this._needsConfigCheck) {
+ performOptimalModeCheck(this._displayConfigProxy, monitorConnector, this.headset_as_primary, ((configChanged, error) => {
+ this._needsConfigCheck = false;
+ if (error) {
+ Globals.logger.log(`Failed to switch to optimal mode for monitor ${monitorConnector}: ${error}`);
+ } else {
+ if (configChanged) {
+ Globals.logger.log(`Switched to optimal mode for monitor ${monitorConnector}`);
+ } else if (!!this._changeHookFn) {
+ Globals.logger.log_debug('MonitorManager checkOptimalMode: no config change');
+
+ // no config change means this won't be triggered automatically, so trigger it manually
+ this._changeHookFn();
+ } else {
+ Globals.logger.log('MonitorManager checkOptimalMode: can\'t trigger change hook, no hook set!');
+ }
+ }
+ }).bind(this));
+ } else {
+ Globals.logger.log_debug('MonitorManager checkOptimalMode: skipping config check');
+ }
+ return this._needsConfigCheck;
+ }
+
_on_monitors_change() {
+ Globals.logger.log_debug('MonitorManager _on_monitors_change');
if (this._displayConfigProxy == null) {
return;
}
- getMonitorConfig(this._displayConfigProxy, (result, error) => {
+ this._needsConfigCheck = this.use_optimal_monitor_config;
+ getMonitorConfig(this._displayConfigProxy, ((result, error) => {
if (error) {
return;
}
@@ -161,9 +323,11 @@ export default class MonitorManager {
}
}
this._monitorProperties = monitorProperties;
- if (this._changeHookFn !== null) {
+ if (!!this._changeHookFn) {
this._changeHookFn();
+ } else {
+ Globals.logger.log('MonitorManager _on_monitors_change: can\'t trigger change hook, no hook set!');
}
- });
+ }).bind(this));
}
-}
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/gnome/src/time.js b/gnome/src/time.js
index 6c3259c..7883b9b 100644
--- a/gnome/src/time.js
+++ b/gnome/src/time.js
@@ -4,4 +4,8 @@ export function getEpochSec() {
export function toSec(milliseconds) {
return Math.floor(milliseconds / 1000);
+}
+
+export function isValidKeepAlive(dateSec, strictCheck = false) {
+ return Math.abs(toSec(Date.now()) - dateSec) <= (strictCheck ? 1 : 5);
}
\ No newline at end of file
diff --git a/gnome/src/xrEffect.js b/gnome/src/xrEffect.js
index 0c0b15b..2680cae 100644
--- a/gnome/src/xrEffect.js
+++ b/gnome/src/xrEffect.js
@@ -11,7 +11,8 @@ import {
dataViewEnd,
dataViewUint8,
dataViewBigUint,
- dataViewUintArray,
+ dataViewUint32Array,
+ dataViewUint8Array,
dataViewFloat,
dataViewFloatArray,
BOOL_SIZE,
@@ -23,12 +24,12 @@ import {
} from "./ipc.js";
import { degreeToRadian } from "./math.js";
import { getShaderSource } from "./shader.js";
-import { toSec } from "./time.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 = 2;
+const DATA_LAYOUT_VERSION = 3;
// DataView info: [offset, size, count]
const VERSION = [0, UINT8_SIZE, 1];
@@ -41,7 +42,8 @@ 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 DATA_VIEW_LENGTH = dataViewEnd(IMU_QUAT_DATA);
+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 = {
@@ -50,20 +52,23 @@ const shaderUniformLocations = {
'imu_quat_data': null,
'look_ahead_cfg': null,
'look_ahead_ms': null,
- 'stage_aspect_ratio': null,
- 'display_aspect_ratio': null,
'trim_width_percent': null,
'trim_height_percent': null,
- 'display_zoom': null,
+ 'display_size': null,
'display_north_offset': null,
- 'lens_distance_ratio': 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,
- 'sbs_content': null,
'custom_banner_enabled': null,
'half_fov_z_rads': null,
'half_fov_y_rads': null,
- 'screen_distance': null,
- 'display_res': null
+ 'fov_half_widths': null,
+ 'fov_widths': null,
+ 'display_resolution': null,
+ 'source_to_display_ratio': null,
+ 'curved_display': null
};
function setUniformFloat(effect, locationName, dataViewInfo, value) {
@@ -109,67 +114,121 @@ function lookAheadMS(dataView) {
// most uniforms don't change frequently, this function should be called periodically
function setIntermittentUniformVariables() {
- const dataView = this._dataView;
+ try {
+ const dataView = this._dataView;
- if (dataView.byteLength === DATA_VIEW_LENGTH) {
- const version = dataViewUint8(dataView, VERSION);
- const imuDateMS = dataViewBigUint(dataView, EPOCH_MS);
- const currentDateMS = Date.now();
- const validKeepalive = Math.abs(toSec(currentDateMS) - toSec(imuDateMS)) < 5;
- 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;
+ 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;
- if (enabled) {
- const displayRes = dataViewUintArray(dataView, DISPLAY_RES);
- const displayFov = dataViewFloat(dataView, DISPLAY_FOV);
- const lensDistanceRatio = dataViewFloat(dataView, LENS_DISTANCE_RATIO);
+ if (enabled) {
+ const displayFov = dataViewFloat(dataView, DISPLAY_FOV);
- // compute these values once, they only change when the XR device changes
- const displayAspectRatio = displayRes[0] / displayRes[1];
- const stageAspectRatio = this.target_monitor.width / this.target_monitor.height;
- const diagToVertRatio = Math.sqrt(Math.pow(stageAspectRatio, 2) + 1);
- const halfFovZRads = degreeToRadian(displayFov / diagToVertRatio) / 2;
- const halfFovYRads = halfFovZRads * stageAspectRatio;
- const screenDistance = 1.0 - lensDistanceRatio;
+ // TODO - drive these values from settings
+ const sbsContent = false;
+ const sbsModeStretched = true;
- // our overlay doesn't quite cover the full screen texture, which allows us to see some of the real desktop
- // underneath, so we trim two pixels around the entire edge of the texture
- const trimWidthPercent = 3.0 / this.target_monitor.width;
- const trimHeightPercent = 3.0 / this.target_monitor.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);
+ // 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];
+ let texcoordXLimits = [0.0, 1.0];
+ let texcoordXLimitsRight = [0.0, 1.0];
+ if (sbsEnabled) {
+ 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;
+ }
+ }
+ const lensDistanceRatio = dataViewFloat(dataView, LENS_DISTANCE_RATIO);
+ const lensFromCenter = lensDistanceRatio / 3.0;
+ const lensVector = [lensDistanceRatio, lensFromCenter, 0.0];
+ const lensVectorRight = [lensDistanceRatio, -lensFromCenter, 0.0];
- // computed values with no dataViewInfo, so we set these manually
- setSingleFloat(this, 'stage_aspect_ratio', stageAspectRatio);
- setSingleFloat(this, 'display_aspect_ratio', displayAspectRatio);
- setSingleFloat(this, 'trim_width_percent', trimWidthPercent);
- setSingleFloat(this, 'trim_height_percent', trimHeightPercent);
- setSingleFloat(this, 'half_fov_z_rads', halfFovZRads);
- setSingleFloat(this, 'half_fov_y_rads', halfFovYRads);
- setSingleFloat(this, 'screen_distance', screenDistance);
+ // 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;
+
+ // 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);
- // TOOD - drive from settings
- setSingleFloat(this, 'display_zoom', 1.0);
+ // computed values with no dataViewInfo, so we set these manually
+ setSingleFloat(this, 'trim_width_percent', trimWidthPercent);
+ setSingleFloat(this, 'trim_height_percent', 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);
+ 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['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, '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);
+
+ 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]]);
+ } else if (dataView.byteLength !== 0) {
+ throw new Error(`Invalid dataView.byteLength: ${dataView.byteLength} !== ${DATA_VIEW_LENGTH}`);
}
-
- // these variables are always in play, even if enabled is false
- setSingleFloat(this, 'enabled', enabled ? 1.0 : 0.0);
- setSingleFloat(this, 'show_banner', imuResetState ? 1.0 : 0.0);
- setSingleFloat(this, 'sbs_content', 0.0); // TOOD - drive from settings
- setSingleFloat(this, 'sbs_enabled', dataViewUint8(dataView, SBS_ENABLED) !== 0 ? 1.0 : 0.0);
- setSingleFloat(this, 'custom_banner_enabled', dataViewUint8(dataView, CUSTOM_BANNER_ENABLED) !== 0 ? 1.0 : 0.0);
-
- this.set_uniform_float(shaderUniformLocations['display_res'], 2, [this.target_monitor.width, this.target_monitor.height]);
- } else if (dataView.byteLength !== 0) {
- Globals.logger.log(`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',
@@ -191,6 +250,15 @@ export const XREffect = GObject.registerClass({
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',
@@ -208,14 +276,42 @@ export const XREffect = GObject.registerClass({
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._frametime = Math.floor(1000 / this.target_framerate);
-
this._is_display_distance_at_end = false;
this._distance_ease_timeline = null;
@@ -255,14 +351,11 @@ export const XREffect = GObject.registerClass({
}
vfunc_paint_target(node, paintContext) {
- var now = Date.now();
- var lastPaint = this._last_paint || 0;
- var frametime = this._frametime;
var calibratingImage = this.calibratingImage;
var customBannerImage = this.customBannerImage;
- const data = Globals.ipc_file.load_contents(null);
+ let data = Globals.ipc_file.load_contents(null);
if (data[0]) {
- const buffer = new Uint8Array(data[1]).buffer;
+ let buffer = new Uint8Array(data[1]).buffer;
this._dataView = new DataView(buffer);
if (!this._initialized) {
this.set_uniform_float(this.get_uniform_location('uDesktopTexture'), 1, [0]);
@@ -278,10 +371,12 @@ export const XREffect = GObject.registerClass({
this.setIntermittentUniformVariables = setIntermittentUniformVariables.bind(this);
this.setIntermittentUniformVariables();
- this._redraw_timeout_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._frametime, () => {
- if ((now - lastPaint) > frametime) global.stage.queue_redraw();
- return GLib.SOURCE_CONTINUE;
- });
+ 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();
@@ -291,30 +386,48 @@ export const XREffect = GObject.registerClass({
this._initialized = true;
}
- if (this._dataView.byteLength === DATA_VIEW_LENGTH) {
- setSingleFloat(this, 'display_north_offset', this.display_distance);
- setSingleFloat(this, 'look_ahead_ms', lookAheadMS(this._dataView));
- setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA);
- } else if (this._dataView.byteLength !== 0) {
- Globals.logger.log(`ERROR: Invalid dataView.byteLength: ${this._dataView.byteLength} !== ${DATA_VIEW_LENGTH}`)
+ 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);
+ }
+ }
}
- // 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);
- } else {
- super.vfunc_paint_target(node, paintContext);
+ 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
+ );
+ }
}
- this._last_paint = now;
+ super.vfunc_paint_target(node, paintContext);
}
cleanup() {
- if (this._redraw_timeout_id) GLib.source_remove(this._redraw_timeout_id);
+ if (this._redraw_timeline) {
+ this._redraw_timeline.stop();
+ this._redraw_timeline = null;
+ }
if (this._uniforms_timeout_id) GLib.source_remove(this._uniforms_timeout_id);
}
});
\ No newline at end of file
diff --git a/modules/XRLinuxDriver b/modules/XRLinuxDriver
index f45b8fb..b97faa8 160000
--- a/modules/XRLinuxDriver
+++ b/modules/XRLinuxDriver
@@ -1 +1 @@
-Subproject commit f45b8fbaa7ef0b3f824a3452a91dc74bffb6ea38
+Subproject commit b97faa82a7767e4270e46886d68dd1c70b71abca
diff --git a/modules/sombrero b/modules/sombrero
index 17ebe10..ec3f82a 160000
--- a/modules/sombrero
+++ b/modules/sombrero
@@ -1 +1 @@
-Subproject commit 17ebe10ad9a006cd6a51d5705d6ceddca4464369
+Subproject commit ec3f82aa4834847adf1f0c640eba4b87b6f3ff65
diff --git a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
index a1babc3..7b33ad7 100644
--- a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
+++ b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
@@ -64,6 +64,69 @@
End distance when using the "toggle display distance" shortcut.
+
+
+ false
+
+ Widescreen mode
+
+ Enable widescreen/SBS mode
+
+
+
+
+ 1.0
+
+ Display size
+
+ The size of the display
+
+
+
+
+ false
+
+ Curved display
+
+ Enable curved display mode
+
+
+
+
+ -1
+
+ Look-ahead override
+
+ Manually override the look-ahead calculation
+
+
+
+
+ true
+
+ Use optimal monitor configuration
+
+ Automatically set the optimal monitor configuration upon connection
+
+
+
+
+ true
+
+ Headset as primary
+
+ Automatically set the headset as the primary display upon connection
+
+
+
+
+ false
+
+ Disable anti-aliasing
+
+ Disable anti-aliasing
+
+
false
diff --git a/ui/modules/PyXRLinuxDriverIPC b/ui/modules/PyXRLinuxDriverIPC
index 50ce02f..34a349d 160000
--- a/ui/modules/PyXRLinuxDriverIPC
+++ b/ui/modules/PyXRLinuxDriverIPC
@@ -1 +1 @@
-Subproject commit 50ce02fc9e341d417785bd26abeee9f7305bee6c
+Subproject commit 34a349d39efc02f4b65550debd193d29d95a84f9
diff --git a/ui/src/connecteddevice.py b/ui/src/connecteddevice.py
index 16f9ad4..241c4d5 100644
--- a/ui/src/connecteddevice.py
+++ b/ui/src/connecteddevice.py
@@ -10,13 +10,17 @@ from .xrdriveripc import XRDriverIPC
class ConnectedDevice(Gtk.Box):
__gtype_name__ = "ConnectedDevice"
+ device_label = Gtk.Template.Child()
effect_enable_switch = Gtk.Template.Child()
display_distance_scale = Gtk.Template.Child()
display_distance_adjustment = Gtk.Template.Child()
+ display_size_scale = Gtk.Template.Child()
+ display_size_adjustment = Gtk.Template.Child()
follow_threshold_scale = Gtk.Template.Child()
follow_threshold_adjustment = Gtk.Template.Child()
follow_mode_switch = Gtk.Template.Child()
- device_label = Gtk.Template.Child()
+ widescreen_mode_switch = Gtk.Template.Child()
+ curved_display_switch = Gtk.Template.Child()
set_toggle_display_distance_start_button = Gtk.Template.Child()
set_toggle_display_distance_end_button = Gtk.Template.Child()
reassign_recenter_display_shortcut_button = Gtk.Template.Child()
@@ -25,19 +29,30 @@ class ConnectedDevice(Gtk.Box):
toggle_display_distance_shortcut_label = Gtk.Template.Child()
reassign_toggle_follow_shortcut_button = Gtk.Template.Child()
toggle_follow_shortcut_label = Gtk.Template.Child()
+ headset_as_primary_switch = Gtk.Template.Child()
+ use_optimal_monitor_config_switch = Gtk.Template.Child()
+ movement_look_ahead_scale = Gtk.Template.Child()
+ movement_look_ahead_adjustment = Gtk.Template.Child()
+
def __init__(self):
super(Gtk.Box, self).__init__()
self.init_template()
self.all_enabled_state_inputs = [
self.display_distance_scale,
+ self.display_size_scale,
self.follow_mode_switch,
self.follow_threshold_scale,
+ self.widescreen_mode_switch,
+ self.curved_display_switch,
self.set_toggle_display_distance_start_button,
self.set_toggle_display_distance_end_button,
self.reassign_recenter_display_shortcut_button,
self.reassign_toggle_display_distance_shortcut_button,
- self.reassign_toggle_follow_shortcut_button
+ self.reassign_toggle_follow_shortcut_button,
+ self.headset_as_primary_switch,
+ self.use_optimal_monitor_config_switch,
+ self.movement_look_ahead_scale
]
self.settings = SettingsManager.get_instance().settings
@@ -45,7 +60,13 @@ class ConnectedDevice(Gtk.Box):
self.extensions_manager = ExtensionsManager.get_instance()
self.settings.bind('display-distance', self.display_distance_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.bind('display-size', self.display_size_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('follow-threshold', self.follow_threshold_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.bind('widescreen-mode', self.widescreen_mode_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.bind('curved-display', self.curved_display_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.bind('headset-as-primary', self.headset_as_primary_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.bind('use-optimal-monitor-config', self.use_optimal_monitor_config_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.bind('look-ahead-override', self.movement_look_ahead_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
bind_shortcut_settings(self.get_parent(), [
[self.reassign_recenter_display_shortcut_button, self.recenter_display_shortcut_label],
@@ -68,8 +89,11 @@ class ConnectedDevice(Gtk.Box):
self.effect_enable_switch.set_active(self._is_config_enabled(self.ipc.retrieve_config()) and self.extensions_manager.is_enabled())
self.effect_enable_switch.connect('notify::active', self._refresh_inputs_for_enabled_state)
+ self.use_optimal_monitor_config_switch.connect('notify::active', self._refresh_use_optimal_monitor_config)
+
self._handle_enabled_features(self.state_manager, None)
self._refresh_inputs_for_enabled_state(self.effect_enable_switch, None)
+ self._refresh_use_optimal_monitor_config(self.use_optimal_monitor_config_switch, None)
self.extensions_manager.bind_property('breezy-enabled', self.effect_enable_switch, 'active', GObject.BindingFlags.BIDIRECTIONAL)
self.connect("destroy", self._on_widget_destroy)
@@ -98,7 +122,8 @@ class ConnectedDevice(Gtk.Box):
for widget in self.all_enabled_state_inputs:
widget.set_sensitive(requesting_enabled)
- if requesting_enabled: self._refresh_follow_mode(self.follow_mode_switch, None)
+ if requesting_enabled:
+ self._refresh_follow_mode(self.follow_mode_switch, None)
def _refresh_follow_mode(self, switch, param):
self.follow_threshold_scale.set_sensitive(switch.get_active())
@@ -109,6 +134,11 @@ class ConnectedDevice(Gtk.Box):
'enable_breezy_desktop_smooth_follow': switch.get_active()
})
+ def _refresh_use_optimal_monitor_config(self, switch, param):
+ self.headset_as_primary_switch.set_sensitive(switch.get_active())
+ if not switch.get_active():
+ self.headset_as_primary_switch.set_active(False)
+
def set_device_name(self, name):
self.device_label.set_markup(f"{name}")
@@ -120,7 +150,9 @@ class ConnectedDevice(Gtk.Box):
def _on_widget_destroy(self, widget):
self.state_manager.unbind_property('follow-mode', self.follow_mode_switch, 'active')
self.settings.unbind('display-distance', self.display_distance_adjustment, 'value')
+ self.settings.unbind('display-size', self.display_size_adjustment, 'value')
self.settings.unbind('follow-threshold', self.follow_threshold_adjustment, 'value')
+ self.settings.unbind('widescreen-mode', self.widescreen_mode_switch, 'active')
self.extensions_manager.unbind_property('breezy-enabled', self.effect_enable_switch, 'active')
def reload_display_distance_toggle_button(widget):
@@ -131,4 +163,4 @@ def on_set_display_distance_toggle(widget):
settings = SettingsManager.get_instance().settings
distance = settings.get_double('display-distance')
settings.set_double(widget.get_name(), distance)
- reload_display_distance_toggle_button(widget)
\ No newline at end of file
+ reload_display_distance_toggle_button(widget)
diff --git a/ui/src/gtk/connected-device.ui b/ui/src/gtk/connected-device.ui
index 5617700..215ca1c 100644
--- a/ui/src/gtk/connected-device.ui
+++ b/ui/src/gtk/connected-device.ui
@@ -24,201 +24,364 @@
-
+
+
+ shortcuts
+ Keyboard Shortcuts
+ preferences-desktop-keyboard-shortcuts-symbolic
+
+
+
+
+ Keyboard Shortcuts
+
+
+ Re-center display shortcut
+ Pin the virtual display to the current position.
+ 2
+
+
+ 30
+ 30
+
+
+ 3
+
+
+
+
+
+
+ recenter-display-shortcut
+ 3
+ Change
+
+
+
+
+
+
+
+
+ Display distance shortcut
+ Quickly toggle between two predefined distances.
+ 2
+
+
+ 30
+ 30
+
+
+ 3
+
+
+
+
+
+
+ toggle-display-distance-shortcut
+ 3
+ Change
+
+
+
+
+
+
+
+
+ Toggle follow mode shortcut
+ Quickly toggle follow mode.
+ 2
+
+
+ 30
+ 30
+
+
+ 3
+
+
+
+
+
+
+ toggle-follow-shortcut
+ 3
+ Change
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ advanced
+ Advanced Settings
+ applications-system-symbolic
+
+
+
+
+ Advanced Settings
+
+
+ Find optimal display config
+ Automatically modify the glasses display configuration for maximum resolution and best scaling when plugged in.
+
+
+ 3
+
+
+
+
+
+
+ Always primary display
+ Automatically set the glasses as the primary display when plugged in.
+
+
+ 3
+
+
+
+
+
+
+ Movement look-ahead
+ Counteracts input lag by predicting head-tracking position ahead of render time. Stick with default unless virtual display drags behind your head movements, jumps ahead, or is very shaky.
+
+
+ 3
+ false
+ 0
+ 0
+ 350
+ false
+
+
+ -1
+ 40
+ 1
+ -1
+
+
+
+ Default
+ 10ms
+ 20ms
+ 30ms
+ 40ms
+
+
+
+
+
+
+
+
+
-
- Shortcuts
- Modify keyboard shortcuts and how they work
-
-
- Re-center display shortcut
- Pin the virtual display to the current position
- 2
-
-
- 30
- 30
-
-
- 3
-
-
-
-
-
-
- recenter-display-shortcut
- 3
- Change
-
-
-
-
-
-
-
-
- Display distance shortcut
- Quickly toggle between two predefined distances
- 2
-
-
- 30
- 30
-
-
- 3
-
-
-
-
-
-
- toggle-display-distance-shortcut
- 3
- Change
-
-
-
-
-
-
-
-
- Display distance start and end
- Use the buttons to capture the current display distance as start and end points.
- 2
-
-
- 30
- 150
- 30
-
-
- toggle-display-distance-start
- 3
-
-
-
-
- toggle-display-distance-end
- 3
-
-
-
-
-
-
-
-
- Toggle follow mode shortcut
- Quickly toggle follow mode
- 2
-
-
- 30
- 30
-
-
- 3
-
-
-
-
-
-
- toggle-follow-shortcut
- 3
- Change
-
-
-
-
-
-
+
+ stack
+ wide
diff --git a/ui/src/gtk/license-dialog.ui b/ui/src/gtk/license-dialog.ui
index 896b618..2954559 100644
--- a/ui/src/gtk/license-dialog.ui
+++ b/ui/src/gtk/license-dialog.ui
@@ -57,11 +57,11 @@
-
+
-
+
diff --git a/ui/src/gtk/window.ui b/ui/src/gtk/window.ui
index d06e8c7..40717e0 100644
--- a/ui/src/gtk/window.ui
+++ b/ui/src/gtk/window.ui
@@ -55,6 +55,10 @@
License Details
app.license
+ -
+ Force Reset
+ app.reset_driver
+
-
_About BreezyDesktop
app.about
diff --git a/ui/src/licensefeaturerow.py b/ui/src/licensefeaturerow.py
index 5f4904a..7d55ed8 100644
--- a/ui/src/licensefeaturerow.py
+++ b/ui/src/licensefeaturerow.py
@@ -3,10 +3,10 @@ from gi.repository import Adw
from .time import time_remaining_text
FEATURE_NAMES = {
- 'sbs': 'Side-by-side mode (for gaming)',
- 'smooth_follow': 'Smooth Follow',
- 'productivity_basic': 'Breezy Desktop',
- 'productivity_pro': 'Breezy Desktop w/ multiple monitors',
+ 'sbs': 'Side-by-side mode (gaming)',
+ 'smooth_follow': 'Smooth Follow (gaming)',
+ 'productivity_basic': 'Breezy Desktop (productivity)',
+ 'productivity_pro': 'Breezy Desktop Pro (productivity)',
}
class LicenseFeatureRow(Adw.ActionRow):
diff --git a/ui/src/licensetierrow.py b/ui/src/licensetierrow.py
index 9ee6cc7..9f5917c 100644
--- a/ui/src/licensetierrow.py
+++ b/ui/src/licensetierrow.py
@@ -53,7 +53,7 @@ class LicenseTierRow(Adw.ExpanderRow):
elif active_period is not None:
amount_text += " to upgrade"
elif active_period is not None and PERIOD_RANKS[period] >= PERIOD_RANKS[active_period]:
- amount_text = "Ready to auto-renew"
+ amount_text = "Paid through next renewal period"
if amount_text is not None:
row_widget = Adw.ActionRow(title=period.capitalize())
diff --git a/ui/src/main.py b/ui/src/main.py
index f77a4dc..a2a2281 100644
--- a/ui/src/main.py
+++ b/ui/src/main.py
@@ -64,6 +64,7 @@ class BreezydesktopApplication(Adw.Application):
self.create_action('quit', self.on_quit_action, ['q'])
self.create_action('about', self.on_about_action)
self.create_action('license', self.on_license_action)
+ self.create_action('reset_driver', self.on_reset_driver_action)
def do_activate(self):
"""Called when the application is activated.
@@ -94,6 +95,11 @@ class BreezydesktopApplication(Adw.Application):
dialog.set_transient_for(self.props.active_window)
dialog.present()
+ def on_reset_driver_action(self, widget, _):
+ XRDriverIPC.get_instance().write_control_flags({
+ 'force_quit': True
+ })
+
def create_action(self, name, callback, shortcuts=None):
"""Add an application action.
diff --git a/ui/src/statemanager.py b/ui/src/statemanager.py
index a8aa977..8c2497f 100644
--- a/ui/src/statemanager.py
+++ b/ui/src/statemanager.py
@@ -22,6 +22,7 @@ class StateManager(GObject.GObject):
__gproperties__ = {
'follow-mode': (bool, 'Follow Mode', 'Whether the follow mode is enabled', False, GObject.ParamFlags.READWRITE),
'follow-threshold': (float, 'Follow Threshold', 'The follow threshold', 1.0, 45.0, 15.0, GObject.ParamFlags.READWRITE),
+ 'widescreen-mode': (bool, 'Widescreen Mode', 'Whether widescreen mode is enabled', False, GObject.ParamFlags.READWRITE),
'license-action-needed': (bool, 'License Action Needed', 'Whether the license needs attention', False, GObject.ParamFlags.READWRITE),
'license-present': (bool, 'License Present', 'Whether a license is present', False, GObject.ParamFlags.READWRITE),
'enabled-features-list': (object, 'Enabled Features List', 'A list of the enabled features', GObject.ParamFlags.READWRITE),
@@ -94,12 +95,15 @@ class StateManager(GObject.GObject):
self.set_property('license-present', False)
self.set_property('follow-mode', self.state.get('breezy_desktop_smooth_follow_enabled'))
+ self.set_property('widescreen-mode', self.state.get('sbs_mode_enabled'))
if self.running: threading.Timer(1.0, self._refresh_state).start()
def do_set_property(self, prop, value):
if prop.name == 'follow-mode':
self.follow_mode = value
+ if prop.name == 'widescreen-mode':
+ self.widescreen_mode = value
if prop.name == 'license-action-needed':
self.license_action_needed = value
if prop.name == 'license-present':
@@ -110,6 +114,8 @@ class StateManager(GObject.GObject):
def do_get_property(self, prop):
if prop.name == 'follow-mode':
return self.follow_mode
+ if prop.name == 'widescreen-mode':
+ return self.widescreen_mode
if prop.name == 'license-action-needed':
return self.license_action_needed
if prop.name == 'license-present':
From 4ab2b596575468b0d9dc3d5b178c69db3c516252 Mon Sep 17 00:00:00 2001
From: wheaney <42350981+wheaney@users.noreply.github.com>
Date: Mon, 8 Jul 2024 16:15:49 -0700
Subject: [PATCH 06/13] Revert change to switch to widescreen mode quicker,
allow extension to run on the session unlock view
Switching to widescreen mode too quickly caused some problems
---
gnome/src/extension.js | 12 ++----------
gnome/src/metadata.json | 1 +
2 files changed, 3 insertions(+), 10 deletions(-)
diff --git a/gnome/src/extension.js b/gnome/src/extension.js
index 62cdcac..16958a5 100644
--- a/gnome/src/extension.js
+++ b/gnome/src/extension.js
@@ -151,16 +151,7 @@ export default class BreezyDesktopExtension extends Extension {
// A false result means we'll expect _handle_monitor_change to be triggered, so active polling
// can be disabled.
_target_monitor_ready(target_monitor) {
- if (target_monitor.is_dummy) {
- // do this so it updates the state, if needed, but don't worry about the result since
- // it won't trigger a monitor change
- this._needs_widescreen_monitor_update();
- return true;
- }
-
- // widescreen check should always be first since it changes the type of monitor, and we wouldn't
- // want to update the optimal mode if the monitor configs are going to change again
- return !this._needs_widescreen_monitor_update() &&
+ return target_monitor.is_dummy ||
!this._monitor_manager.needsOptimalModeCheck(target_monitor.connector);
}
@@ -268,6 +259,7 @@ export default class BreezyDesktopExtension extends Extension {
});
this._update_follow_threshold(this.settings);
+ 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));
diff --git a/gnome/src/metadata.json b/gnome/src/metadata.json
index 894c670..b9b5ebf 100644
--- a/gnome/src/metadata.json
+++ b/gnome/src/metadata.json
@@ -3,6 +3,7 @@
"name": "Breezy GNOME XR Desktop",
"description": "XR virtual desktop for GNOME.",
"settings-schema": "com.xronlinux.BreezyDesktop",
+ "session-modes": ["user", "unlock-dialog"],
"shell-version": [
"45", "46"
],
From baa7dd3bd0a11e6a64c90bd954ecad47578fba54 Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Mon, 8 Jul 2024 16:33:08 -0700
Subject: [PATCH 07/13] Update README.md
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 66d982f..0508ec9 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ Breezy GNOME is a virtual workspace solution for Linux desktops that use the GNO
2. Download the Breezy GNOME [setup script](https://github.com/wheaney/breezy-desktop/releases/latest/download/breezy_gnome_setup) and set the execute flag (e.g. from the terminal: `chmod +x ~/Downloads/breezy_gnome_setup`)
3. Run the setup script (e.g. `~/Downloads/breezy_gnome_setup`)
4. You'll have an application called `Breezy Desktop` installed. Launch that and follow any instructions. You will need to log out and back in at least once to get the GNOME extension working.
+5. For a double-wide screen, enable "widescreen mode" using the toggle in the Breezy Desktop application. **Note: this can be significantly more resource intensive that non-widescreen, you may notice performance dips on older hardware**
### Non-GNOME Setup
A workable solution (with some [QoL improvements needed](#upcoming-features)) is to use your preferred desktop environment with a GNOME window open in nested mode. To do this:
From 2ad27d4d50ab2d3a91d892c0804c11979943a13f Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Mon, 8 Jul 2024 16:33:29 -0700
Subject: [PATCH 08/13] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0508ec9..5ed5109 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ Breezy GNOME is a virtual workspace solution for Linux desktops that use the GNO
2. Download the Breezy GNOME [setup script](https://github.com/wheaney/breezy-desktop/releases/latest/download/breezy_gnome_setup) and set the execute flag (e.g. from the terminal: `chmod +x ~/Downloads/breezy_gnome_setup`)
3. Run the setup script (e.g. `~/Downloads/breezy_gnome_setup`)
4. You'll have an application called `Breezy Desktop` installed. Launch that and follow any instructions. You will need to log out and back in at least once to get the GNOME extension working.
-5. For a double-wide screen, enable "widescreen mode" using the toggle in the Breezy Desktop application. **Note: this can be significantly more resource intensive that non-widescreen, you may notice performance dips on older hardware**
+5. For a double-wide screen, enable "widescreen mode" using the toggle in the Breezy Desktop application. **Note: this can be significantly more resource intensive than non-widescreen, you may notice performance dips on older hardware**
### Non-GNOME Setup
A workable solution (with some [QoL improvements needed](#upcoming-features)) is to use your preferred desktop environment with a GNOME window open in nested mode. To do this:
From 9fb52bf4ec537194d28fb8882d28a8b7c31d2f9f Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Tue, 9 Jul 2024 09:26:03 -0700
Subject: [PATCH 09/13] Update README.md
---
README.md | 59 +++++++++++++++++++++++++++++++++----------------------
1 file changed, 35 insertions(+), 24 deletions(-)
diff --git a/README.md b/README.md
index 5ed5109..ffca9f6 100644
--- a/README.md
+++ b/README.md
@@ -32,12 +32,31 @@ A workable solution (with some [QoL improvements needed](#upcoming-features)) is
All controls are provided through the Breezy Desktop application. You can also configure keyboard shortcuts for the most common toggle actions. The Breezy Desktop app doesn't have to be running to use the virtual desktop or the keyboard shortcuts once you've configured everything to your liking.
### Upcoming Features
-1. Widescreen + true display depth w/ SBS
-2. Port to GNOME 43/44
-3. ARM/AARCH64 build
-4. Port to KWin Effect (KDE Plasma support)
-5. Multiple virtual monitors + multiple physical monitors
-6. Supported nested or Distrobox deployment
+1. Port to GNOME 43/44
+2. ARM/AARCH64 build
+3. Port to KWin Effect (KDE Plasma support)
+4. Multiple virtual monitors + multiple physical monitors
+5. Supported nested or Distrobox deployment
+
+### Breezy GNOME Pricing (Productivity Tier)
+
+Breezy GNOME comes with 2 free trial months. After that, it requires an active Productivity Tier license. Payments are currently only accepted via [Ko-fi](https://ko-fi.com/wheaney). Here's the pricing structure:
+
+| Payment period | Price | Upgrade window \* |
+| -------------- | ------------------ | ------------------------------------- |
+| Monthly | $5 USD, recurring | Within 7 days to upgrade to yearly |
+| Yearly | $50 USD, recurring | Within 30 days to upgrade to lifetime |
+| Lifetime | $125 USD, one-time | — |
+
+\* If you pay for a plan and decide to upgrade to a longer-term plan, you may pay the difference within this window.
+
+If you have enough funds, your license will renew automatically within 7 days of expiration so you never experience an unexpected outage. Your device is never required to be online to continue using Productivity Tier features when enabled, but if your access expires while offline (even if you have enough funds), the features will be disabled until the next time your device goes online and the license can be refreshed. Be sure to check for expiration warnings prior to travel.
+
+#### Unlocking Productivity Tier
+
+After your first payment, you should immediately receive an email (to your Ko-fi email address) with a verification token. Once you receive that, enter it in the `License Details` view of the `Breezy Desktop` application, available from the menu in the top window bar.
+
+If you don't receive a token, you can request one in the `License Details` view by entering your email address.
## Breezy Vulkan
@@ -71,26 +90,18 @@ To see all the configuration options available to you, type `~/bin/xreal_driver_
#### Multi-tap to re-center or re-calibrate
I've implemented an experimental multi-tap detection feature for screen **re-centering (2 taps)** and **re-calibrating the device (3 taps)**. To perform a multi-tap, you'll want to give decent taps on the top of the glasses. I tend to do this on the corner, right on top of the hinge. It should be a firm, sharp tap, and wait just a split second to do the second tap, as it needs to detect a slight pause in between (but it also shouldn't take more than a half a second between taps so don't wait too long).
-### Troubleshooting
-
-#### Screen drag or flickering
-Framerate is really important here, because individual frames are static, so moving your head quickly may produce a noticeable flicker as it moves the screen. Higher framerates will produce an overall better experience (less flicker and smoother follow), but lower framerates should still be totally usable.
-
-#### Unexpected screen movement or drift
-It's important that your glasses are either on your head or sitting on a flat surface when they're first plugged in and calibrated. If you notice that your screen is constantly drifting in one direction or continues to move for several seconds after a head movement, almost as if the screen has some momentum that takes time to slow down, then you'll want to re-calibrate them. To do this, do a triple-tap as described in the Multi-tap section above.
-
-#### Display size
-
-If the screen appears very small in your view, you may be playing at the Deck screen's native resolution, and not at the glasses' native
-resolution. To fix this:
-1. Go to the game details in Steam, hit the Settings/cog icon, and open `Properties`, then for `Game Resolution` choose `Native`.
-2. After launching the game, if it's still small, go into the game options, and in the graphics or video settings, change the resolution (the glasses run at 1920x1080).
-
-If you *WANT* to keep a low resolution, then you can just use the `Zoom` setting to make the screen appear larger. For now this is done through the config script: `~/bin/xreal_driver_config -z 1.0`. Larger numbers zoom in (e.g. `2.0` doubles the screen size) and smaller numbers zoom out (e.g. `0.5` is half the screen size).
-
### Supporter Tier
-Supporter Tier features are enhancments to core functionality, offered as a way to reward those who have [supported the project](https://ko-fi.com/wheaney). Core features -- like Virtual Display mode, VR-Lite mouse/joystick modes, and Follow mode's display positioning/resizing settings -- will always remain available to everyone regardless of supporter status. Donating $10 gets you a year, and $25 gets you lifetime of Supporter Tier access. If you have enough funds, your access will renew automatically within 7 days of expiration so you never experience an unexpected outage. Your device is never required to be online to continue using Supporter Tier features when enabled, but if your access expires while offline (even if you have enough funds), the features will be disabled until the next time your device goes online and the license can be refreshed. Be sure to check for expiration warnings prior to travel.
+Breezy Vulkan's Supporter Tier features are enhancments to core functionality, offered as a way to reward those who have [supported the project](https://ko-fi.com/wheaney). Core features -- like Virtual Display mode, VR-Lite mouse/joystick modes, and Follow mode's display positioning/resizing settings -- will always remain available to everyone regardless of supporter status. Here's the pricing structure:
+
+| Payment period | Price | Upgrade window \* |
+| -------------- | ------------------ | ------------------------------------- |
+| Yearly | $10 USD, recurring | Within 30 days to upgrade to lifetime |
+| Lifetime | $25 USD, one-time | — |
+
+\* If you pay for a plan and decide to upgrade to a longer-term plan, you may pay the difference within this window.
+
+If you have enough funds, your access will renew automatically within 7 days of expiration so you never experience an unexpected outage. Your device is never required to be online to continue using Supporter Tier features when enabled, but if your access expires while offline (even if you have enough funds), the features will be disabled until the next time your device goes online and the license can be refreshed. Be sure to check for expiration warnings prior to travel.
Features currently offered:
* Smooth Follow (in Follow mode)
From 8f4a17c41eeed1ec0a2f638890fe215d65ab1427 Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Tue, 9 Jul 2024 09:28:31 -0700
Subject: [PATCH 10/13] Update README.md
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index ffca9f6..b486d72 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,7 @@ Breezy GNOME comes with 2 free trial months. After that, it requires an active P
| Payment period | Price | Upgrade window \* |
| -------------- | ------------------ | ------------------------------------- |
| Monthly | $5 USD, recurring | Within 7 days to upgrade to yearly |
-| Yearly | $50 USD, recurring | Within 30 days to upgrade to lifetime |
+| Yearly | $50 USD, recurring | Within 90 days to upgrade to lifetime |
| Lifetime | $125 USD, one-time | — |
\* If you pay for a plan and decide to upgrade to a longer-term plan, you may pay the difference within this window.
@@ -96,7 +96,7 @@ Breezy Vulkan's Supporter Tier features are enhancments to core functionality, o
| Payment period | Price | Upgrade window \* |
| -------------- | ------------------ | ------------------------------------- |
-| Yearly | $10 USD, recurring | Within 30 days to upgrade to lifetime |
+| Yearly | $10 USD, recurring | Within 90 days to upgrade to lifetime |
| Lifetime | $25 USD, one-time | — |
\* If you pay for a plan and decide to upgrade to a longer-term plan, you may pay the difference within this window.
From c6c05d8715decd0b078ed8df34491eaa53161575 Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Tue, 9 Jul 2024 21:34:32 -0700
Subject: [PATCH 11/13] Update README.md
---
README.md | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/README.md b/README.md
index b486d72..8879ea4 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,18 @@ Breezy GNOME comes with 2 free trial months. After that, it requires an active P
If you have enough funds, your license will renew automatically within 7 days of expiration so you never experience an unexpected outage. Your device is never required to be online to continue using Productivity Tier features when enabled, but if your access expires while offline (even if you have enough funds), the features will be disabled until the next time your device goes online and the license can be refreshed. Be sure to check for expiration warnings prior to travel.
+#### Free Productivity Tier
+
+To make Breezy widely accessible, Productivity Tier is currently free of charge for qualified individuals using it for non-commercial purposes. Eligible groups include:
+
+* Students
+* Public school educators
+* Active duty service members and veterans of the U.S. Armed Forces
+* Ukrainian citizens
+* Individuals experiencing financial hardship or special circumstances that make electronic payments prohibitive
+
+If you believe you qualify, please email wayne@xronlinux.com. You may be asked to provide documentation to verify your eligibility.
+
#### Unlocking Productivity Tier
After your first payment, you should immediately receive an email (to your Ko-fi email address) with a verification token. Once you receive that, enter it in the `License Details` view of the `Breezy Desktop` application, available from the menu in the top window bar.
From 7369cb0551abcd9c1688838f565cb0258a15a2a8 Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Tue, 9 Jul 2024 21:36:27 -0700
Subject: [PATCH 12/13] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 8879ea4..c0400ff 100644
--- a/README.md
+++ b/README.md
@@ -59,8 +59,8 @@ To make Breezy widely accessible, Productivity Tier is currently free of charge
* Students
* Public school educators
* Active duty service members and veterans of the U.S. Armed Forces
-* Ukrainian citizens
* Individuals experiencing financial hardship or special circumstances that make electronic payments prohibitive
+* Individuals affected by active war zones or humanitarian crises (e.g. Ukrainian citizens)
If you believe you qualify, please email wayne@xronlinux.com. You may be asked to provide documentation to verify your eligibility.
From 04edf2eecc9439b0529998688a3d51d90980c859 Mon Sep 17 00:00:00 2001
From: wheaney <42350981+wheaney@users.noreply.github.com>
Date: Wed, 17 Jul 2024 21:55:49 -0700
Subject: [PATCH 13/13] Add refresh rate and fast SBS mode switching settings
---
gnome/src/cursormanager.js | 1 -
gnome/src/extension.js | 48 +++++++++++++++----
gnome/src/monitormanager.js | 26 ++++++++--
ui/bin/package | 4 +-
.../com.xronlinux.BreezyDesktop.gschema.xml | 18 +++++++
...om.xronlinux.BreezyDesktop.metainfo.xml.in | 8 +++-
ui/src/connecteddevice.py | 8 ++++
ui/src/gtk/connected-device.ui | 22 +++++++++
8 files changed, 118 insertions(+), 17 deletions(-)
diff --git a/gnome/src/cursormanager.js b/gnome/src/cursormanager.js
index c2f5e82..44b3f5f 100644
--- a/gnome/src/cursormanager.js
+++ b/gnome/src/cursormanager.js
@@ -1,5 +1,4 @@
import Clutter from 'gi://Clutter';
-import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.js';
import { MouseSpriteContent } from './cursor.js';
diff --git a/gnome/src/extension.js b/gnome/src/extension.js
index 16958a5..a7edafd 100644
--- a/gnome/src/extension.js
+++ b/gnome/src/extension.js
@@ -71,6 +71,7 @@ export default class BreezyDesktopExtension extends Extension {
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'),
+ use_highest_refresh_rate: this.settings.get_boolean('use-highest-refresh-rate'),
extension_path: this.path
});
this._monitor_manager.setChangeHook(this._handle_monitor_change.bind(this));
@@ -120,7 +121,7 @@ export default class BreezyDesktopExtension extends Extension {
const target_monitor = this._monitor_manager.getMonitorPropertiesList()?.find(
monitor => SUPPORTED_MONITOR_PRODUCTS.includes(monitor.product));
if (target_monitor !== undefined) {
- Globals.logger.log_debug(`BreezyDesktopExtension _find_supported_monitor - Identified supported monitor: ${target_monitor.connector}`);
+ 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,
@@ -151,8 +152,11 @@ export default class BreezyDesktopExtension extends Extension {
// A false result means we'll expect _handle_monitor_change to be triggered, so active polling
// can be disabled.
_target_monitor_ready(target_monitor) {
- return target_monitor.is_dummy ||
- !this._monitor_manager.needsOptimalModeCheck(target_monitor.connector);
+ if (target_monitor.is_dummy) return true;
+
+ const needs_sbs_mode_switch = this.settings.get_boolean('fast-sbs-mode-switching') &&
+ this._needs_widescreen_monitor_update();
+ return !needs_sbs_mode_switch && !this._monitor_manager.needsOptimalModeCheck(target_monitor.connector);
}
_setup() {
@@ -213,7 +217,7 @@ export default class BreezyDesktopExtension extends Extension {
const widescreen_setting_enabled = this.settings.get_boolean('widescreen-mode');
if (widescreen_setting_enabled !== sbs_enabled) {
Globals.logger.log_debug('BreezyDesktopExtension _needs_widescreen_monitor_update - true');
- this._write_control('sbs_mode', widescreen_setting_enabled ? 'enable' : 'disable');
+ this._request_sbs_mode_change(widescreen_setting_enabled);
return true;
}
@@ -259,7 +263,10 @@ export default class BreezyDesktopExtension extends Extension {
});
this._update_follow_threshold(this.settings);
- this._update_widescreen_mode_from_settings(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._supported_device_detected_connected = this._xr_effect.connect('notify::supported-device-detected', this._handle_supported_device_change.bind(this));
@@ -362,16 +369,41 @@ export default class BreezyDesktopExtension extends Extension {
if (value !== undefined) this._write_control('breezy_desktop_follow_threshold', value);
}
+ // requests sbs_mode change and monitors to ensure the state reflects the setting
+ _request_sbs_mode_change(value) {
+ this._write_control('sbs_mode', value ? 'enable' : 'disable');
+ if (!this._sbs_mode_update_timeout) {
+ var attempts = 0;
+ this._sbs_mode_update_timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 10000, (() => {
+ if (attempts++ < 3) {
+ this._write_control('sbs_mode', value ? 'enable' : 'disable');
+ return GLib.SOURCE_CONTINUE;
+ }
+
+ // the state never updated to reflect our request, revert the setting
+ this.settings.set_boolean('widescreen-mode', !value);
+ this._sbs_mode_update_timeout = undefined;
+ return GLib.SOURCE_REMOVE;
+ }).bind(this));
+ }
+ }
+
_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._write_control('sbs_mode', value ? 'enable' : 'disable');
- else
+ 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) {
+ // kill our state checker if it's running
+ if (this._sbs_mode_update_timeout) {
+ GLib.source_remove(this._sbs_mode_update_timeout);
+ this._sbs_mode_update_timeout = undefined;
+ }
+
const value = effect.widescreen_mode_state;
Globals.logger.log_debug(`BreezyDesktopExtension _update_widescreen_mode_from_state ${value}`);
if (value !== this.settings.get_boolean('widescreen-mode'))
diff --git a/gnome/src/monitormanager.js b/gnome/src/monitormanager.js
index 8126ecf..1d4b98c 100644
--- a/gnome/src/monitormanager.js
+++ b/gnome/src/monitormanager.js
@@ -86,7 +86,7 @@ function getMonitorConfig(displayConfigProxy, callback) {
}
// triggers callback with true result if an an async monitor config change was triggered, false if no config change needed
-function performOptimalModeCheck(displayConfigProxy, connectorName, headsetAsPrimary, callback) {
+function performOptimalModeCheck(displayConfigProxy, connectorName, headsetAsPrimary, useHighestRefreshRate, callback) {
Globals.logger.log_debug(`monitormanager.js performOptimalModeCheck for ${connectorName}`);
displayConfigProxy.GetCurrentStateRemote((result, error) => {
if (error) {
@@ -101,10 +101,19 @@ function performOptimalModeCheck(displayConfigProxy, connectorName, headsetAsPri
let monitorToModeIdMap = {};
let bestFitMode = undefined;
for (let monitor of monitors) {
- const [details, modes, monProperties] = monitor;
+ const [details, availableModes, monProperties] = monitor;
const [connector, vendor, product, monitorSerial] = details;
const isOurMonitor = connector == connectorName;
- if (isOurMonitor) ourMonitor = monitor;
+ let modes = availableModes;
+ if (isOurMonitor) {
+ ourMonitor = monitor;
+ if (!useHighestRefreshRate) {
+ const currentMode = modes.find((mode) => !!mode[6]['is-current']);
+
+ // filter modes to only include the current refresh rate
+ modes = availableModes.filter((mode) => mode[3] === currentMode[3]);
+ }
+ }
for (let mode of modes) {
const [modeId, width, height, refreshRate, preferredScale, supportedScales, modeProperites] = mode;
@@ -199,6 +208,13 @@ export const MonitorManager = GObject.registerClass({
GObject.ParamFlags.READWRITE,
true
),
+ 'use-highest-refresh-rate': GObject.ParamSpec.boolean(
+ 'use-highest-refresh-rate',
+ 'Use highest refresh rate',
+ 'Set the highest refresh rate which choosing optimal configs',
+ GObject.ParamFlags.READWRITE,
+ true
+ ),
'headset-as-primary': GObject.ParamSpec.boolean(
'headset-as-primary',
'Use headset as primary monitor',
@@ -272,7 +288,7 @@ export const MonitorManager = GObject.registerClass({
}
if (this._needsConfigCheck) {
- performOptimalModeCheck(this._displayConfigProxy, monitorConnector, this.headset_as_primary, ((configChanged, error) => {
+ performOptimalModeCheck(this._displayConfigProxy, monitorConnector, this.headset_as_primary, this.use_highest_refresh_rate, ((configChanged, error) => {
this._needsConfigCheck = false;
if (error) {
Globals.logger.log(`Failed to switch to optimal mode for monitor ${monitorConnector}: ${error}`);
@@ -309,7 +325,7 @@ export const MonitorManager = GObject.registerClass({
for (let i = 0; i < result.length; i++) {
const [monitorName, connectorName, vendor, product, serial, refreshRate] = result[i];
const monitorIndex = this._backendManager.get_monitor_for_connector(connectorName);
- Globals.logger.log(`Found monitor ${monitorName}, vendor ${vendor}, product ${product}, serial ${serial}, connector ${connectorName}, index ${monitorIndex}`);
+ Globals.logger.log_debug(`Found monitor ${monitorName}, vendor ${vendor}, product ${product}, serial ${serial}, connector ${connectorName}, index ${monitorIndex}`);
if (monitorIndex >= 0) {
monitorProperties[monitorIndex] = {
index: monitorIndex,
diff --git a/ui/bin/package b/ui/bin/package
index a3c6764..7b04f59 100755
--- a/ui/bin/package
+++ b/ui/bin/package
@@ -16,12 +16,12 @@ check_command "flatpak-builder"
# https://stackoverflow.com/a/246128
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
-TMP_DIR=$(mktemp -d -t breezy-ui-flatpak-XXXXXXXXXX)
+TMP_DIR=$(mktemp -d -t --tmpdir=$SCRIPT_DIR/.. breezy-ui-flatpak-XXXXXXXXXX)
OUT_DIR=$SCRIPT_DIR/../out
rm -rf $OUT_DIR
mkdir -p $OUT_DIR
-flatpak-builder --force-clean $TMP_DIR/build $SCRIPT_DIR/../com.xronlinux.BreezyDesktop.json
+flatpak-builder --force-clean --delete-build-dirs $TMP_DIR/build $SCRIPT_DIR/../com.xronlinux.BreezyDesktop.json
flatpak build-export $TMP_DIR/export $TMP_DIR/build
flatpak build-bundle $TMP_DIR/export $OUT_DIR/com.xronlinux.BreezyDesktop.flatpak com.xronlinux.BreezyDesktop --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
diff --git a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
index 7b33ad7..4372563 100644
--- a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
+++ b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
@@ -118,6 +118,24 @@
Automatically set the headset as the primary display upon connection
+
+
+ true
+
+ Use highest refresh rate
+
+ Automatically set the highest refresh rate upon connection
+
+
+
+
+ true
+
+ Fast SBS mode switching
+
+ Enable fast SBS mode switching
+
+
false
diff --git a/ui/data/com.xronlinux.BreezyDesktop.metainfo.xml.in b/ui/data/com.xronlinux.BreezyDesktop.metainfo.xml.in
index eab0852..b16137d 100644
--- a/ui/data/com.xronlinux.BreezyDesktop.metainfo.xml.in
+++ b/ui/data/com.xronlinux.BreezyDesktop.metainfo.xml.in
@@ -3,7 +3,13 @@
com.xronlinux.BreezyDesktop.desktop
CC0-1.0
GPL-3.0-or-later
+ Breezy Desktop
+ XR Desktop Control Panel
- No description
+ XR Desktop Control Panel
+
+ Office
+ Development
+
diff --git a/ui/src/connecteddevice.py b/ui/src/connecteddevice.py
index 241c4d5..133d2c2 100644
--- a/ui/src/connecteddevice.py
+++ b/ui/src/connecteddevice.py
@@ -31,6 +31,8 @@ class ConnectedDevice(Gtk.Box):
toggle_follow_shortcut_label = Gtk.Template.Child()
headset_as_primary_switch = Gtk.Template.Child()
use_optimal_monitor_config_switch = Gtk.Template.Child()
+ use_highest_refresh_rate_switch = Gtk.Template.Child()
+ fast_sbs_mode_switch = Gtk.Template.Child()
movement_look_ahead_scale = Gtk.Template.Child()
movement_look_ahead_adjustment = Gtk.Template.Child()
@@ -52,6 +54,8 @@ class ConnectedDevice(Gtk.Box):
self.reassign_toggle_follow_shortcut_button,
self.headset_as_primary_switch,
self.use_optimal_monitor_config_switch,
+ self.use_highest_refresh_rate_switch,
+ self.fast_sbs_mode_switch,
self.movement_look_ahead_scale
]
@@ -66,6 +70,8 @@ class ConnectedDevice(Gtk.Box):
self.settings.bind('curved-display', self.curved_display_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('headset-as-primary', self.headset_as_primary_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('use-optimal-monitor-config', self.use_optimal_monitor_config_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.bind('use-highest-refresh-rate', self.use_highest_refresh_rate_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.bind('fast-sbs-mode-switching', self.fast_sbs_mode_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('look-ahead-override', self.movement_look_ahead_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
bind_shortcut_settings(self.get_parent(), [
@@ -136,8 +142,10 @@ class ConnectedDevice(Gtk.Box):
def _refresh_use_optimal_monitor_config(self, switch, param):
self.headset_as_primary_switch.set_sensitive(switch.get_active())
+ self.use_highest_refresh_rate_switch.set_sensitive(switch.get_active())
if not switch.get_active():
self.headset_as_primary_switch.set_active(False)
+ self.use_highest_refresh_rate_switch.set_active(False)
def set_device_name(self, name):
self.device_label.set_markup(f"{name}")
diff --git a/ui/src/gtk/connected-device.ui b/ui/src/gtk/connected-device.ui
index 215ca1c..b5953c3 100644
--- a/ui/src/gtk/connected-device.ui
+++ b/ui/src/gtk/connected-device.ui
@@ -328,6 +328,17 @@
+
+
+ Use highest refresh rate
+ Refresh rate may affect performance, disable this to set it manually.
+
+
+ 3
+
+
+
+
Always primary display
@@ -339,6 +350,17 @@
+
+
+ Fast SBS mode switching
+ Switches glasses to SBS mode immediately when plugged in, if widescreen mode is on. May cause instability.
+
+
+ 3
+
+
+
+
Movement look-ahead