diff --git a/README.md b/README.md
index 934628d..c0400ff 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.
@@ -16,9 +16,11 @@ There are two installations at the moment:
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.
+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:
@@ -32,11 +34,41 @@ All controls are provided through the Breezy Desktop application. You can also c
### 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
+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 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.
+
+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
+* 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.
+
+#### 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
@@ -70,26 +102,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 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.
+
+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)
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..44b3f5f 100644
--- a/gnome/src/cursormanager.js
+++ b/gnome/src/cursormanager.js
@@ -6,10 +6,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 +25,7 @@ export class CursorManager {
this._cursorWatch = null;
this._cursorChangedConnection = null;
this._cursorVisibilityChangedConnection = null;
- this._cursorPositionInvalidatedConnection = null;
+ this._redraw_timeline = null;
}
enable() {
@@ -71,11 +70,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 +87,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 +123,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 +172,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 +198,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..a7edafd 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,23 @@ 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'),
+ 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));
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 +93,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 +121,107 @@ 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(`Identified supported monitor: ${target_monitor.product} on ${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) 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() {
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._request_sbs_mode_change(widescreen_setting_enabled);
+ return true;
+ }
+
+ return false;
}
_effect_enable() {
@@ -153,23 +231,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 +253,35 @@ 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 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));
+
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 +290,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 +321,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 +336,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 +369,65 @@ 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._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'))
+ 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 +438,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 +455,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 +474,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 +486,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 +535,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/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"
],
diff --git a/gnome/src/monitormanager.js b/gnome/src/monitormanager.js
index 0eacea5..1d4b98c 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,179 @@ 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, useHighestRefreshRate, 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, availableModes, monProperties] = monitor;
+ const [connector, vendor, product, monitorSerial] = details;
+ const isOurMonitor = connector == connectorName;
+ 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;
+ 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
+ ),
+ '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',
+ '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 +271,6 @@ export default class MonitorManager {
this._changeHookFn = fn;
}
- setPostCallback(callback) {
- this._postCallback = callback;
- }
-
getMonitors() {
return Main.layoutManager.monitors;
}
@@ -135,11 +279,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, 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}`);
+ } 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;
}
@@ -147,7 +325,7 @@ export default class MonitorManager {
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,
@@ -161,9 +339,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 26ece49..ec3f82a 160000
--- a/modules/sombrero
+++ b/modules/sombrero
@@ -1 +1 @@
-Subproject commit 26ece497d36bbe2a7445ea2f4e1cce31c35daa3a
+Subproject commit ec3f82aa4834847adf1f0c640eba4b87b6f3ff65
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 a1babc3..4372563 100644
--- a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
+++ b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
@@ -64,6 +64,87 @@
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
+
+
+
+
+ true
+
+ Use highest refresh rate
+
+ Automatically set the highest refresh rate upon connection
+
+
+
+
+ true
+
+ Fast SBS mode switching
+
+ Enable fast SBS mode switching
+
+
+
+
+ false
+
+ Disable anti-aliasing
+
+ Disable anti-aliasing
+
+
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/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..133d2c2 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,34 @@ 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()
+ 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()
+
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.use_highest_refresh_rate_switch,
+ self.fast_sbs_mode_switch,
+ self.movement_look_ahead_scale
]
self.settings = SettingsManager.get_instance().settings
@@ -45,7 +64,15 @@ 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('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(), [
[self.reassign_recenter_display_shortcut_button, self.recenter_display_shortcut_label],
@@ -68,8 +95,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 +128,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 +140,13 @@ 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())
+ 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}")
@@ -120,7 +158,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 +171,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..b5953c3 100644
--- a/ui/src/gtk/connected-device.ui
+++ b/ui/src/gtk/connected-device.ui
@@ -24,201 +24,386 @@
-
+
+
+ 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
+
+
+
+
+
+
+ Use highest refresh rate
+ Refresh rate may affect performance, disable this to set it manually.
+
+
+ 3
+
+
+
+
+
+
+ Always primary display
+ Automatically set the glasses as the primary display when plugged in.
+
+
+ 3
+
+
+
+
+
+
+ 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
+ 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 9a26160..8781f21 100644
--- a/ui/src/main.py
+++ b/ui/src/main.py
@@ -65,6 +65,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)
self._skip_verification = skip_verification
def do_activate(self):
@@ -96,6 +97,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':