Add support for testing without glasses connected, fix zoom so it always comes towards the camera

This commit is contained in:
wheaney 2025-02-12 14:04:44 -08:00
parent ed6981567c
commit efdb6b464c
5 changed files with 170 additions and 20 deletions

View File

@ -51,6 +51,28 @@ function checkParityByte(dataView) {
return parityByte === parity; 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({ export const DeviceDataStream = GObject.registerClass({
Properties: { Properties: {
'breezy-desktop-running': GObject.ParamSpec.boolean( 'breezy-desktop-running': GObject.ParamSpec.boolean(
@ -72,6 +94,13 @@ export const DeviceDataStream = GObject.registerClass({
'IMU Snapshots', 'IMU Snapshots',
'Latest IMU quaternion snapshots and epoch timestamp for when it was collected', 'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
GObject.ParamFlags.READWRITE 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 { }, class DeviceDataStream extends GObject.Object {
@ -90,6 +119,7 @@ export const DeviceDataStream = GObject.registerClass({
stop() { stop() {
this._running = false; 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 // 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 // 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. // hasn't been checked within KEEPALIVE_REFRESH_INTERVAL_SEC.
refresh_data(keepalive_only = false) { 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) && ( if (this._ipc_file.query_exists(null) && (
!this.device_data?.imuData || !this.device_data?.imuData ||
!keepalive_only || !keepalive_only ||
@ -114,9 +185,9 @@ export const DeviceDataStream = GObject.registerClass({
let buffer = new Uint8Array(data[1]).buffer; let buffer = new Uint8Array(data[1]).buffer;
let dataView = new DataView(buffer); let dataView = new DataView(buffer);
if (dataView.byteLength === DATA_VIEW_LENGTH) { if (dataView.byteLength === DATA_VIEW_LENGTH) {
const imuDateMs = dataViewBigUint(dataView, EPOCH_MS); let imuDateMs = dataViewBigUint(dataView, EPOCH_MS);
const validKeepalive = isValidKeepAlive(toSec(imuDateMs)); 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 imuResetState = validKeepalive && imuData[0] === 0.0 && imuData[1] === 0.0 && imuData[2] === 0.0 && imuData[3] === 1.0;
const version = dataViewUint8(dataView, VERSION); const version = dataViewUint8(dataView, VERSION);
const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive; const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive;
@ -132,9 +203,16 @@ export const DeviceDataStream = GObject.registerClass({
imuResetState, imuResetState,
displayRes: dataViewUint32Array(dataView, DISPLAY_RES), displayRes: dataViewUint32Array(dataView, DISPLAY_RES),
sbsEnabled, sbsEnabled,
displayFov: dataViewFloat(dataView, DISPLAY_FOV), displayFov: 44.0, // dataViewFloat(dataView, DISPLAY_FOV),
lookAheadCfg: dataViewFloatArray(dataView, LOOK_AHEAD_CFG), lookAheadCfg: dataViewFloatArray(dataView, LOOK_AHEAD_CFG),
}; };
} else if (keepalive_only) {
this.device_data = {
...this.device_data,
imuResetState,
enabled,
sbsEnabled
}
} }
let success = keepalive_only; let success = keepalive_only;
@ -159,6 +237,8 @@ export const DeviceDataStream = GObject.registerClass({
if (data[0]) { if (data[0]) {
buffer = new Uint8Array(data[1]).buffer; buffer = new Uint8Array(data[1]).buffer;
dataView = new DataView(buffer); dataView = new DataView(buffer);
imuDateMs = dataViewBigUint(dataView, EPOCH_MS);
imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA);
} }
} }
} }

View File

@ -79,7 +79,10 @@ export default class BreezyDesktopExtension extends Extension {
} }
if (!Globals.data_stream) { 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 = new CursorManager(Main.layoutManager.uiGroup, [targetMonitor, ...virtualMonitors], refreshRate);
this._cursor_manager.enable(); 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 = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);', reactive: false, clip_to_allocation: true });
this._overlay.opacity = 255; this._overlay.opacity = 255;
this._overlay.set_position(targetMonitor.x, targetMonitor.y); this._overlay.set_position(targetMonitor.x, targetMonitor.y);

View File

@ -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) { function monitorWrap(cachedMonitorWrap, radiusPixels, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels) {
let closestWrap = cachedMonitorWrap.reduce((previous, current) => { 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 // 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 edgeRadius = fovDetails.widthPixels / 2 / Math.sin(fovHorizontalRadians / 2);
const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels;
cachedMonitorWrap.push({ pixel: 0, radians: -fovHorizontalRadians / 2 }); cachedMonitorWrap.push({ pixel: 0, radians: -fovHorizontalRadians / 2 });
monitorDetailsList.forEach(monitorDetails => { monitorDetailsList.forEach(monitorDetails => {
const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacing * fovDetails.widthPixels, monitorDetails.x, monitorDetails.width); 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 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({ monitorPlacements.push({
topLeftNoRotate: [ topLeftNoRotate: [
monitorCenterRadius, monitorCenterRadius,
// west stays aligned with (0, 0), will apply rotationAngleRadians value during rendering
-(monitorDetails.width - fovDetails.widthPixels) / 2, -(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: [ center: [
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians // 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), -monitorCenterRadius * Math.sin(monitorWrapDetails.center),
// up is flat when wrapping horizontally // up is flat when wrapping horizontally
-(monitorDetails.y + monitorDetails.height / 2 - fovDetails.heightPixels / 2) -upCenterPixels
], ],
rotationAngleRadians: { rotationAngleRadians: {
x: 0, 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 // 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 edgeRadius = fovDetails.heightPixels / 2 / Math.sin(fovVerticalRadians / 2);
const monitorSpacingPixels = monitorSpacing * fovDetails.heightPixels;
cachedMonitorWrap.push({ pixel: 0, radians: -fovVerticalRadians / 2 }); cachedMonitorWrap.push({ pixel: 0, radians: -fovVerticalRadians / 2 });
monitorDetailsList.forEach(monitorDetails => { monitorDetailsList.forEach(monitorDetails => {
const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacing * fovDetails.heightPixels, monitorDetails.y, monitorDetails.height); 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 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({ monitorPlacements.push({
topLeftNoRotate: [ topLeftNoRotate: [
monitorCenterRadius, 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 (monitorDetails.height - fovDetails.heightPixels) / 2
], ],
centerNoRotate: [
monitorCenterRadius,
// west is flat when wrapping horizontally
westCenterPixels,
// west centered about the FOV center
0
],
center: [ center: [
// north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians // north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
monitorCenterRadius * Math.cos(monitorWrapDetails.center), monitorCenterRadius * Math.cos(monitorWrapDetails.center),
// west is flat when wrapping vertically // 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 // up is opposite where radius is the hypotenuse, using monitorWrapDetails.center as the radians
-monitorCenterRadius * Math.sin(monitorWrapDetails.center) -monitorCenterRadius * Math.sin(monitorWrapDetails.center)
@ -187,18 +219,29 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch
}); });
}); });
} else { } else {
const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels;
// monitors make a flat wall in front of us, no wrapping // monitors make a flat wall in front of us, no wrapping
monitorDetailsList.forEach(monitorDetails => { 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({ monitorPlacements.push({
topLeftNoRotate: [ topLeftNoRotate: [
centerRadius, centerRadius,
monitorDetails.x, westPixels,
-monitorDetails.y -upPixels
],
centerNoRotate: [
centerRadius,
westCenterPixels,
-upCenterPixels
], ],
center: [ center: [
centerRadius, centerRadius,
-(monitorDetails.x + monitorDetails.width / 2 - fovDetails.widthPixels / 2), -westCenterPixels,
-(monitorDetails.y + monitorDetails.height / 2 - fovDetails.heightPixels / 2) -upCenterPixels
], ],
rotationAngleRadians: { rotationAngleRadians: {
x: 0, x: 0,
@ -387,12 +430,17 @@ export const VirtualMonitorEffect = GObject.registerClass({
_update_display_position_uniforms() { _update_display_position_uniforms() {
// this is in NWU coordinates // this is in NWU coordinates
const noRotationVector = this.monitor_placements[this.monitor_index].topLeftNoRotate; const monitorPlacement = this.monitor_placements[this.monitor_index];
Globals.logger.log_debug(`\t\t\tMonitor ${this.monitor_index} vectors: ${JSON.stringify(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 // convert to CoGL's east-down-south coordinates and apply display distance
this.set_uniform_float(this.get_uniform_location("u_display_position"), 3, 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; 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]); this.set_uniform_float(this.get_uniform_location("u_rotation_x_radians"), 1, [rotation_radians.x]);

View File

@ -208,6 +208,15 @@
Log debug messages Log debug messages
</description> </description>
</key> </key>
<key name="debug-no-device" type="b">
<default>
false
</default>
<summary>Debug no device</summary>
<description>
Debug no device
</description>
</key>
<key name="custom-monitor-product" type="s"> <key name="custom-monitor-product" type="s">
<default> <default>
"" ""

View File

@ -22,6 +22,7 @@ from .extensionsmanager import ExtensionsManager
from .license import BREEZY_GNOME_FEATURES from .license import BREEZY_GNOME_FEATURES
from .licensedialog import LicenseDialog from .licensedialog import LicenseDialog
from .statemanager import StateManager from .statemanager import StateManager
from .settingsmanager import SettingsManager
from .connecteddevice import ConnectedDevice from .connecteddevice import ConnectedDevice
from .failedverification import FailedVerification from .failedverification import FailedVerification
from .nodevice import NoDevice from .nodevice import NoDevice
@ -45,11 +46,13 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
self._skip_verification = skip_verification self._skip_verification = skip_verification
self.settings = SettingsManager.get_instance().settings
self.state_manager = StateManager.get_instance() self.state_manager = StateManager.get_instance()
self.state_manager.connect('device-update', self._handle_state_update) 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-action-needed', self._handle_state_update)
self.state_manager.connect('notify::license-present', 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.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.connected_device = ConnectedDevice()
self.failed_verification = FailedVerification() self.failed_verification = FailedVerification()
@ -67,6 +70,9 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
self.connect("destroy", self._on_window_destroy) 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): def _handle_state_update(self, state_manager, val):
GLib.idle_add(self._handle_state_update_gui, state_manager) 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) self.main_content.append(self.failed_verification)
elif not ExtensionsManager.get_instance().is_installed(): elif not ExtensionsManager.get_instance().is_installed():
self.main_content.append(self.no_extension) 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: elif not self.state_manager.driver_running:
self.main_content.append(self.no_driver) self.main_content.append(self.no_driver)
elif not self.state_manager.license_present: elif not self.state_manager.license_present: