diff --git a/gnome/src/devicedatastream.js b/gnome/src/devicedatastream.js index 161e9ff..03cd97f 100644 --- a/gnome/src/devicedatastream.js +++ b/gnome/src/devicedatastream.js @@ -51,6 +51,28 @@ function checkParityByte(dataView) { return parityByte === parity; } +const COUNTER_MAX = 150; +function nextDebugIMUQuaternion(counter) { + const angle = counter / COUNTER_MAX * 2 * Math.PI; + const yaw = 10 * Math.PI / 180 * Math.cos(angle); + const roll = 0; + const pitch = 10 * Math.PI / 180 * Math.sin(angle); + + const cy = Math.cos(yaw * 0.5); + const sy = Math.sin(yaw * 0.5); + const cp = Math.cos(pitch * 0.5); + const sp = Math.sin(pitch * 0.5); + const cr = Math.cos(roll * 0.5); + const sr = Math.sin(roll * 0.5); + + const w = cr * cp * cy + sr * sp * sy; + const x = sr * cp * cy - cr * sp * sy; + const y = cr * sp * cy + sr * cp * sy; + const z = cr * cp * sy - sr * sp * cy; + + return [x, y, z, w]; +} + export const DeviceDataStream = GObject.registerClass({ Properties: { 'breezy-desktop-running': GObject.ParamSpec.boolean( @@ -72,6 +94,13 @@ export const DeviceDataStream = GObject.registerClass({ 'IMU Snapshots', 'Latest IMU quaternion snapshots and epoch timestamp for when it was collected', GObject.ParamFlags.READWRITE + ), + 'debug-no-device': GObject.ParamSpec.boolean( + 'debug-no-device', + 'Debug without device', + 'Debug mode that allows for testing with moving IMU values without a device connected', + GObject.ParamFlags.READWRITE, + false ) } }, class DeviceDataStream extends GObject.Object { @@ -90,6 +119,7 @@ export const DeviceDataStream = GObject.registerClass({ stop() { this._running = false; + this.device_data = null; } // polling is just intended to keep breezy_desktop_running current, anything needing up-to-date imu data should @@ -104,6 +134,47 @@ export const DeviceDataStream = GObject.registerClass({ // Refresh the data from the IPC file. if keepalive_only is true, we'll only check and update breezy_desktop_running if it // hasn't been checked within KEEPALIVE_REFRESH_INTERVAL_SEC. refresh_data(keepalive_only = false) { + if (this.debug_no_device) { + this.was_debug_no_device = true; + if (!this.device_data) { + this.device_data = { + version: 1.0, + enabled: true, + imuResetState: false, + displayRes: [1920.0, 1080.0], + sbsEnabled: false, + displayFov: 46.0, + lookAheadCfg: [0.0, 0.0, 0.0, 0.0] + } + } + + if (!keepalive_only) { + this._counter = ((this._counter ?? -1)+1)%COUNTER_MAX; + + const imuDataFirst = nextDebugIMUQuaternion(this._counter); + const imuData = [ + ...imuDataFirst, + ...imuDataFirst, + ...imuDataFirst, + 2.0, 1.0, 0.0, 0.0 + ] + const imuDateMs = Date.now(); + this.device_data.imuData = imuData; + this.device_data.imuDateMs = imuDateMs; + this.imu_snapshots = { + imu_data: imuData, + timestamp_ms: imuDateMs + }; + } + this.breezy_desktop_running = true; + return; + } else if (this.was_debug_no_device) { + this.was_debug_no_device = false; + this.device_data = null; + this.breezy_desktop_running = false; + this.imu_snapshots = null; + } + if (this._ipc_file.query_exists(null) && ( !this.device_data?.imuData || !keepalive_only || @@ -114,9 +185,9 @@ export const DeviceDataStream = GObject.registerClass({ let buffer = new Uint8Array(data[1]).buffer; let dataView = new DataView(buffer); if (dataView.byteLength === DATA_VIEW_LENGTH) { - const imuDateMs = dataViewBigUint(dataView, EPOCH_MS); + let imuDateMs = dataViewBigUint(dataView, EPOCH_MS); const validKeepalive = isValidKeepAlive(toSec(imuDateMs)); - const imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA); + let imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA); const imuResetState = validKeepalive && imuData[0] === 0.0 && imuData[1] === 0.0 && imuData[2] === 0.0 && imuData[3] === 1.0; const version = dataViewUint8(dataView, VERSION); const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive; @@ -132,9 +203,16 @@ export const DeviceDataStream = GObject.registerClass({ imuResetState, displayRes: dataViewUint32Array(dataView, DISPLAY_RES), sbsEnabled, - displayFov: dataViewFloat(dataView, DISPLAY_FOV), + displayFov: 44.0, // dataViewFloat(dataView, DISPLAY_FOV), lookAheadCfg: dataViewFloatArray(dataView, LOOK_AHEAD_CFG), }; + } else if (keepalive_only) { + this.device_data = { + ...this.device_data, + imuResetState, + enabled, + sbsEnabled + } } let success = keepalive_only; @@ -159,6 +237,8 @@ export const DeviceDataStream = GObject.registerClass({ if (data[0]) { buffer = new Uint8Array(data[1]).buffer; dataView = new DataView(buffer); + imuDateMs = dataViewBigUint(dataView, EPOCH_MS); + imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA); } } } diff --git a/gnome/src/extension.js b/gnome/src/extension.js index 4d4daa5..ea8bd5c 100644 --- a/gnome/src/extension.js +++ b/gnome/src/extension.js @@ -79,7 +79,10 @@ export default class BreezyDesktopExtension extends Extension { } if (!Globals.data_stream) { - Globals.data_stream = new DeviceDataStream(); + Globals.data_stream = new DeviceDataStream({ + debug_no_device: this.settings.get_boolean('debug-no-device') + }); + this.settings.bind('debug-no-device', Globals.data_stream, 'debug-no-device', Gio.SettingsBindFlags.DEFAULT); } } @@ -286,6 +289,7 @@ export default class BreezyDesktopExtension extends Extension { this._cursor_manager = new CursorManager(Main.layoutManager.uiGroup, [targetMonitor, ...virtualMonitors], refreshRate); this._cursor_manager.enable(); + // use rgba(255, 4, 144, 1) for chroma key background this._overlay = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);', reactive: false, clip_to_allocation: true }); this._overlay.opacity = 255; this._overlay.set_position(targetMonitor.x, targetMonitor.y); diff --git a/gnome/src/virtualmonitorsactor.js b/gnome/src/virtualmonitorsactor.js index d95f25e..1addf63 100644 --- a/gnome/src/virtualmonitorsactor.js +++ b/gnome/src/virtualmonitorsactor.js @@ -66,7 +66,7 @@ function degreesToRadians(degrees) { } /*** - * @returns {Object} - containing `start`, `center`, and `end` radians for rotating the given monitor + * @returns {Object} - containing `begin`, `center`, and `end` radians for rotating the given monitor */ function monitorWrap(cachedMonitorWrap, radiusPixels, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels) { let closestWrap = cachedMonitorWrap.reduce((previous, current) => { @@ -125,17 +125,33 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch // distance to a horizontal edge is the hypothenuse of the triangle where the opposite side is half the width of the reference fov screen const edgeRadius = fovDetails.widthPixels / 2 / Math.sin(fovHorizontalRadians / 2); + const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels; cachedMonitorWrap.push({ pixel: 0, radians: -fovHorizontalRadians / 2 }); monitorDetailsList.forEach(monitorDetails => { - const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacing * fovDetails.widthPixels, monitorDetails.x, monitorDetails.width); - const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.width / 2, 2)) + const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacingPixels, monitorDetails.x, monitorDetails.width); + const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.width / 2, 2)); + const upTopPixels = monitorDetails.y + (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels; + const upCenterPixels = upTopPixels + monitorDetails.height / 2 - fovDetails.heightPixels / 2; monitorPlacements.push({ topLeftNoRotate: [ monitorCenterRadius, + + // west stays aligned with (0, 0), will apply rotationAngleRadians value during rendering -(monitorDetails.width - fovDetails.widthPixels) / 2, - -monitorDetails.y + + // up is flat when wrapping horizontally, apply it here as a constant, not touched by rendering + -upTopPixels + ], + centerNoRotate: [ + monitorCenterRadius, + + // west centered about the FOV center + 0, + + // up is flat when wrapping horizontally + -upCenterPixels ], center: [ // north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians @@ -145,7 +161,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch -monitorCenterRadius * Math.sin(monitorWrapDetails.center), // up is flat when wrapping horizontally - -(monitorDetails.y + monitorDetails.height / 2 - fovDetails.heightPixels / 2) + -upCenterPixels ], rotationAngleRadians: { x: 0, @@ -158,24 +174,40 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch // distance to a vertical edge is the hypothenuse of the triangle where the opposite side is half the height of the reference fov screen const edgeRadius = fovDetails.heightPixels / 2 / Math.sin(fovVerticalRadians / 2); + const monitorSpacingPixels = monitorSpacing * fovDetails.heightPixels; cachedMonitorWrap.push({ pixel: 0, radians: -fovVerticalRadians / 2 }); monitorDetailsList.forEach(monitorDetails => { - const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacing * fovDetails.heightPixels, monitorDetails.y, monitorDetails.height); - const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.height / 2, 2)) ; + const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacingPixels, monitorDetails.y, monitorDetails.height); + const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.height / 2, 2)); + const westPixels = monitorDetails.x + (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels; + const westCenterPixels = westPixels + monitorDetails.width / 2 - fovDetails.widthPixels / 2; monitorPlacements.push({ topLeftNoRotate: [ monitorCenterRadius, - monitorDetails.x, + + // west is flat when wrapping vertically, apply it here as a constant, not touched by rendering + westPixels, + + // up stays aligned with (0, 0), will apply rotationAngleRadians value during rendering (monitorDetails.height - fovDetails.heightPixels) / 2 ], + centerNoRotate: [ + monitorCenterRadius, + + // west is flat when wrapping horizontally + westCenterPixels, + + // west centered about the FOV center + 0 + ], center: [ // north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians monitorCenterRadius * Math.cos(monitorWrapDetails.center), // west is flat when wrapping vertically - -(monitorDetails.x + monitorDetails.width / 2 - fovDetails.widthPixels / 2), + -westCenterPixels, // up is opposite where radius is the hypotenuse, using monitorWrapDetails.center as the radians -monitorCenterRadius * Math.sin(monitorWrapDetails.center) @@ -187,18 +219,29 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch }); }); } else { + const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels; + // monitors make a flat wall in front of us, no wrapping monitorDetailsList.forEach(monitorDetails => { + const upPixels = monitorDetails.y + (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels; + const westPixels = monitorDetails.x + (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels; + const westCenterPixels = westPixels + monitorDetails.width / 2 - fovDetails.widthPixels / 2; + const upCenterPixels = upPixels + monitorDetails.height / 2 - fovDetails.heightPixels / 2; monitorPlacements.push({ topLeftNoRotate: [ centerRadius, - monitorDetails.x, - -monitorDetails.y + westPixels, + -upPixels + ], + centerNoRotate: [ + centerRadius, + westCenterPixels, + -upCenterPixels ], center: [ centerRadius, - -(monitorDetails.x + monitorDetails.width / 2 - fovDetails.widthPixels / 2), - -(monitorDetails.y + monitorDetails.height / 2 - fovDetails.heightPixels / 2) + -westCenterPixels, + -upCenterPixels ], rotationAngleRadians: { x: 0, @@ -387,12 +430,17 @@ export const VirtualMonitorEffect = GObject.registerClass({ _update_display_position_uniforms() { // this is in NWU coordinates - const noRotationVector = this.monitor_placements[this.monitor_index].topLeftNoRotate; - Globals.logger.log_debug(`\t\t\tMonitor ${this.monitor_index} vectors: ${JSON.stringify(this.monitor_placements[this.monitor_index])}`); + const monitorPlacement = this.monitor_placements[this.monitor_index]; + // Globals.logger.log_debug(`\t\t\tMonitor ${this.monitor_index} vectors: ${JSON.stringify(monitorPlacement)}`); + + // use the center vector with the distance applied to determine how much to move each coordinate, so they all move uniformly + const inverseAppliedDistance = 1.0 - this._current_display_distance / this.display_distance_default; + const distanceDelta = monitorPlacement.centerNoRotate.map(coord => coord * inverseAppliedDistance); + const noRotationVector = monitorPlacement.topLeftNoRotate.map((coord, index) => coord - distanceDelta[index]); // convert to CoGL's east-down-south coordinates and apply display distance this.set_uniform_float(this.get_uniform_location("u_display_position"), 3, - [-noRotationVector[1], -noRotationVector[2], this._current_display_distance / this.display_distance_default * -noRotationVector[0]]); + [-noRotationVector[1], -noRotationVector[2], -noRotationVector[0]]); const rotation_radians = this.monitor_placements[this.monitor_index].rotationAngleRadians; this.set_uniform_float(this.get_uniform_location("u_rotation_x_radians"), 1, [rotation_radians.x]); diff --git a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml index 429d0e9..a1a2fea 100644 --- a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml +++ b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml @@ -208,6 +208,15 @@ Log debug messages + + + false + + Debug no device + + Debug no device + + "" diff --git a/ui/src/window.py b/ui/src/window.py index 502f7dc..03e3cb1 100644 --- a/ui/src/window.py +++ b/ui/src/window.py @@ -22,6 +22,7 @@ from .extensionsmanager import ExtensionsManager from .license import BREEZY_GNOME_FEATURES from .licensedialog import LicenseDialog from .statemanager import StateManager +from .settingsmanager import SettingsManager from .connecteddevice import ConnectedDevice from .failedverification import FailedVerification from .nodevice import NoDevice @@ -45,11 +46,13 @@ class BreezydesktopWindow(Gtk.ApplicationWindow): self._skip_verification = skip_verification + self.settings = SettingsManager.get_instance().settings self.state_manager = StateManager.get_instance() self.state_manager.connect('device-update', self._handle_state_update) self.state_manager.connect('notify::license-action-needed', self._handle_state_update) self.state_manager.connect('notify::license-present', self._handle_state_update) self.state_manager.connect('notify::enabled-features-list', self._handle_state_update) + self.settings.connect('changed::debug-no-device', self._handle_settings_update) self.connected_device = ConnectedDevice() self.failed_verification = FailedVerification() @@ -67,6 +70,9 @@ class BreezydesktopWindow(Gtk.ApplicationWindow): self.connect("destroy", self._on_window_destroy) + def _handle_settings_update(self, settings_manager, key): + self._handle_state_update(self.state_manager, None) + def _handle_state_update(self, state_manager, val): GLib.idle_add(self._handle_state_update_gui, state_manager) @@ -83,6 +89,9 @@ class BreezydesktopWindow(Gtk.ApplicationWindow): self.main_content.append(self.failed_verification) elif not ExtensionsManager.get_instance().is_installed(): self.main_content.append(self.no_extension) + elif self.settings.get_boolean('debug-no-device'): + self.main_content.append(self.connected_device) + self.connected_device.set_device_name('Fake device') elif not self.state_manager.driver_running: self.main_content.append(self.no_driver) elif not self.state_manager.license_present: