From 1ba9185a59ac89579de13f700083d723bb463f02 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:20:28 -0700 Subject: [PATCH] Refactor XREffect and other logic out into their own modules --- .gitignore | 3 +- .../breezydesktop@org.xronlinux/extension.js | 285 +----------------- .../breezydesktop@org.xronlinux/fxaaEffect.js | 0 gnome/breezydesktop@org.xronlinux/globals.js | 5 + gnome/breezydesktop@org.xronlinux/ipc.js | 45 +++ gnome/breezydesktop@org.xronlinux/math.js | 3 + gnome/breezydesktop@org.xronlinux/shader.js | 14 + gnome/breezydesktop@org.xronlinux/time.js | 3 + gnome/breezydesktop@org.xronlinux/xrEffect.js | 232 ++++++++++++++ 9 files changed, 318 insertions(+), 272 deletions(-) create mode 100644 gnome/breezydesktop@org.xronlinux/fxaaEffect.js create mode 100644 gnome/breezydesktop@org.xronlinux/globals.js create mode 100644 gnome/breezydesktop@org.xronlinux/ipc.js create mode 100644 gnome/breezydesktop@org.xronlinux/math.js create mode 100644 gnome/breezydesktop@org.xronlinux/shader.js create mode 100644 gnome/breezydesktop@org.xronlinux/time.js create mode 100644 gnome/breezydesktop@org.xronlinux/xrEffect.js diff --git a/.gitignore b/.gitignore index 6f4d703..1119e10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vulkan/build/ -/build/ \ No newline at end of file +/build/ +/gnome/setup-45.sh diff --git a/gnome/breezydesktop@org.xronlinux/extension.js b/gnome/breezydesktop@org.xronlinux/extension.js index d5de026..2b0c5fe 100644 --- a/gnome/breezydesktop@org.xronlinux/extension.js +++ b/gnome/breezydesktop@org.xronlinux/extension.js @@ -1,230 +1,23 @@ import Clutter from 'gi://Clutter' import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; -import GObject from 'gi://GObject'; import Meta from 'gi://Meta'; import Shell from 'gi://Shell'; import St from 'gi://St'; import { CursorManager } from './cursormanager.js'; +import Globals from './globals.js'; +import { IPC_FILE_PATH, XREffect } from './xrEffect.js'; import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; -const UINT8_SIZE = 1; -const BOOL_SIZE = UINT8_SIZE; -const UINT_SIZE = 4; -const FLOAT_SIZE = 4; - -const DATA_VIEW_INFO_OFFSET_INDEX = 0; -const DATA_VIEW_INFO_SIZE_INDEX = 1; -const DATA_VIEW_INFO_COUNT_INDEX = 2; - -// computes the end offset, exclusive -function dataViewEnd(dataViewInfo) { - return dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX] + dataViewInfo[DATA_VIEW_INFO_SIZE_INDEX] * dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; -} - -// the driver should be using the same data layout version -const DATA_LAYOUT_VERSION = 1; - -// DataView info: [offset, size, count] -const VERSION = [0, UINT8_SIZE, 1]; -const ENABLED = [dataViewEnd(VERSION), BOOL_SIZE, 1]; -const EPOCH_SEC = [dataViewEnd(ENABLED), UINT_SIZE, 1]; -const LOOK_AHEAD_CFG = [dataViewEnd(EPOCH_SEC), FLOAT_SIZE, 4]; -const DISPLAY_RES = [dataViewEnd(LOOK_AHEAD_CFG), UINT_SIZE, 2]; -const DISPLAY_FOV = [dataViewEnd(DISPLAY_RES), FLOAT_SIZE, 1]; -const DISPLAY_ZOOM = [dataViewEnd(DISPLAY_FOV), FLOAT_SIZE, 1]; -const DISPLAY_NORTH_OFFSET = [dataViewEnd(DISPLAY_ZOOM), FLOAT_SIZE, 1]; -const LENS_DISTANCE_RATIO = [dataViewEnd(DISPLAY_NORTH_OFFSET), FLOAT_SIZE, 1]; -const SBS_ENABLED = [dataViewEnd(LENS_DISTANCE_RATIO), BOOL_SIZE, 1]; -const SBS_CONTENT = [dataViewEnd(SBS_ENABLED), BOOL_SIZE, 1]; -const SBS_MODE_STRETCHED = [dataViewEnd(SBS_CONTENT), BOOL_SIZE, 1]; -const CUSTOM_BANNER_ENABLED = [dataViewEnd(SBS_MODE_STRETCHED), BOOL_SIZE, 1]; -const IMU_QUAT_DATA = [dataViewEnd(CUSTOM_BANNER_ENABLED), FLOAT_SIZE, 16]; -const DATA_VIEW_LENGTH = dataViewEnd(IMU_QUAT_DATA); - -// cached after first retrieval -const shaderUniformLocations = { - 'enabled': null, - 'show_banner': null, - 'imu_quat_data': null, - 'look_ahead_cfg': null, - 'stage_aspect_ratio': null, - 'display_aspect_ratio': null, - 'trim_width_percent': null, - 'trim_height_percent': null, - 'display_zoom': null, - 'display_north_offset': null, - 'lens_distance_ratio': null, - 'sbs_enabled': null, - 'sbs_content': null, - 'sbs_mode_stretched': null, - 'custom_banner_enabled': null, - 'half_fov_z_rads': null, - 'half_fov_y_rads': null, - 'screen_distance': null, - 'frametime': null -}; - -function dataViewUint8(dataView, dataViewInfo) { - return dataView.getUint8(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX]); -} - -function dataViewUint(dataView, dataViewInfo) { - return dataView.getUint32(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true); -} - -function dataViewUintArray(dataView, dataViewInfo) { - const uintArray = [] - let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX]; - for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) { - uintArray.push(dataView.getUint32(offset, true)); - offset += UINT_SIZE; - } - return uintArray; -} - -function dataViewFloat(dataView, dataViewInfo) { - return dataView.getFloat32(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true); -} - -function dataViewFloatArray(dataView, dataViewInfo) { - const floatArray = [] - let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX]; - for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) { - floatArray.push(dataView.getFloat32(offset, true)); - offset += FLOAT_SIZE; - } - return floatArray; -} - - -function getShaderSource(path) { - const file = Gio.file_new_for_path(path); - const data = file.load_contents(null); - - // version string helps with linting, but GNOME extension doesn't like it, so remove it if it's there - // - // TODO - Gjs on GNOME 45.5 WARNING: Some code called array.toString() on a Uint8Array instance. Previously this - // would have interpreted the bytes of the array as a string, but that is nonstandard. In the future this - // will return the bytes as comma-separated digits. For the time being, the old behavior has been preserved, - // but please fix your code anyway to use TextDecoder. - return data[1].toString().replace(/^#version .*$/gm, '') + '\n'; -} - -function transferUniformBoolean(effect, locationName, dataView, dataViewInfo) { - // GLSL bool is a float under the hood, evaluates false if 0 or 0.0, true otherwise - effect.set_uniform_float(locationName, 1, [dataViewUint8(dataView, dataViewInfo)]); -} - -function setUniformFloat(effect, locationName, dataViewInfo, value) { - effect.set_uniform_float(shaderUniformLocations[locationName], dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX], value); -} - -function transferUniformFloat(effect, locationName, dataView, dataViewInfo) { - setUniformFloat(effect, locationName, dataViewInfo, dataViewFloatArray(dataView, dataViewInfo)); -} - -function setSingleFloat(effect, locationName, value) { - effect.set_uniform_float(shaderUniformLocations[locationName], 1, [value]); -} - -function setUniformMatrix(effect, locationName, components, dataView, dataViewInfo) { - const numValues = dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; - if (numValues / components !== components) { - throw new Error('Invalid matrix size'); - } - - const floatArray = [].fill(0, 0, numValues); - let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX]; - for (let i = 0; i < numValues; i++) { - // GLSL uses column-major order, so we need to transpose the matrix - const row = i % components; - const column = Math.floor(i / components); - - floatArray[row * components + column] = dataView.getFloat32(offset, true); - offset += FLOAT_SIZE; - } - effect.set_uniform_matrix(shaderUniformLocations[locationName], true, components, floatArray); -} - -function getEpochSec() { - return Math.floor(Date.now() / 1000); -} - -function degreeToRadian(degree) { - return degree * Math.PI / 180; -} - - -// most uniforms don't change frequently, this function should be called periodically -function setIntermittentUniformVariables() { - const dataView = this._dataView; - - if (dataView.byteLength === DATA_VIEW_LENGTH) { - const version = dataViewUint8(dataView, VERSION); - const date = dataViewUint(dataView, EPOCH_SEC); - const validKeepalive = Math.abs(getEpochSec() - date) < 5; - const imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA); - const imuResetState = 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 && !imuResetState; - - if (enabled) { - const displayRes = dataViewUintArray(dataView, DISPLAY_RES); - const displayFov = dataViewFloat(dataView, DISPLAY_FOV); - const lensDistanceRatio = dataViewFloat(dataView, LENS_DISTANCE_RATIO); - - // compute these values once, they only change when the XR device changes - const displayAspectRatio = displayRes[0] / displayRes[1]; - const stageAspectRatio = this._targetMonitor.width / this._targetMonitor.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; - - // 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 = 2.0 / this._targetMonitor.width; - const trimHeightPercent = 2.0 / this._targetMonitor.height; - - // all these values are transferred directly, unmodified from the driver - transferUniformFloat(this, 'look_ahead_cfg', dataView, LOOK_AHEAD_CFG); - transferUniformFloat(this, 'display_zoom', dataView, DISPLAY_ZOOM); - transferUniformFloat(this, 'display_north_offset', dataView, DISPLAY_NORTH_OFFSET); - transferUniformFloat(this, 'lens_distance_ratio', dataView, LENS_DISTANCE_RATIO); - transferUniformBoolean(this, 'sbs_enabled', dataView, SBS_ENABLED); - transferUniformBoolean(this, 'sbs_content', dataView, SBS_CONTENT); - transferUniformBoolean(this, 'sbs_mode_stretched', dataView, SBS_MODE_STRETCHED); - transferUniformBoolean(this, 'custom_banner_enabled', dataView, CUSTOM_BANNER_ENABLED); - - // computed values with no dataViewInfo, so we set these manually - setSingleFloat(this, 'show_banner', imuResetState); - 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); - setSingleFloat(this, 'frametime', this._frametime); - } - setSingleFloat(this, 'enabled', enabled ? 1.0 : 0.0); - } else if (dataView.byteLength !== 0) { - console.error(`Invalid dataView.byteLength: ${dataView.byteLength} !== ${DATA_VIEW_LENGTH}`) - } -} - - export default class BreezyDesktopExtension extends Extension { constructor(metadata, uuid) { super(metadata, uuid); - this._extensionPath = metadata.path; // Set/destroyed by enable/disable this._cursorManager = null; - this._shared_mem_file = null; this._xr_effect = null; this._overlay = null; } @@ -246,22 +39,24 @@ export default class BreezyDesktopExtension extends Extension { } _check_driver_running() { - if (!this._shared_mem_file) this._shared_mem_file = Gio.file_new_for_path("/dev/shm/imu_data"); - return this._shared_mem_file.query_exists(null); + if (!Globals.ipc_file) Globals.ipc_file = Gio.file_new_for_path(IPC_FILE_PATH); + return Globals.ipc_file.query_exists(null); } _effect_enable() { + if (!Globals.extension_dir) Globals.extension_dir = this.metadata.path; + if (!this._cursorManager) this._cursorManager = new CursorManager(Main.layoutManager.uiGroup); this._cursorManager.enable(); if (!this._overlay) { const monitors = Main.layoutManager.monitors; - this._targetMonitor = monitors[monitors.length-1]; + this._target_monitor = monitors[monitors.length-1]; this._overlay = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);'}); this._overlay.opacity = 255; - this._overlay.set_position(this._targetMonitor.x, this._targetMonitor.y); - this._overlay.set_size(this._targetMonitor.width, this._targetMonitor.height); + this._overlay.set_position(this._target_monitor.x, this._target_monitor.y); + this._overlay.set_size(this._target_monitor.width, this._target_monitor.height); const overlayContent = new Clutter.Actor({clip_to_allocation: true}); const uiClone = new Clutter.Clone({ source: Main.layoutManager.uiGroup, clip_to_allocation: true }); @@ -272,67 +67,15 @@ export default class BreezyDesktopExtension extends Extension { global.stage.insert_child_above(this._overlay, null); Shell.util_set_hidden_from_pick(this._overlay, true); - uiClone.x = -this._targetMonitor.x; - uiClone.y = -this._targetMonitor.y; + uiClone.x = -this._target_monitor.x; + uiClone.y = -this._target_monitor.y; } if (!this._xr_effect) { - const extensionPath = this._extensionPath; - const shared_mem_file = this._shared_mem_file; - const targetMonitor = this._targetMonitor; - var XREffect = GObject.registerClass({}, class XREffect extends Shell.GLSLEffect { - vfunc_build_pipeline() { - const code = getShaderSource(`${extensionPath}/IMUAdjust.frag`); - const main = 'PS_IMU_Transform(vec4(0, 0, 0, 0), cogl_tex_coord_in[0].xy, cogl_color_out);'; - this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, code, main, false); - - this._frametime = Math.floor(1000 / 90); // 90 FPS - this._targetMonitor = targetMonitor; - } - - vfunc_paint_target(node, paintContext) { - var now = Date.now(); - var lastPaint = this._last_paint || 0; - var frametime = this._frametime; - const data = shared_mem_file.load_contents(null); - if (data[0]) { - const 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]); - for (let key in shaderUniformLocations) { - shaderUniformLocations[key] = this.get_uniform_location(key); - } - this.setIntermittentUniformVariables = setIntermittentUniformVariables.bind(this); - this.setIntermittentUniformVariables(); - - GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._frametime, () => { - if ((now - lastPaint) > frametime) global.stage.queue_redraw(); - return GLib.SOURCE_CONTINUE; - }); - - GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, (() => { - this.setIntermittentUniformVariables(); - return GLib.SOURCE_CONTINUE; - }).bind(this)); - this._initialized = true; - } - - if (this._dataView.byteLength === DATA_VIEW_LENGTH) { - setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA); - } else if (this._dataView.byteLength !== 0) { - console.error(`Invalid dataView.byteLength: ${this._dataView.byteLength} !== ${DATA_VIEW_LENGTH}`) - } - - super.vfunc_paint_target(node, paintContext); - } else { - super.vfunc_paint_target(node, paintContext); - } - this._last_paint = now; - } + this._xr_effect = new XREffect({ + target_monitor: this._target_monitor, + target_framerate: 60 }); - - this._xr_effect = new XREffect(); } this._overlay.add_effect_with_name('xr-desktop', this._xr_effect); diff --git a/gnome/breezydesktop@org.xronlinux/fxaaEffect.js b/gnome/breezydesktop@org.xronlinux/fxaaEffect.js new file mode 100644 index 0000000..e69de29 diff --git a/gnome/breezydesktop@org.xronlinux/globals.js b/gnome/breezydesktop@org.xronlinux/globals.js new file mode 100644 index 0000000..b30b510 --- /dev/null +++ b/gnome/breezydesktop@org.xronlinux/globals.js @@ -0,0 +1,5 @@ +const Globals = { + ipc_file: null, // Gio.File instance, file exists + extension_dir: null // string path +} +export default Globals; \ No newline at end of file diff --git a/gnome/breezydesktop@org.xronlinux/ipc.js b/gnome/breezydesktop@org.xronlinux/ipc.js new file mode 100644 index 0000000..f1fe3c0 --- /dev/null +++ b/gnome/breezydesktop@org.xronlinux/ipc.js @@ -0,0 +1,45 @@ +export const UINT8_SIZE = 1; +export const BOOL_SIZE = UINT8_SIZE; +export const UINT_SIZE = 4; +export const FLOAT_SIZE = 4; + +export const DATA_VIEW_INFO_OFFSET_INDEX = 0; +export const DATA_VIEW_INFO_SIZE_INDEX = 1; +export const DATA_VIEW_INFO_COUNT_INDEX = 2; + +// computes the end offset, exclusive +export function dataViewEnd(dataViewInfo) { + return dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX] + dataViewInfo[DATA_VIEW_INFO_SIZE_INDEX] * dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; +} + +export function dataViewUint8(dataView, dataViewInfo) { + return dataView.getUint8(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX]); +} + +export function dataViewUint(dataView, dataViewInfo) { + return dataView.getUint32(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true); +} + +export function dataViewUintArray(dataView, dataViewInfo) { + const uintArray = [] + let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX]; + for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) { + uintArray.push(dataView.getUint32(offset, true)); + offset += UINT_SIZE; + } + return uintArray; +} + +export function dataViewFloat(dataView, dataViewInfo) { + return dataView.getFloat32(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true); +} + +export function dataViewFloatArray(dataView, dataViewInfo) { + const floatArray = [] + let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX]; + for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) { + floatArray.push(dataView.getFloat32(offset, true)); + offset += FLOAT_SIZE; + } + return floatArray; +} \ No newline at end of file diff --git a/gnome/breezydesktop@org.xronlinux/math.js b/gnome/breezydesktop@org.xronlinux/math.js new file mode 100644 index 0000000..497274e --- /dev/null +++ b/gnome/breezydesktop@org.xronlinux/math.js @@ -0,0 +1,3 @@ +export function degreeToRadian(degree) { + return degree * Math.PI / 180; +} \ No newline at end of file diff --git a/gnome/breezydesktop@org.xronlinux/shader.js b/gnome/breezydesktop@org.xronlinux/shader.js new file mode 100644 index 0000000..b906762 --- /dev/null +++ b/gnome/breezydesktop@org.xronlinux/shader.js @@ -0,0 +1,14 @@ +import Gio from 'gi://Gio'; + +export function getShaderSource(path) { + const file = Gio.file_new_for_path(path); + const data = file.load_contents(null); + + // version string helps with linting, but GNOME extension doesn't like it, so remove it if it's there + // + // TODO - Gjs on GNOME 45.5 WARNING: Some code called array.toString() on a Uint8Array instance. Previously this + // would have interpreted the bytes of the array as a string, but that is nonstandard. In the future this + // will return the bytes as comma-separated digits. For the time being, the old behavior has been preserved, + // but please fix your code anyway to use TextDecoder. + return data[1].toString().replace(/^#version .*$/gm, '') + '\n'; +} \ No newline at end of file diff --git a/gnome/breezydesktop@org.xronlinux/time.js b/gnome/breezydesktop@org.xronlinux/time.js new file mode 100644 index 0000000..b4cd219 --- /dev/null +++ b/gnome/breezydesktop@org.xronlinux/time.js @@ -0,0 +1,3 @@ +export function getEpochSec() { + return Math.floor(Date.now() / 1000); +} \ No newline at end of file diff --git a/gnome/breezydesktop@org.xronlinux/xrEffect.js b/gnome/breezydesktop@org.xronlinux/xrEffect.js new file mode 100644 index 0000000..0c5c1ef --- /dev/null +++ b/gnome/breezydesktop@org.xronlinux/xrEffect.js @@ -0,0 +1,232 @@ +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Shell from 'gi://Shell'; + +import Globals from './globals.js'; +import { + dataViewEnd, + dataViewUint8, + dataViewUint, + dataViewUintArray, + dataViewFloat, + dataViewFloatArray, + BOOL_SIZE, + DATA_VIEW_INFO_COUNT_INDEX, + DATA_VIEW_INFO_OFFSET_INDEX, + FLOAT_SIZE, + UINT_SIZE, + UINT8_SIZE +} from "./ipc.js"; +import { degreeToRadian } from "./math.js"; +import { getShaderSource } from "./shader.js"; +import { getEpochSec } from "./time.js"; + +export const IPC_FILE_PATH = "/dev/shm/imu_data"; + +// the driver should be using the same data layout version +const DATA_LAYOUT_VERSION = 1; + +// DataView info: [offset, size, count] +const VERSION = [0, UINT8_SIZE, 1]; +const ENABLED = [dataViewEnd(VERSION), BOOL_SIZE, 1]; +const EPOCH_SEC = [dataViewEnd(ENABLED), UINT_SIZE, 1]; +const LOOK_AHEAD_CFG = [dataViewEnd(EPOCH_SEC), FLOAT_SIZE, 4]; +const DISPLAY_RES = [dataViewEnd(LOOK_AHEAD_CFG), UINT_SIZE, 2]; +const DISPLAY_FOV = [dataViewEnd(DISPLAY_RES), FLOAT_SIZE, 1]; +const DISPLAY_ZOOM = [dataViewEnd(DISPLAY_FOV), FLOAT_SIZE, 1]; +const DISPLAY_NORTH_OFFSET = [dataViewEnd(DISPLAY_ZOOM), FLOAT_SIZE, 1]; +const LENS_DISTANCE_RATIO = [dataViewEnd(DISPLAY_NORTH_OFFSET), FLOAT_SIZE, 1]; +const SBS_ENABLED = [dataViewEnd(LENS_DISTANCE_RATIO), BOOL_SIZE, 1]; +const SBS_CONTENT = [dataViewEnd(SBS_ENABLED), BOOL_SIZE, 1]; +const SBS_MODE_STRETCHED = [dataViewEnd(SBS_CONTENT), BOOL_SIZE, 1]; +const CUSTOM_BANNER_ENABLED = [dataViewEnd(SBS_MODE_STRETCHED), BOOL_SIZE, 1]; +const IMU_QUAT_DATA = [dataViewEnd(CUSTOM_BANNER_ENABLED), FLOAT_SIZE, 16]; +const DATA_VIEW_LENGTH = dataViewEnd(IMU_QUAT_DATA); + +// cached after first retrieval +const shaderUniformLocations = { + 'enabled': null, + 'show_banner': null, + 'imu_quat_data': null, + 'look_ahead_cfg': null, + 'stage_aspect_ratio': null, + 'display_aspect_ratio': null, + 'trim_width_percent': null, + 'trim_height_percent': null, + 'display_zoom': null, + 'display_north_offset': null, + 'lens_distance_ratio': null, + 'sbs_enabled': null, + 'sbs_content': null, + 'sbs_mode_stretched': null, + 'custom_banner_enabled': null, + 'half_fov_z_rads': null, + 'half_fov_y_rads': null, + 'screen_distance': null, + 'frametime': null +}; + +function transferUniformBoolean(effect, location, dataView, dataViewInfo) { + // GLSL bool is a float under the hood, evaluates false if 0 or 0.0, true otherwise + effect.set_uniform_float(location, 1, [dataViewUint8(dataView, dataViewInfo)]); +} + +function setUniformFloat(effect, locationName, dataViewInfo, value) { + effect.set_uniform_float(shaderUniformLocations[locationName], dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX], value); +} + +function transferUniformFloat(effect, locationName, dataView, dataViewInfo) { + setUniformFloat(effect, locationName, dataViewInfo, dataViewFloatArray(dataView, dataViewInfo)); +} + +function setSingleFloat(effect, locationName, value) { + effect.set_uniform_float(shaderUniformLocations[locationName], 1, [value]); +} + +function setUniformMatrix(effect, locationName, components, dataView, dataViewInfo) { + const numValues = dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; + if (numValues / components !== components) { + throw new Error('Invalid matrix size'); + } + + const floatArray = [].fill(0, 0, numValues); + let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX]; + for (let i = 0; i < numValues; i++) { + // GLSL uses column-major order, so we need to transpose the matrix + const row = i % components; + const column = Math.floor(i / components); + + floatArray[row * components + column] = dataView.getFloat32(offset, true); + offset += FLOAT_SIZE; + } + effect.set_uniform_matrix(shaderUniformLocations[locationName], true, components, floatArray); +} + +// most uniforms don't change frequently, this function should be called periodically +function setIntermittentUniformVariables() { + const dataView = this._dataView; + + if (dataView.byteLength === DATA_VIEW_LENGTH) { + const version = dataViewUint8(dataView, VERSION); + const date = dataViewUint(dataView, EPOCH_SEC); + const validKeepalive = Math.abs(getEpochSec() - date) < 5; + const imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA); + const imuResetState = 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 && !imuResetState; + + if (enabled) { + const displayRes = dataViewUintArray(dataView, DISPLAY_RES); + const displayFov = dataViewFloat(dataView, DISPLAY_FOV); + const lensDistanceRatio = dataViewFloat(dataView, LENS_DISTANCE_RATIO); + + // 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; + + // 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 = 2.0 / this.target_monitor.width; + const trimHeightPercent = 2.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, 'display_zoom', dataView, DISPLAY_ZOOM); + transferUniformFloat(this, 'display_north_offset', dataView, DISPLAY_NORTH_OFFSET); + transferUniformFloat(this, 'lens_distance_ratio', dataView, LENS_DISTANCE_RATIO); + transferUniformBoolean(this, 'sbs_enabled', dataView, SBS_ENABLED); + transferUniformBoolean(this, 'sbs_content', dataView, SBS_CONTENT); + transferUniformBoolean(this, 'sbs_mode_stretched', dataView, SBS_MODE_STRETCHED); + transferUniformBoolean(this, 'custom_banner_enabled', dataView, CUSTOM_BANNER_ENABLED); + + // computed values with no dataViewInfo, so we set these manually + setSingleFloat(this, 'show_banner', imuResetState); + 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); + setSingleFloat(this, 'frametime', this._frametime); + } + setSingleFloat(this, 'enabled', enabled ? 1.0 : 0.0); + } else if (dataView.byteLength !== 0) { + console.error(`Invalid dataView.byteLength: ${dataView.byteLength} !== ${DATA_VIEW_LENGTH}`) + } +} + +export const XREffect = GObject.registerClass({ + Properties: { + 'target-monitor': GObject.ParamSpec.jsobject( + 'target-monitor', + 'Target Monitor', + 'Geometry of the target monitor for this effect', + GObject.ParamFlags.READWRITE + ), + 'target-framerate': GObject.ParamSpec.uint( + 'target-framerate', + 'Target Framerate', + 'Target framerate for this effect', + GObject.ParamFlags.READWRITE, 60, 240, 60 + ) + } +}, class XREffect extends Shell.GLSLEffect { + constructor(params = {}) { + super(params); + + console.log(`\n\n\nXREffect constructor called ${JSON.stringify(params)}\n\n\n`); + this._frametime = Math.floor(1000 / this.target_framerate); + } + + vfunc_build_pipeline() { + console.log(`\n\n\nXREffect vfunc_build_pipeline called ${Globals.extension_dir}\n\n\n`); + const code = getShaderSource(`${Globals.extension_dir}/IMUAdjust.frag`); + const main = 'PS_IMU_Transform(vec4(0, 0, 0, 0), cogl_tex_coord_in[0].xy, cogl_color_out);'; + this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, code, main, false); + } + + vfunc_paint_target(node, paintContext) { + var now = Date.now(); + var lastPaint = this._last_paint || 0; + var frametime = this._frametime; + const data = Globals.ipc_file.load_contents(null); + if (data[0]) { + const 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]); + for (let key in shaderUniformLocations) { + shaderUniformLocations[key] = this.get_uniform_location(key); + } + this.setIntermittentUniformVariables = setIntermittentUniformVariables.bind(this); + this.setIntermittentUniformVariables(); + + GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._frametime, () => { + if ((now - lastPaint) > frametime) global.stage.queue_redraw(); + return GLib.SOURCE_CONTINUE; + }); + + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, (() => { + this.setIntermittentUniformVariables(); + return GLib.SOURCE_CONTINUE; + }).bind(this)); + this._initialized = true; + } + + if (this._dataView.byteLength === DATA_VIEW_LENGTH) { + setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA); + } else if (this._dataView.byteLength !== 0) { + console.error(`Invalid dataView.byteLength: ${this._dataView.byteLength} !== ${DATA_VIEW_LENGTH}`) + } + + super.vfunc_paint_target(node, paintContext); + } else { + super.vfunc_paint_target(node, paintContext); + } + this._last_paint = now; + } +}); \ No newline at end of file