Compare commits
11 Commits
main
...
breezy-gno
| Author | SHA1 | Date |
|---|---|---|
|
|
7c6d7ef84d | |
|
|
0779ffe7d2 | |
|
|
f5e08e4ba6 | |
|
|
82276b3ae3 | |
|
|
2f0a053a4a | |
|
|
7da53847cb | |
|
|
fe828db999 | |
|
|
a109b5e897 | |
|
|
8ba4f790c0 | |
|
|
4f80efdf75 | |
|
|
47294824b9 |
|
|
@ -0,0 +1,176 @@
|
|||
#version 330 core
|
||||
|
||||
uniform bool enabled;
|
||||
uniform bool show_banner;
|
||||
uniform sampler2D uDesktopTexture;
|
||||
uniform mat4 imu_quat_data;
|
||||
uniform vec4 look_ahead_cfg;
|
||||
uniform float display_zoom;
|
||||
uniform float display_north_offset;
|
||||
uniform float lens_distance_ratio;
|
||||
uniform bool sbs_enabled;
|
||||
uniform bool sbs_content;
|
||||
uniform bool sbs_mode_stretched;
|
||||
uniform bool custom_banner_enabled;
|
||||
uniform float stage_aspect_ratio;
|
||||
uniform float display_aspect_ratio;
|
||||
uniform float half_fov_z_rads;
|
||||
uniform float half_fov_y_rads;
|
||||
uniform float screen_distance;
|
||||
uniform float frametime;
|
||||
|
||||
float look_ahead_ms_cap = 45.0;
|
||||
|
||||
vec4 quatMul(vec4 q1, vec4 q2) {
|
||||
vec3 u = vec3(q1.x, q1.y, q1.z);
|
||||
float s = q1.w;
|
||||
vec3 v = vec3(q2.x, q2.y, q2.z);
|
||||
float t = q2.w;
|
||||
return vec4(s*v + t*u + cross(u, v), s*t - dot(u, v));
|
||||
}
|
||||
|
||||
vec4 quatConj(vec4 q) {
|
||||
return vec4(-q.x, -q.y, -q.z, q.w);
|
||||
}
|
||||
|
||||
vec3 applyQuaternionToVector(vec4 q, vec3 v) {
|
||||
vec4 p = quatMul(quatMul(q, vec4(v, 0)), quatConj(q));
|
||||
return p.xyz;
|
||||
}
|
||||
|
||||
const int day_in_seconds = 24 * 60 * 60;
|
||||
|
||||
vec3 applyLookAhead(
|
||||
in vec3 position,
|
||||
in vec3 velocity,
|
||||
in vec3 accel,
|
||||
in float t,
|
||||
in float t_squared) {
|
||||
return position + velocity * t + 0.5 * accel * t_squared;
|
||||
}
|
||||
|
||||
vec3 rateOfChange(
|
||||
in vec3 v1,
|
||||
in vec3 v2,
|
||||
in float delta_time) {
|
||||
return (v1 - v2) / delta_time;
|
||||
}
|
||||
|
||||
void PS_IMU_Transform(vec4 pos, vec2 texcoord, out vec4 color) {
|
||||
float texcoord_x_min = 0.0;
|
||||
float texcoord_x_max = 1.0;
|
||||
float lens_y_offset = 0.0;
|
||||
float lens_z_offset = 0.0;
|
||||
float aspect_ratio = stage_aspect_ratio;
|
||||
|
||||
if (enabled && sbs_enabled) {
|
||||
bool right_display = texcoord.x > 0.5;
|
||||
aspect_ratio /= 2;
|
||||
|
||||
lens_y_offset = lens_distance_ratio / 3;
|
||||
if (right_display) lens_y_offset = -lens_y_offset;
|
||||
if (sbs_content) {
|
||||
// source video is SBS, left-half of the screen goes to the left lens, right-half to the right lens
|
||||
if (right_display)
|
||||
texcoord_x_min = 0.5;
|
||||
else
|
||||
texcoord_x_max = 0.5;
|
||||
}
|
||||
if (!sbs_mode_stretched) {
|
||||
// if the content isn't stretched, assume it's centered in the middle 50% of the screen
|
||||
texcoord_x_min = max(0.25, texcoord_x_min);
|
||||
texcoord_x_max = min(0.75, texcoord_x_max);
|
||||
}
|
||||
|
||||
// translate the texcoord respresenting the current lens's half of the screen to a full-screen texcoord
|
||||
texcoord.x = (texcoord.x - (right_display ? 0.5 : 0.0)) * 2;
|
||||
}
|
||||
|
||||
if (!enabled || show_banner) {
|
||||
// vec2 banner_size = vec2(800.0 / ReShade::ScreenSize.x, 200.0 / ReShade::ScreenSize.y); // Assuming ScreenWidth and ScreenHeight are defined
|
||||
|
||||
// if (show_banner &&
|
||||
// texcoord.x >= banner_position.x - banner_size.x / 2 &&
|
||||
// texcoord.x <= banner_position.x + banner_size.x / 2 &&
|
||||
// texcoord.y >= banner_position.y - banner_size.y / 2 &&
|
||||
// texcoord.y <= banner_position.y + banner_size.y / 2)
|
||||
// {
|
||||
// vec2 banner_texcoord = (texcoord - (banner_position - banner_size / 2)) / banner_size;
|
||||
// if (custom_banner_enabled) {
|
||||
// color = tex2D(customBannerSampler, banner_texcoord);
|
||||
// } else {
|
||||
// color = tex2D(calibratingSampler, banner_texcoord);
|
||||
// }
|
||||
// } else {
|
||||
// adjust texcoord back to the range that describes where the content is displayed
|
||||
float texcoord_width = texcoord_x_max - texcoord_x_min;
|
||||
texcoord.x = texcoord.x * texcoord_width + texcoord_x_min;
|
||||
|
||||
color = texture2D(uDesktopTexture, texcoord);
|
||||
// }
|
||||
} else {
|
||||
float lens_fov_z_offset_rads = atan(lens_z_offset/screen_distance);
|
||||
float fov_z_pos = tan(half_fov_z_rads - lens_fov_z_offset_rads) * screen_distance;
|
||||
float fov_z_neg = -tan(half_fov_z_rads + lens_fov_z_offset_rads) * screen_distance;
|
||||
float fov_z_width = fov_z_pos - fov_z_neg;
|
||||
|
||||
float lens_fov_y_offset_rads = atan(lens_y_offset/screen_distance);
|
||||
float fov_y_pos = tan(half_fov_y_rads - lens_fov_y_offset_rads) * screen_distance;
|
||||
float fov_y_neg = -tan(half_fov_y_rads + lens_fov_y_offset_rads) * screen_distance;
|
||||
float fov_y_width = fov_y_pos - fov_y_neg;
|
||||
float vec_x = screen_distance;
|
||||
float vec_y = -texcoord.x * fov_y_width + fov_y_pos;
|
||||
float vec_z = -texcoord.y * fov_z_width + fov_z_pos;
|
||||
vec3 texcoord_vector = vec3(vec_x, vec_y, vec_z);
|
||||
vec3 lens_vector = vec3(lens_distance_ratio, lens_y_offset, lens_z_offset);
|
||||
|
||||
// then rotate the vector using each of the snapshots provided
|
||||
vec3 rotated_vector_t0 = applyQuaternionToVector(imu_quat_data[0], texcoord_vector);
|
||||
vec3 rotated_vector_t1 = applyQuaternionToVector(imu_quat_data[1], texcoord_vector);
|
||||
vec3 rotated_vector_t2 = applyQuaternionToVector(imu_quat_data[2], texcoord_vector);
|
||||
vec3 rotated_lens_vector = applyQuaternionToVector(imu_quat_data[0], lens_vector);
|
||||
|
||||
// compute the two velocities (units/ms) as change in the 3 rotation snapshots
|
||||
float delta_time_t0 = imu_quat_data[3].x - imu_quat_data[3].y;
|
||||
vec3 velocity_t0 = rateOfChange(rotated_vector_t0, rotated_vector_t1, delta_time_t0);
|
||||
vec3 velocity_t1 = rateOfChange(rotated_vector_t1, rotated_vector_t2, imu_quat_data[3].y - imu_quat_data[3].z);
|
||||
|
||||
// and then the acceleration (units/ms^2) as the change in velocities
|
||||
vec3 accel_t0 = rateOfChange(velocity_t0, velocity_t1, delta_time_t0);
|
||||
|
||||
// allows for the bottom and top of the screen to have different look-ahead values
|
||||
float look_ahead_scanline_adjust = texcoord.y * look_ahead_cfg.z;
|
||||
|
||||
// use the 4th value of the look-ahead config to cap the look-ahead value
|
||||
float look_ahead_ms = min(min(look_ahead_cfg.x + frametime * look_ahead_cfg.y, look_ahead_cfg.w), look_ahead_ms_cap) + look_ahead_scanline_adjust;
|
||||
float look_ahead_ms_squared = pow(look_ahead_ms, 2);
|
||||
|
||||
// apply most recent velocity and acceleration to most recent position to get a predicted position
|
||||
vec3 res = applyLookAhead(rotated_vector_t0, velocity_t0, accel_t0, look_ahead_ms, look_ahead_ms_squared);
|
||||
|
||||
bool looking_behind = res.x < 0.0;
|
||||
|
||||
// divide all values by x to scale the magnitude so x is exactly 1, and multiply by the final display distance
|
||||
// so the vector is pointing at a coordinate on the screen
|
||||
float display_distance = (sbs_enabled ? display_north_offset : 1.0) - rotated_lens_vector.x;
|
||||
res *= display_distance/res.x;
|
||||
|
||||
// adjust x and y by how much our lens moved from its original offset
|
||||
res += rotated_lens_vector - lens_vector;
|
||||
|
||||
// deconstruct the rotated and scaled vector back to a texcoord (just inverse operations of the first conversion
|
||||
// above)
|
||||
texcoord.x = (fov_y_pos - res.y) / fov_y_width;
|
||||
texcoord.y = (fov_z_pos - res.z) / fov_z_width;
|
||||
|
||||
// apply the screen offsets now
|
||||
float texcoord_width = texcoord_x_max - texcoord_x_min;
|
||||
texcoord.x = texcoord.x * texcoord_width + texcoord_x_min;
|
||||
|
||||
if (looking_behind || texcoord.x < texcoord_x_min || texcoord.y < 0.0 || texcoord.x > texcoord_x_max || texcoord.y > 1.0 || texcoord.x <= 0.005 && texcoord.y <= 0.005) {
|
||||
color = vec4(0, 0, 0, 1);
|
||||
} else {
|
||||
color = texture2D(uDesktopTexture, texcoord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// Taken from https://github.com/jkitching/soft-brightness-plus
|
||||
//
|
||||
// Copyright (C) 2023 Joel Kitching (jkitching on Github)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import Clutter from 'gi://Clutter';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
// Copied almost verbatim from ui/magnifier.js.
|
||||
export const MouseSpriteContent = GObject.registerClass({
|
||||
Implements: [Clutter.Content],
|
||||
}, class MouseSpriteContent extends GObject.Object {
|
||||
_init() {
|
||||
super._init();
|
||||
this._texture = null;
|
||||
}
|
||||
|
||||
vfunc_get_preferred_size() {
|
||||
if (!this._texture)
|
||||
return [false, 0, 0];
|
||||
|
||||
return [true, this._texture.get_width(), this._texture.get_height()];
|
||||
}
|
||||
|
||||
vfunc_paint_content(actor, node, _paintContext) {
|
||||
if (!this._texture)
|
||||
return;
|
||||
|
||||
let color = Clutter.Color.get_static(Clutter.StaticColor.WHITE);
|
||||
let [minFilter, magFilter] = actor.get_content_scaling_filters();
|
||||
let textureNode = new Clutter.TextureNode(this._texture,
|
||||
color, minFilter, magFilter);
|
||||
textureNode.set_name('BreezyDesktopSpriteContent');
|
||||
node.add_child(textureNode);
|
||||
|
||||
textureNode.add_rectangle(actor.get_content_box());
|
||||
}
|
||||
|
||||
get texture() {
|
||||
return this._texture;
|
||||
}
|
||||
|
||||
set texture(coglTexture) {
|
||||
if (this._texture === coglTexture)
|
||||
return;
|
||||
|
||||
let oldTexture = this._texture;
|
||||
this._texture = coglTexture;
|
||||
this.invalidate();
|
||||
|
||||
if (!oldTexture || !coglTexture ||
|
||||
oldTexture.get_width() !== coglTexture.get_width() ||
|
||||
oldTexture.get_height() !== coglTexture.get_height())
|
||||
this.invalidate_size();
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
|
||||
|
||||
import Clutter from 'gi://Clutter';
|
||||
import GLib from 'gi://GLib';
|
||||
import Meta from 'gi://Meta';
|
||||
import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.js';
|
||||
import { MouseSpriteContent } from './cursor.js';
|
||||
|
||||
// Taken from https://github.com/jkitching/soft-brightness-plus
|
||||
export class CursorManager {
|
||||
constructor(mainActor) {
|
||||
this._mainActor = mainActor;
|
||||
|
||||
this._enableTimeoutId = null;
|
||||
this._changeHookFn = null;
|
||||
|
||||
// Set/destroyed by _enableCloningMouse/_disableCloningMouse
|
||||
this._cursorWantedVisible = null;
|
||||
this._cursorTracker = null;
|
||||
this._cursorTrackerSetPointerVisible = null;
|
||||
this._cursorTrackerSetPointerVisibleBound = null;
|
||||
this._cursorSprite = null;
|
||||
this._cursorActor = null;
|
||||
this._cursorWatcher = null;
|
||||
this._cursorSeat = null;
|
||||
// Set/destroyed by _startCloningMouse / _stopCloningMouse
|
||||
this._cursorWatch = null;
|
||||
this._cursorChangedConnection = null;
|
||||
this._cursorVisibilityChangedConnection = null;
|
||||
}
|
||||
|
||||
enable() {
|
||||
// First 500ms: For some reason, starting the mouse cloning at this
|
||||
// stage fails when gnome-shell is restarting on x11 and the mouse
|
||||
// listener doesn't receive any events. Adding a small delay before
|
||||
// starting the whole mouse cloning business helps.
|
||||
this._enableTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
|
||||
// Wait 500ms before starting to check for the _brightness object.
|
||||
this._enableTimeoutId = null;
|
||||
this._enable();
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}
|
||||
|
||||
_enable() {
|
||||
this._enableCloningMouse();
|
||||
this.startCloning();
|
||||
}
|
||||
|
||||
disable() {
|
||||
// If _enableTimeoutId is non-null, _enable() has not run yet, and will
|
||||
// not run. Do not run _disable() in this case.
|
||||
GLib.source_remove(this._enableTimeoutId);
|
||||
if (this._enableTimeoutId !== null) {
|
||||
return;
|
||||
}
|
||||
this._enableTimeoutId = null;
|
||||
this._disableCloningMouse();
|
||||
|
||||
// Set/destroyed by _enableCloningMouse/_disableCloningMouse
|
||||
this._cursorWantedVisible = null;
|
||||
this._cursorTracker = null;
|
||||
this._cursorTrackerSetPointerVisible = null;
|
||||
this._cursorTrackerSetPointerVisibleBound = null;
|
||||
this._cursorSprite = null;
|
||||
this._cursorActor = null;
|
||||
this._cursorWatcher = null;
|
||||
this._cursorSeat = null;
|
||||
// Set/destroyed by _startCloningMouse / _stopCloningMouse
|
||||
this._cursorWatch = null;
|
||||
this._cursorChangedConnection = null;
|
||||
this._cursorVisibilityChangedConnection = null;
|
||||
}
|
||||
|
||||
startCloning() {
|
||||
if (this._cursorWantedVisible) {
|
||||
this._startCloningMouse();
|
||||
}
|
||||
}
|
||||
|
||||
stopCloning() {
|
||||
this._stopCloningShowMouse();
|
||||
}
|
||||
|
||||
_enableCloningMouse() {
|
||||
this._cursorWantedVisible = true;
|
||||
this._cursorTracker = Meta.CursorTracker.get_for_display(global.display);
|
||||
this._cursorTrackerSetPointerVisible = Meta.CursorTracker.prototype.set_pointer_visible;
|
||||
this._cursorTrackerSetPointerVisibleBound = this._cursorTrackerSetPointerVisible.bind(this._cursorTracker);
|
||||
Meta.CursorTracker.prototype.set_pointer_visible = this._cursorTrackerSetPointerVisibleReplacement.bind(this);
|
||||
this._cursorTrackerSetPointerVisibleBound(false);
|
||||
|
||||
this._cursorSprite = new Clutter.Actor({ request_mode: Clutter.RequestMode.CONTENT_SIZE });
|
||||
this._cursorSprite.content = new MouseSpriteContent();
|
||||
|
||||
this._cursorActor = new Clutter.Actor();
|
||||
this._cursorActor.add_actor(this._cursorSprite);
|
||||
this._cursorWatcher = PointerWatcher.getPointerWatcher();
|
||||
this._cursorSeat = Clutter.get_default_backend().get_default_seat();
|
||||
}
|
||||
|
||||
_disableCloningMouse() {
|
||||
this._cursorTrackerSetPointerVisibleBound(this._cursorWantedVisible);
|
||||
Meta.CursorTracker.prototype.set_pointer_visible = this._cursorTrackerSetPointerVisible;
|
||||
|
||||
this._cursorWantedVisible = null;
|
||||
this._cursorTracker = null;
|
||||
this._cursorTrackerSetPointerVisible = null;
|
||||
this._cursorTrackerSetPointerVisibleBound = null;
|
||||
this._cursorSprite = null;
|
||||
this._cursorActor = null;
|
||||
this._cursorWatcher = null;
|
||||
this._cursorSeat = null;
|
||||
}
|
||||
|
||||
_cursorTrackerSetPointerVisibleReplacement(visible) {
|
||||
this._cursorWantedVisible = visible;
|
||||
if (visible) {
|
||||
this._startCloningMouse();
|
||||
} else {
|
||||
this._stopCloningMouse();
|
||||
}
|
||||
}
|
||||
|
||||
_startCloningMouse() {
|
||||
if (this._cursorWatch == null) {
|
||||
this._mainActor.add_actor(this._cursorActor);
|
||||
this._cursorChangedConnection = this._cursorTracker.connect('cursor-changed', this._updateMouseSprite.bind(this));
|
||||
this._cursorVisibilityChangedConnection = this._cursorTracker.connect('visibility-changed', this._updateMouseSprite.bind(this));
|
||||
const interval = 1000 / 100;
|
||||
this._cursorWatch = this._cursorWatcher.addWatch(interval, this._updateMousePosition.bind(this));
|
||||
|
||||
this._updateMouseSprite();
|
||||
}
|
||||
|
||||
if (this._cursorTracker.set_keep_focus_while_hidden) {
|
||||
this._cursorTracker.set_keep_focus_while_hidden(true);
|
||||
}
|
||||
|
||||
if (!this._cursorSeat.is_unfocus_inhibited()) {
|
||||
this._cursorSeat.inhibit_unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
_stopCloningShowMouse() {
|
||||
this._stopCloningMouse();
|
||||
this._cursorTrackerSetPointerVisibleBound(this._cursorWantedVisible);
|
||||
|
||||
if (this._cursorTracker.set_keep_focus_while_hidden) {
|
||||
this._cursorTracker.set_keep_focus_while_hidden(false);
|
||||
}
|
||||
|
||||
if (this._cursorSeat.is_unfocus_inhibited()) {
|
||||
this._cursorSeat.uninhibit_unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
_stopCloningMouse() {
|
||||
if (this._cursorWatch != null) {
|
||||
this._cursorWatch.remove();
|
||||
this._cursorWatch = null;
|
||||
|
||||
this._cursorTracker.disconnect(this._cursorChangedConnection);
|
||||
this._cursorChangedConnection = null;
|
||||
|
||||
this._cursorTracker.disconnect(this._cursorVisibilityChangedConnection);
|
||||
this._cursorVisibilityChangedConnection = null;
|
||||
|
||||
this._mainActor.remove_actor(this._cursorActor);
|
||||
}
|
||||
}
|
||||
|
||||
_updateMousePosition(x, y) {
|
||||
this._cursorActor.set_position(x, y);
|
||||
}
|
||||
|
||||
_updateMouseSprite() {
|
||||
const sprite = this._cursorTracker.get_sprite();
|
||||
if (sprite) {
|
||||
this._cursorSprite.content.texture = sprite;
|
||||
this._cursorSprite.show();
|
||||
} else {
|
||||
this._cursorSprite.hide();
|
||||
}
|
||||
|
||||
const [xHot, yHot] = this._cursorTracker.get_hot();
|
||||
this._cursorSprite.set({
|
||||
translation_x: -xHot,
|
||||
translation_y: -yHot,
|
||||
});
|
||||
this._cursorTrackerSetPointerVisibleBound(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import Shell from 'gi://Shell';
|
||||
import Meta from 'gi://Meta';
|
||||
|
||||
import { CursorManager } from './cursormanager.js';
|
||||
|
||||
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.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,
|
||||
'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
|
||||
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 = global.stage.get_width() / global.stage.get_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;
|
||||
|
||||
// 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, '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;
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (!this._check_driver_running()) {
|
||||
this._running_poller_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, (() => {
|
||||
if (this._check_driver_running()) {
|
||||
this._effect_enable();
|
||||
this._running_poller_id = undefined;
|
||||
return GLib.SOURCE_REMOVE;
|
||||
} else {
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
}
|
||||
}).bind(this));
|
||||
} else {
|
||||
this._effect_enable();
|
||||
}
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
_effect_enable() {
|
||||
if (!this._cursorManager) this._cursorManager = new CursorManager(global.stage);
|
||||
this._cursorManager.enable();
|
||||
|
||||
if (!this._xr_effect) {
|
||||
const extensionPath = this._extensionPath;
|
||||
const shared_mem_file = this._shared_mem_file;
|
||||
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 = 10; // 100 FPS
|
||||
}
|
||||
|
||||
vfunc_paint_target(node, paintContext) {
|
||||
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();
|
||||
|
||||
this._repaint_needed = false;
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._frametime, () => {
|
||||
this._repaint_needed = true;
|
||||
this.queue_repaint();
|
||||
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}`)
|
||||
}
|
||||
|
||||
if (this._repaint_needed) {
|
||||
super.vfunc_paint_target(node, paintContext);
|
||||
this._repaint_needed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._xr_effect = new XREffect();
|
||||
}
|
||||
|
||||
global.stage.add_effect_with_name('xr-desktop', this._xr_effect);
|
||||
Meta.disable_unredirect_for_display(global.display);
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (this._running_poller_id) {
|
||||
GLib.source_remove(this._running_poller_id);
|
||||
} else {
|
||||
Meta.enable_unredirect_for_display(global.display);
|
||||
global.stage.remove_effect_by_name('xr-desktop');
|
||||
this._cursorManager.disable();
|
||||
this._cursorManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
return new Extension();
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"uuid": "breezydesktop@org.xronlinux",
|
||||
"name": "Breezy GNOME XR Desktop",
|
||||
"description": "XR virtual desktop for GNOME.",
|
||||
"shell-version": [
|
||||
"45", "46"
|
||||
],
|
||||
"url": "https://github.com/wheaney/breezy-desktop"
|
||||
}
|
||||
Loading…
Reference in New Issue