From f19b2f6e848ad2b4b5fde8969958e37e76844faa Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Sat, 16 Aug 2025 04:49:13 -0400 Subject: [PATCH] Add preference observer API for runtime webxr preference changes (#38649) Adds a global preference observer that is notified whenever any preference value is updated. This is used to support runtime configuration of WebXR automated testing, which is a prerequisite for running multiple WPT tests in a single browser session. Testing: Ran `./mach test-wpt /webxr --product=servodriver` Fixes: #38647 --------- Signed-off-by: Josh Matthews --- components/config/macro/lib.rs | 12 ++++++++++++ components/config/prefs.rs | 16 ++++++++++++++++ components/webxr/headless/mod.rs | 16 ++++++++++++++-- ports/servoshell/desktop/webxr.rs | 25 +++++++++++++++++++++---- 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/components/config/macro/lib.rs b/components/config/macro/lib.rs index da5d3635bc8..d5e1e016e9c 100644 --- a/components/config/macro/lib.rs +++ b/components/config/macro/lib.rs @@ -34,6 +34,12 @@ fn servo_preferences_derive(input: synstructure::Structure) -> TokenStream { set_match_cases.extend(quote!(stringify!(#name) => self.#name = value.try_into().unwrap(),)) } + let mut comparisons = quote!(); + for field in named_fields.named.iter() { + let name = field.ident.as_ref().unwrap(); + comparisons.extend(quote!(if self.#name != other.#name { changes.push((stringify!(#name), self.#name.clone().into(),)) })) + } + let structure_name = &ast.ident; quote! { impl #structure_name { @@ -50,6 +56,12 @@ fn servo_preferences_derive(input: synstructure::Structure) -> TokenStream { _ => { panic!("Unknown preference: {:?}", name); } } } + + pub fn diff(&self, other: &Self) -> Vec<(&'static str, PrefValue)> { + let mut changes = vec![]; + #comparisons + changes + } } } } diff --git a/components/config/prefs.rs b/components/config/prefs.rs index 82e5135a7f9..b7449ac1b07 100644 --- a/components/config/prefs.rs +++ b/components/config/prefs.rs @@ -11,12 +11,22 @@ pub use crate::pref_util::PrefValue; static PREFERENCES: RwLock = RwLock::new(Preferences::const_default()); +pub trait Observer: Send + Sync { + fn prefs_changed(&self, _changes: Vec<(&'static str, PrefValue)>) {} +} + +static OBSERVER: RwLock>> = RwLock::new(None); + #[inline] /// Get the current set of global preferences for Servo. pub fn get() -> RwLockReadGuard<'static, Preferences> { PREFERENCES.read().unwrap() } +pub fn set_observer(observer: Box) { + *OBSERVER.write().unwrap() = Some(observer); +} + pub fn set(preferences: Preferences) { // Map between Stylo preference names and Servo preference names as the This should be // kept in sync with components/script/dom/bindings/codegen/run.py which generates the @@ -39,7 +49,13 @@ pub fn set(preferences: Preferences) { preferences.layout_container_queries_enabled, ); + let changed = preferences.diff(&PREFERENCES.read().unwrap()); + *PREFERENCES.write().unwrap() = preferences; + + if let Some(observer) = OBSERVER.read().unwrap().as_deref() { + observer.prefs_changed(changed); + } } /// A convenience macro for accessing a preference value using its static path. diff --git a/components/webxr/headless/mod.rs b/components/webxr/headless/mod.rs index 17eda8ce822..19dac2dd0d4 100644 --- a/components/webxr/headless/mod.rs +++ b/components/webxr/headless/mod.rs @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; @@ -19,8 +20,15 @@ use webxr_api::{ use crate::{SurfmanGL, SurfmanLayerManager}; -#[derive(Default)] -pub struct HeadlessMockDiscovery {} +pub struct HeadlessMockDiscovery { + enabled: Arc, +} + +impl HeadlessMockDiscovery { + pub fn new(enabled: Arc) -> Self { + Self { enabled } + } +} struct HeadlessDiscovery { data: Arc>, @@ -76,6 +84,10 @@ impl MockDiscoveryAPI for HeadlessMockDiscovery { init: MockDeviceInit, receiver: WebXrReceiver, ) -> Result>, Error> { + if !self.enabled.load(Ordering::Relaxed) { + return Err(Error::NoMatchingDevice); + } + let viewer_origin = init.viewer_origin; let floor_transform = init.floor_origin.map(|f| f.inverse()); let views = init.views.clone(); diff --git a/ports/servoshell/desktop/webxr.rs b/ports/servoshell/desktop/webxr.rs index 8f85d6b08b2..e4ace54638c 100644 --- a/ports/servoshell/desktop/webxr.rs +++ b/ports/servoshell/desktop/webxr.rs @@ -4,9 +4,11 @@ use std::cell::RefCell; use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use servo::config::pref; -use servo::config::prefs::Preferences; +use servo::config::prefs::{self, Preferences}; use servo::webxr::WebXrRegistry; use servo::webxr::glwindow::GlWindowDiscovery; #[cfg(target_os = "windows")] @@ -60,14 +62,29 @@ impl XrDiscoveryWebXrRegistry { } } +struct XrPrefObserver(Arc); + +impl prefs::Observer for XrPrefObserver { + fn prefs_changed(&self, changes: Vec<(&'static str, prefs::PrefValue)>) { + if let Some((_, value)) = changes.iter().find(|(name, _)| *name == "dom_webxr_test") { + let prefs::PrefValue::Bool(value) = value else { + return; + }; + self.0.store(*value, Ordering::Relaxed); + } + } +} + #[cfg(feature = "webxr")] impl WebXrRegistry for XrDiscoveryWebXrRegistry { fn register(&self, xr: &mut servo::webxr::MainThreadRegistry) { use servo::webxr::headless::HeadlessMockDiscovery; - if pref!(dom_webxr_test) { - xr.register_mock(HeadlessMockDiscovery::default()); - } else if let Some(xr_discovery) = self.xr_discovery.take() { + let mock_enabled = Arc::new(AtomicBool::new(pref!(dom_webxr_test))); + xr.register_mock(HeadlessMockDiscovery::new(mock_enabled.clone())); + prefs::set_observer(Box::new(XrPrefObserver(mock_enabled))); + + if let Some(xr_discovery) = self.xr_discovery.take() { match xr_discovery { XrDiscovery::GlWindow(discovery) => xr.register(discovery), #[cfg(target_os = "windows")]