From c222c25ddfa5bf303e0b37aea5a7e53480082602 Mon Sep 17 00:00:00 2001
From: galister <22305755+galister@users.noreply.github.com>
Date: Thu, 22 Jan 2026 00:51:23 +0900
Subject: [PATCH] angle fade (partial, need monado fix)
---
wayvr/src/assets/gui/edit.xml | 1 +
wayvr/src/assets/lang/en.json | 2 +
wayvr/src/backend/openvr/mod.rs | 12 +-----
wayvr/src/backend/openxr/helpers.rs | 5 +++
wayvr/src/backend/openxr/mod.rs | 14 +++---
wayvr/src/backend/openxr/overlay.rs | 66 ++++++++++++++++++++++++++++-
wayvr/src/overlays/edit/mod.rs | 14 ++++++
wayvr/src/overlays/watch.rs | 16 +------
wayvr/src/subsystem/osc.rs | 2 +-
wayvr/src/windowing/manager.rs | 49 +++++++++++++++------
wayvr/src/windowing/window.rs | 26 +++++++++++-
wlx-common/src/windowing.rs | 2 +
12 files changed, 159 insertions(+), 50 deletions(-)
diff --git a/wayvr/src/assets/gui/edit.xml b/wayvr/src/assets/gui/edit.xml
index 0abe10a..ca50072 100644
--- a/wayvr/src/assets/gui/edit.xml
+++ b/wayvr/src/assets/gui/edit.xml
@@ -114,6 +114,7 @@
diff --git a/wayvr/src/assets/lang/en.json b/wayvr/src/assets/lang/en.json
index a09a5a5..55525d4 100644
--- a/wayvr/src/assets/lang/en.json
+++ b/wayvr/src/assets/lang/en.json
@@ -26,6 +26,8 @@
"ADJUST_CURVATURE": "Adjust curvature",
"ALPHA_BLEND_MODE": "Alpha blend mode",
"BLENDING_ADDITIVE": "Additive blending",
+ "ANGLE_FADE": "Angle fade",
+ "ANGLE_FADE_HELP": "Fade when not facing HMD",
"CURVATURE": "Curvature",
"DELETE": "Long press to remove from current set",
"DISABLE_GRAB": "Disable grab",
diff --git a/wayvr/src/backend/openvr/mod.rs b/wayvr/src/backend/openvr/mod.rs
index 9eef177..cc6ca6b 100644
--- a/wayvr/src/backend/openvr/mod.rs
+++ b/wayvr/src/backend/openvr/mod.rs
@@ -30,10 +30,7 @@ use crate::{
},
config::{save_settings, save_state},
graphics::{GpuFutures, init_openvr_graphics},
- overlays::{
- toast::Toast,
- watch::{WATCH_NAME, watch_fade},
- },
+ overlays::toast::Toast,
state::AppState,
subsystem::notifications::NotificationManager,
windowing::{
@@ -134,8 +131,6 @@ pub fn openvr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
log::info!("HMD running @ {refresh_rate} Hz");
- let watch_id = overlays.lookup(WATCH_NAME).unwrap(); // want panic
-
// want at least half refresh rate
let frame_timeout = 2 * (1000.0 / refresh_rate).floor() as u32;
@@ -266,11 +261,8 @@ pub fn openvr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
.enqueue(TaskType::Overlay(OverlayTask::ToggleDashboard));
}
- overlays
- .values_mut()
- .for_each(|o| o.config.auto_movement(&mut app));
+ overlays.values_mut().for_each(|o| o.config.tick(&mut app));
- watch_fade(&mut app, overlays.mut_by_id(watch_id).unwrap()); // want panic
playspace.update(&mut chaperone_mgr, &mut overlays, &app);
current_lines.clear();
diff --git a/wayvr/src/backend/openxr/helpers.rs b/wayvr/src/backend/openxr/helpers.rs
index 5d1905e..722e25c 100644
--- a/wayvr/src/backend/openxr/helpers.rs
+++ b/wayvr/src/backend/openxr/helpers.rs
@@ -52,6 +52,11 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
} else {
log::warn!("Missing EXT_composition_layer_equirect2 extension.");
}
+ if available_extensions.khr_composition_layer_color_scale_bias {
+ enabled_extensions.khr_composition_layer_color_scale_bias = true;
+ } else {
+ log::warn!("Missing XR_KHR_composition_layer_color_scale_bias extension.");
+ }
let xr_mndx_system_buttons = "XR_MNDX_system_buttons".as_bytes().to_vec();
if available_extensions.other.contains(&xr_mndx_system_buttons) {
diff --git a/wayvr/src/backend/openxr/mod.rs b/wayvr/src/backend/openxr/mod.rs
index 8c71106..40fb5d6 100644
--- a/wayvr/src/backend/openxr/mod.rs
+++ b/wayvr/src/backend/openxr/mod.rs
@@ -22,10 +22,7 @@ use crate::{
},
config::{save_settings, save_state},
graphics::{GpuFutures, init_openxr_graphics},
- overlays::{
- toast::Toast,
- watch::{WATCH_NAME, watch_fade},
- },
+ overlays::{toast::Toast, watch::WATCH_NAME},
state::AppState,
subsystem::notifications::NotificationManager,
windowing::{
@@ -289,7 +286,6 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
.enqueue(TaskType::Overlay(OverlayTask::ToggleDashboard));
}
- watch_fade(&mut app, overlays.mut_by_id(watch_id).unwrap()); // want panic
if let Some(ref mut space_mover) = playspace {
space_mover.update(&mut overlays, &mut app);
}
@@ -317,9 +313,7 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
.submit(&mut app);
}
- overlays
- .values_mut()
- .for_each(|o| o.config.auto_movement(&mut app));
+ overlays.values_mut().for_each(|o| o.config.tick(&mut app));
current_lines.clear();
@@ -372,6 +366,10 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
log::trace!("{}: hidden, skip render", o.config.name);
continue;
};
+ if alpha < 0.05 {
+ log::trace!("{}: alpha too low, skip render", o.config.name);
+ continue;
+ }
if !o.data.init {
log::trace!("{}: init", o.config.name);
diff --git a/wayvr/src/backend/openxr/overlay.rs b/wayvr/src/backend/openxr/overlay.rs
index 00fc738..9d19f7d 100644
--- a/wayvr/src/backend/openxr/overlay.rs
+++ b/wayvr/src/backend/openxr/overlay.rs
@@ -18,6 +18,18 @@ pub struct OpenXrOverlayData {
pub(super) init: bool,
pub(super) cur_visible: bool,
pub(super) last_alpha: f32,
+ color_bias_khr: Option
>,
+}
+
+macro_rules! next_chain_insert {
+ ($layer:expr, $payload:expr) => {{
+ let payload_ptr = $payload.as_mut() as *mut _ as *mut xr::sys::BaseInStructure;
+ let new_elem = payload_ptr.as_mut().unwrap();
+ let mut raw = $layer.into_raw();
+ new_elem.next = raw.next as _;
+ raw.next = payload_ptr as *const _;
+ raw
+ }};
}
impl OverlayWindowData {
@@ -110,8 +122,10 @@ impl OverlayWindowData {
let posef = helpers::translation_rotation_to_posef(center_point, quat);
let angle = 2.0 * (scale_x / (2.0 * radius));
+ try_update_color_scale_bias(xr, &mut self.data.color_bias_khr, state.alpha);
+
for sub_image in sub_images {
- let cylinder = xr::CompositionLayerCylinderKHR::new()
+ let mut cylinder = xr::CompositionLayerCylinderKHR::new()
.layer_flags(flags)
.pose(posef)
.sub_image(sub_image.0)
@@ -120,12 +134,22 @@ impl OverlayWindowData {
.radius(radius)
.central_angle(angle)
.aspect_ratio(aspect_ratio);
+
+ if let Some(color_bias_khr) = self.data.color_bias_khr.as_mut() {
+ unsafe {
+ let raw = next_chain_insert!(cylinder, color_bias_khr);
+ cylinder = xr::CompositionLayerCylinderKHR::from_raw(raw);
+ }
+ }
+
layers.push(CompositionLayer::Cylinder(cylinder));
}
} else {
let posef = helpers::transform_to_posef(&transform);
+ try_update_color_scale_bias(xr, &mut self.data.color_bias_khr, state.alpha);
+
for sub_image in sub_images {
- let quad = xr::CompositionLayerQuad::new()
+ let mut quad = xr::CompositionLayerQuad::new()
.layer_flags(flags)
.pose(posef)
.sub_image(sub_image.0)
@@ -135,6 +159,14 @@ impl OverlayWindowData {
width: scale_x,
height: scale_y,
});
+
+ if let Some(color_bias_khr) = self.data.color_bias_khr.as_mut() {
+ unsafe {
+ let raw = next_chain_insert!(quad, color_bias_khr);
+ quad = xr::CompositionLayerQuad::from_raw(raw);
+ }
+ }
+
layers.push(CompositionLayer::Quad(quad));
}
}
@@ -159,3 +191,33 @@ impl OverlayWindowData {
Ok(())
}
}
+
+fn try_update_color_scale_bias(
+ xr_state: &XrState,
+ color_bias_khr: &mut Option>,
+ alpha: f32,
+) {
+ if let Some(item) = color_bias_khr.as_mut() {
+ item.color_scale.a = alpha;
+ return;
+ }
+
+ if xr_state
+ .instance
+ .exts()
+ .khr_composition_layer_color_scale_bias
+ .is_none()
+ {
+ return;
+ }
+ let new_item = Box::new(xr::sys::CompositionLayerColorScaleBiasKHR {
+ ty: xr::StructureType::COMPOSITION_LAYER_COLOR_SCALE_BIAS_KHR,
+ next: std::ptr::null(),
+ color_bias: Default::default(),
+ color_scale: xr::Color4f {
+ a: alpha,
+ ..Default::default()
+ },
+ });
+ *color_bias_khr = Some(new_item);
+}
diff --git a/wayvr/src/overlays/edit/mod.rs b/wayvr/src/overlays/edit/mod.rs
index 597ef1f..c3b5270 100644
--- a/wayvr/src/overlays/edit/mod.rs
+++ b/wayvr/src/overlays/edit/mod.rs
@@ -432,6 +432,7 @@ fn make_edit_panel(app: &mut AppState) -> anyhow::Result {
set_up_checkbox(&mut panel, "additive_box", cb_assign_additive)?;
set_up_checkbox(&mut panel, "align_box", cb_assign_align)?;
set_up_checkbox(&mut panel, "global_box", cb_assign_global)?;
+ set_up_checkbox(&mut panel, "angle_fade_box", cb_assign_angle_fade)?;
set_up_checkbox(&mut panel, "block_input_box", cb_assign_block_input)?;
set_up_checkbox(
&mut panel,
@@ -499,6 +500,11 @@ fn reset_panel(
.fetch_component_as::("global_box")?;
c.set_checked(&mut common, owc.global);
+ let c = panel
+ .parser_state
+ .fetch_component_as::("angle_fade_box")?;
+ c.set_checked(&mut common, state.angle_fade);
+
let c = panel
.parser_state
.fetch_component_as::("block_input_box")?;
@@ -607,6 +613,14 @@ const fn cb_assign_global(_app: &mut AppState, owc: &mut OverlayWindowConfig, gl
owc.global = global;
}
+const fn cb_assign_angle_fade(
+ _app: &mut AppState,
+ owc: &mut OverlayWindowConfig,
+ angle_fade: bool,
+) {
+ owc.active_state.as_mut().unwrap().angle_fade = angle_fade;
+}
+
const fn cb_assign_block_input(
_app: &mut AppState,
owc: &mut OverlayWindowConfig,
diff --git a/wayvr/src/overlays/watch.rs b/wayvr/src/overlays/watch.rs
index 1032164..d7c20d6 100644
--- a/wayvr/src/overlays/watch.rs
+++ b/wayvr/src/overlays/watch.rs
@@ -179,6 +179,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result {
WATCH_ROT,
WATCH_POS,
),
+ angle_fade: true,
..OverlayWindowState::default()
},
show_on_spawn: true,
@@ -213,18 +214,3 @@ fn sets_or_overlays(
alterables.set_style(widget[i], StyleSetRequest::Display(display[i]));
}
}
-
-pub fn watch_fade(app: &mut AppState, watch: &mut OverlayWindowData) {
- let Some(state) = watch.config.active_state.as_mut() else {
- return;
- };
-
- let to_hmd = (state.transform.translation - app.input_state.hmd.translation).normalize();
- let watch_normal = state.transform.transform_vector3a(Vec3A::NEG_Z).normalize();
- let dot = to_hmd.dot(watch_normal);
-
- state.alpha = (dot - app.session.config.watch_view_angle_min)
- / (app.session.config.watch_view_angle_max - app.session.config.watch_view_angle_min);
- state.alpha += 0.1;
- state.alpha = state.alpha.clamp(0., 1.);
-}
diff --git a/wayvr/src/subsystem/osc.rs b/wayvr/src/subsystem/osc.rs
index 0a1f26b..5677f1f 100644
--- a/wayvr/src/subsystem/osc.rs
+++ b/wayvr/src/subsystem/osc.rs
@@ -79,7 +79,7 @@ impl OscSender {
};
// skip overlays that are fully transparent; e.g. the watch when not looking at it
- if state.alpha <= 0f32 {
+ if state.alpha <= 0.05 {
continue;
}
diff --git a/wayvr/src/windowing/manager.rs b/wayvr/src/windowing/manager.rs
index dbd7eb6..45fa4ae 100644
--- a/wayvr/src/windowing/manager.rs
+++ b/wayvr/src/windowing/manager.rs
@@ -12,6 +12,7 @@ use wlx_common::{
astr_containers::{AStrMap, AStrMapExt},
config::SerializedWindowSet,
overlays::{BackendAttrib, BackendAttribValue, ToastTopic},
+ windowing::Positioning,
};
use crate::{
@@ -26,7 +27,7 @@ use crate::{
keyboard::create_keyboard,
screen::create_screens,
toast::Toast,
- watch::create_watch,
+ watch::{WATCH_NAME, create_watch},
},
state::AppState,
windowing::{
@@ -44,6 +45,7 @@ pub struct OverlayWindowManager {
wrappers: EditWrapperManager,
overlays: HopSlotMap>,
sets: Vec,
+ global_set: OverlayWindowSet,
/// The set that is currently visible.
current_set: Option,
/// The set that will be restored by show_hide.
@@ -68,6 +70,7 @@ where
current_set: Some(0),
restore_set: 0,
sets: vec![OverlayWindowSet::default()],
+ global_set: OverlayWindowSet::default(),
anchor_local: Affine3A::from_translation(Vec3::NEG_Z),
watch_id: OverlayID::null(), // set down below
keyboard_id: OverlayID::null(), // set down below
@@ -195,16 +198,19 @@ where
_ => {}
};
+ let parent_set = if o.config.global {
+ &mut self.global_set
+ } else {
+ &mut self.sets[self.restore_set]
+ };
+
if let Some(active_state) = o.config.active_state.take() {
log::debug!("{}: toggle off", o.config.name);
- self.sets[self.restore_set]
+ parent_set
.hidden_overlays
.arc_set(o.config.name.clone(), active_state);
- } else if let Some(state) = self.sets[self.restore_set]
- .hidden_overlays
- .arc_rm(&o.config.name)
- {
+ } else if let Some(state) = parent_set.hidden_overlays.arc_rm(&o.config.name) {
let o = &mut self.overlays[id];
log::debug!("{}: toggle on", o.config.name);
o.config.dirty = true;
@@ -509,15 +515,32 @@ impl OverlayWindowManager {
}
// global overlays
- for oid in &[self.watch_id] {
- if let Some(o) = self.mut_by_id(*oid) {
- if let Some(state) = app.session.config.global_set.get(&*o.config.name).cloned() {
- o.config.active_state = Some(state);
- o.config.reset(app, false);
- log::debug!("global set: loaded state for {}", o.config.name);
+ for (name, ows) in app.session.config.global_set.clone().into_iter() {
+ let mut ows = ows.clone();
+
+ // fix angle_fade missing on watch if loading older state
+ if name.as_ref() == WATCH_NAME {
+ ows.angle_fade = true;
+ }
+
+ if let Some(oid) = self.lookup(&*name)
+ && let Some(o) = self.mut_by_id(oid)
+ {
+ o.config.global = true;
+ if o.config.active_state.is_none() {
+ self.global_set.hidden_overlays.arc_set(name.clone(), ows);
} else {
- log::debug!("global set: no state for {}", o.config.name);
+ o.config.active_state = Some(ows);
+ o.config.reset(app, false);
}
+ log::debug!("global set: loaded state for {name}");
+ } else {
+ log::debug!(
+ "global set has saved state for {name} which doesn't exist. will apply state once added."
+ );
+ self.global_set
+ .inactive_overlays
+ .arc_set(name.clone(), ows.clone());
}
}
diff --git a/wayvr/src/windowing/window.rs b/wayvr/src/windowing/window.rs
index cbcc1df..9b48ac7 100644
--- a/wayvr/src/windowing/window.rs
+++ b/wayvr/src/windowing/window.rs
@@ -129,7 +129,12 @@ impl OverlayWindowConfig {
self.active_state = None;
}
- pub fn auto_movement(&mut self, app: &mut AppState) {
+ pub fn tick(&mut self, app: &mut AppState) {
+ self.auto_movement(app);
+ self.angle_fade(app);
+ }
+
+ fn auto_movement(&mut self, app: &mut AppState) {
if self.pause_movement {
return;
}
@@ -188,6 +193,25 @@ impl OverlayWindowConfig {
self.dirty = true;
}
+ fn angle_fade(&mut self, app: &mut AppState) {
+ let Some(state) = self.active_state.as_mut() else {
+ return;
+ };
+
+ if !state.angle_fade {
+ return;
+ }
+
+ let to_hmd = (state.transform.translation - app.input_state.hmd.translation).normalize();
+ let watch_normal = state.transform.transform_vector3a(Vec3A::NEG_Z).normalize();
+ let dot = to_hmd.dot(watch_normal);
+
+ state.alpha = (dot - app.session.config.watch_view_angle_min)
+ / (app.session.config.watch_view_angle_max - app.session.config.watch_view_angle_min);
+ state.alpha += 0.1;
+ state.alpha = state.alpha.clamp(0., 1.);
+ }
+
/// Returns true if changes were saved.
pub fn reset(&mut self, app: &mut AppState, hard_reset: bool) {
let Some(state) = self.active_state.as_mut() else {
diff --git a/wlx-common/src/windowing.rs b/wlx-common/src/windowing.rs
index f921ed0..139d6da 100644
--- a/wlx-common/src/windowing.rs
+++ b/wlx-common/src/windowing.rs
@@ -77,6 +77,7 @@ pub struct OverlayWindowState {
pub additive: bool,
pub saved_transform: Option,
pub block_input: bool,
+ pub angle_fade: bool,
}
impl Default for OverlayWindowState {
@@ -91,6 +92,7 @@ impl Default for OverlayWindowState {
additive: false,
saved_transform: None,
block_input: true,
+ angle_fade: false,
}
}
}