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;
}
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);
}
}
}

View File

@ -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);

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) {
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]);

View File

@ -208,6 +208,15 @@
Log debug messages
</description>
</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">
<default>
""

View File

@ -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: