Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions gnome/src/devicedatastream.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,32 @@ const POSE_ORIENTATION = [dataViewEnd(EPOCH_MS), FLOAT_SIZE, 16];
const IMU_PARITY_BYTE = [dataViewEnd(POSE_ORIENTATION), UINT8_SIZE, 1];
const DATA_VIEW_LENGTH = dataViewEnd(IMU_PARITY_BYTE);

// Flatten a packed pose-orientation buffer (mat4 as 16 floats: orientation
// quaternions at floats [0..3] and [4..7], timestamps at [12..15]) to yaw-only.
// Uses a swing-twist decomposition about the NWU up-axis (Z): keep the Z and W
// components, zero X and Y, then renormalize. This removes all pitch and roll
// from the rendered orientation, so the display tracks only horizontal (yaw)
// head movement. The identity quaternion [0,0,0,1] is preserved.
function applyHorizonLock(poseOrientation) {
for (const offset of [0, 4]) {
const z = poseOrientation[offset + 2];
const w = poseOrientation[offset + 3];
const norm = Math.hypot(z, w);
poseOrientation[offset] = 0;
poseOrientation[offset + 1] = 0;
if (norm < 1e-6) {
// Yaw is undefined here (a pure 180-degree pitch/roll pose); fall
// back to identity rather than emitting a zero (non-unit) quaternion.
poseOrientation[offset + 2] = 0;
poseOrientation[offset + 3] = 1;
} else {
poseOrientation[offset + 2] = z / norm;
poseOrientation[offset + 3] = w / norm;
}
}
return poseOrientation;
}

function checkParityByte(dataView) {
const parityByte = dataViewUint8(dataView, IMU_PARITY_BYTE);
let parity = 0;
Expand Down Expand Up @@ -132,6 +158,13 @@ export const DeviceDataStream = GObject.registerClass({
'Debug mode that allows for testing with moving IMU values without a device connected',
GObject.ParamFlags.READWRITE,
false
),
'horizon-lock': GObject.ParamSpec.boolean(
'horizon-lock',
'Horizon lock',
'Flatten the head pose to yaw-only (ignore pitch and roll) so the display only tracks horizontal head movement',
GObject.ParamFlags.READWRITE,
false
)
}
}, class DeviceDataStream extends GObject.Object {
Expand Down Expand Up @@ -212,9 +245,13 @@ export const DeviceDataStream = GObject.registerClass({
const version = dataViewUint8(dataView, VERSION);
const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validData;
let poseOrientation = dataViewFloatArray(dataView, POSE_ORIENTATION);
if (this.horizon_lock) applyHorizonLock(poseOrientation);
let posePosition = dataViewFloatArray(dataView, POSE_POSITION);
let smoothFollowEnabled = !this.legacy_follow_mode && dataViewUint8(dataView, SMOOTH_FOLLOW_ENABLED) !== 0;
let smoothFollowOrigin = dataViewFloatArray(dataView, SMOOTH_FOLLOW_ORIGIN_DATA);
// The shader renders from the smooth-follow origin when follow is
// active, so horizon lock must flatten it too (same packed layout).
if (this.horizon_lock) applyHorizonLock(smoothFollowOrigin);
const imuResetState = enabled && validData && poseOrientation[0] === 0.0 && poseOrientation[1] === 0.0 && poseOrientation[2] === 0.0 && poseOrientation[3] === 1.0;
const customBannerEnabled = dataViewUint8(dataView, CUSTOM_BANNER_ENABLED) !== 0;
const sbsEnabled = dataViewUint8(dataView, SBS_ENABLED) !== 0;
Expand Down Expand Up @@ -281,6 +318,7 @@ export const DeviceDataStream = GObject.registerClass({
dataView = new DataView(buffer);
imuDateMs = dataViewBigUint(dataView, EPOCH_MS);
poseOrientation = dataViewFloatArray(dataView, POSE_ORIENTATION);
if (this.horizon_lock) applyHorizonLock(poseOrientation);
posePosition = dataViewFloatArray(dataView, POSE_POSITION);
}
}
Expand Down
2 changes: 2 additions & 0 deletions gnome/src/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default class BreezyDesktopExtension extends Extension {
this.settings.bind('disable-physical-displays', this._monitor_manager, 'disable-physical-displays', Gio.SettingsBindFlags.DEFAULT);
this.settings.bind('legacy-follow-mode', Globals.data_stream, 'legacy-follow-mode', Gio.SettingsBindFlags.DEFAULT);
this.settings.bind('debug-no-device', Globals.data_stream, 'debug-no-device', Gio.SettingsBindFlags.DEFAULT);
this.settings.bind('horizon-lock', Globals.data_stream, 'horizon-lock', Gio.SettingsBindFlags.DEFAULT);

this._breezy_desktop_running_connection = Globals.data_stream.connect('notify::breezy-desktop-running',
this._handle_breezy_desktop_running_change.bind(this));
Expand Down Expand Up @@ -732,6 +733,7 @@ export default class BreezyDesktopExtension extends Extension {
Gio.Settings.unbind(this.settings, 'headset-as-primary');
Gio.Settings.unbind(this.settings, 'disable-physical-displays');
Gio.Settings.unbind(this.settings, 'debug-no-device');
Gio.Settings.unbind(this.settings, 'horizon-lock');

if (this._monitor_manager) {
this._monitor_manager.disable();
Expand Down
9 changes: 9 additions & 0 deletions ui/data/com.xronlinux.BreezyDesktop.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@
Enable curved display mode
</description>
</key>
<key name="horizon-lock" type="b">
<default>
false
</default>
<summary>Horizon lock</summary>
<description>
Lock the display to the horizon, tracking only horizontal (yaw) head movement and ignoring pitch (vertical) and roll (tilt)
</description>
</key>
<key name="look-ahead-override" type="i">
<default>
-1
Expand Down
3 changes: 3 additions & 0 deletions ui/src/connecteddevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class ConnectedDevice(Gtk.Box):
follow_threshold_adjustment = Gtk.Template.Child()
follow_mode_switch = Gtk.Template.Child()
curved_display_switch = Gtk.Template.Child()
horizon_lock_switch = Gtk.Template.Child()
top_features_group = Gtk.Template.Child()
virtual_displays_row = Gtk.Template.Child()
add_virtual_display_menu = Gtk.Template.Child()
Expand Down Expand Up @@ -99,6 +100,7 @@ def __init__(self):
self.follow_mode_switch,
self.follow_threshold_scale,
self.curved_display_switch,
self.horizon_lock_switch,
self.add_virtual_display_menu,
self.add_virtual_display_button,
self.change_all_displays_distance_button,
Expand All @@ -124,6 +126,7 @@ def __init__(self):
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('horizon-lock', self.horizon_lock_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('headset-display-as-viewport-center', self.headset_display_as_viewport_center_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('headset-as-primary', self.headset_as_primary_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('remove-virtual-displays-on-disable', self.remove_virtual_displays_on_disable_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
Expand Down
12 changes: 12 additions & 0 deletions ui/src/gtk/connected-device.ui
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes"><!-- feature switch -->Horizon lock</property>
<property name="subtitle" translatable="yes">Track only horizontal head movement; ignore looking up/down and head tilt.</property>
<property name="valign">2</property>
<child>
<object class="GtkSwitch" id="horizon_lock_switch">
<property name="valign">3</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes"><!-- feature switch -->Disable physical displays</property>
Expand Down