-
\ No newline at end of file
diff --git a/dash-frontend/assets/gui/tab/monado_tab_debug_timings.xml b/dash-frontend/assets/gui/tab/monado_tab_debug_timings.xml
new file mode 100644
index 00000000..94dfb5d7
--- /dev/null
+++ b/dash-frontend/assets/gui/tab/monado_tab_debug_timings.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dash-frontend/assets/gui/tab/monado_tab_general_settings.xml b/dash-frontend/assets/gui/tab/monado_tab_general_settings.xml
new file mode 100644
index 00000000..d8329b63
--- /dev/null
+++ b/dash-frontend/assets/gui/tab/monado_tab_general_settings.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dash-frontend/assets/gui/tab/monado_tab_process_list.xml b/dash-frontend/assets/gui/tab/monado_tab_process_list.xml
new file mode 100644
index 00000000..2767411c
--- /dev/null
+++ b/dash-frontend/assets/gui/tab/monado_tab_process_list.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dash-frontend/assets/gui/tab/settings.xml b/dash-frontend/assets/gui/tab/settings.xml
index 5350a0e1..9ca21283 100644
--- a/dash-frontend/assets/gui/tab/settings.xml
+++ b/dash-frontend/assets/gui/tab/settings.xml
@@ -54,7 +54,9 @@
-
+
+
+
\ No newline at end of file
diff --git a/dash-frontend/assets/gui/view/audio_settings.xml b/dash-frontend/assets/gui/view/audio_settings.xml
index a6a4b887..e1dbabc2 100644
--- a/dash-frontend/assets/gui/view/audio_settings.xml
+++ b/dash-frontend/assets/gui/view/audio_settings.xml
@@ -1,16 +1,16 @@
-
+
-
+
-
+
@@ -41,7 +41,7 @@
flex_grow="1"
id="${id}"
translation="${translation}"
- sprite_src="${src}">
+ sprite_src_builtin="${src}">
@@ -67,4 +67,4 @@
-
\ No newline at end of file
+
diff --git a/dash-frontend/assets/lang/de.json b/dash-frontend/assets/lang/de.json
index 6310d031..5d3d933f 100644
--- a/dash-frontend/assets/lang/de.json
+++ b/dash-frontend/assets/lang/de.json
@@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "Stopp",
"FORCE_KILL": "Erzwinge Beenden"
- }
+ },
+ "DEBUG_INFO": "Debug-Informationen"
}
\ No newline at end of file
diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json
index c7b4f8ea..beb5eff9 100644
--- a/dash-frontend/assets/lang/en.json
+++ b/dash-frontend/assets/lang/en.json
@@ -133,6 +133,7 @@
"VOLUME": "Volume"
},
"CLOSE_WINDOW": "Close window",
+ "DEBUG_INFO": "Debug info",
"DISPLAY_BRIGHTNESS": "Display brightness",
"FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:",
"GAME_LAUNCHED": "Game launched",
diff --git a/dash-frontend/assets/lang/es.json b/dash-frontend/assets/lang/es.json
index a42b8b99..b9dac274 100644
--- a/dash-frontend/assets/lang/es.json
+++ b/dash-frontend/assets/lang/es.json
@@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "Detener",
"FORCE_KILL": "Forzar cierre"
- }
+ },
+ "DEBUG_INFO": "Información de depuración"
}
\ No newline at end of file
diff --git a/dash-frontend/assets/lang/it.json b/dash-frontend/assets/lang/it.json
index 7e154553..fd02f8b0 100644
--- a/dash-frontend/assets/lang/it.json
+++ b/dash-frontend/assets/lang/it.json
@@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "Interrompi",
"FORCE_KILL": "Uccidi forzatamente"
- }
+ },
+ "DEBUG_INFO": "Informazioni di debug"
}
\ No newline at end of file
diff --git a/dash-frontend/assets/lang/ja.json b/dash-frontend/assets/lang/ja.json
index 4569f6a6..92acc7bf 100644
--- a/dash-frontend/assets/lang/ja.json
+++ b/dash-frontend/assets/lang/ja.json
@@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "停止",
"FORCE_KILL": "強制終了"
- }
+ },
+ "DEBUG_INFO": "デバッグ情報"
}
\ No newline at end of file
diff --git a/dash-frontend/assets/lang/pl.json b/dash-frontend/assets/lang/pl.json
index 250c387f..2f5f83fa 100644
--- a/dash-frontend/assets/lang/pl.json
+++ b/dash-frontend/assets/lang/pl.json
@@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "Zatrzymaj",
"FORCE_KILL": "Wymuś zakończenie"
- }
+ },
+ "DEBUG_INFO": "Informacje debugowania"
}
\ No newline at end of file
diff --git a/dash-frontend/assets/lang/zh_CN.json b/dash-frontend/assets/lang/zh_CN.json
index bde60a4e..915487e2 100644
--- a/dash-frontend/assets/lang/zh_CN.json
+++ b/dash-frontend/assets/lang/zh_CN.json
@@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "停止",
"FORCE_KILL": "强制关闭"
- }
+ },
+ "DEBUG_INFO": "调试信息"
}
\ No newline at end of file
diff --git a/dash-frontend/src/frontend.rs b/dash-frontend/src/frontend.rs
index ac2000f4..18f1d579 100644
--- a/dash-frontend/src/frontend.rs
+++ b/dash-frontend/src/frontend.rs
@@ -17,6 +17,7 @@ use wgui::{
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
};
use wlx_common::{
+ async_executor::AsyncExecutor,
audio,
dash_interface::{BoxDashInterface, RecenterMode},
locale::WayVRLangProvider,
@@ -29,7 +30,6 @@ use crate::{
util::{
popup_manager::{MountPopupParams, PopupManager, PopupManagerParams},
toast_manager::ToastManager,
- various::AsyncExecutor,
},
views,
};
@@ -45,7 +45,7 @@ pub type FrontendTasks = Tasks;
pub struct Frontend {
pub layout: Layout,
- globals: WguiGlobals,
+ pub globals: WguiGlobals,
pub interface: BoxDashInterface,
diff --git a/dash-frontend/src/tab/apps.rs b/dash-frontend/src/tab/apps.rs
index cd2a5150..7e17ff39 100644
--- a/dash-frontend/src/tab/apps.rs
+++ b/dash-frontend/src/tab/apps.rs
@@ -286,7 +286,7 @@ impl AppList {
let mut params = HashMap::, Rc>::new();
params.insert("text".into(), category_name.into());
- parser_state.parse_template(
+ parser_state.realize_template(
doc_params,
"CategoryText",
&mut frontend.layout,
@@ -318,7 +318,7 @@ impl AppList {
);
params.insert("name".into(), entry.app_name.clone());
- let data = parser_state.parse_template(
+ let data = parser_state.realize_template(
doc_params,
"AppEntry",
&mut frontend.layout,
diff --git a/dash-frontend/src/tab/monado.rs b/dash-frontend/src/tab/monado.rs
index 844ad634..0f714306 100644
--- a/dash-frontend/src/tab/monado.rs
+++ b/dash-frontend/src/tab/monado.rs
@@ -2,24 +2,119 @@ use std::{collections::HashMap, marker::PhantomData, rc::Rc};
use wgui::{
assets::AssetPath,
- components::{checkbox::ComponentCheckbox, slider::ComponentSlider},
+ components::{
+ bar_graph::{ComponentBarGraph, ValueCell},
+ button::ComponentButton,
+ checkbox::ComponentCheckbox,
+ slider::ComponentSlider,
+ tabs::ComponentTabs,
+ },
+ drawing::Color,
globals::WguiGlobals,
- layout::WidgetID,
+ layout::{Layout, WidgetID},
parser::{self, Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
};
-use wlx_common::dash_interface;
+use wlx_common::dash_interface::{self, MonadoDumpSessionFrame};
use crate::{
frontend::Frontend,
tab::{Tab, TabType},
};
-#[derive(Debug)]
+#[derive(Clone)]
+enum TabNameEnum {
+ GeneralSettings,
+ ProcessList,
+ DebugTimings,
+}
+
+impl TabNameEnum {
+ fn from_string(s: &str) -> Option {
+ match s {
+ "general_settings" => Some(TabNameEnum::GeneralSettings),
+ "process_list" => Some(TabNameEnum::ProcessList),
+ "debug_timings" => Some(TabNameEnum::DebugTimings),
+ _ => None,
+ }
+ }
+}
+
enum Task {
- Refresh,
- FocusClient(String),
SetBrightness(f32),
+ SetTab(TabNameEnum),
+
+ // `ProcessList` tab
+ ProcessListRefresh,
+ ProcessListFocusClient(String),
+
+ // `DebugTimings` tab
+ DebugTimingsRefreshSessionList,
+ DebugTimingsSetSessionId(i64),
+}
+
+struct SubtabProcessList {
+ id_list_parent: WidgetID,
+ state: ParserState,
+ cells: Vec,
+}
+
+struct SubtabGeneralSettings {
+ #[allow(dead_code)]
+ state: ParserState,
+}
+
+struct DebugGraph {
+ graph: Rc,
+}
+
+struct DebugSessionList {
+ #[allow(dead_code)]
+ buttons: Vec>,
+}
+
+struct TimingsSession {
+ resolved_name: Option,
+ last_frame: MonadoDumpSessionFrame,
+}
+
+struct Graphs {
+ predicted_display_time: DebugGraph,
+ predicted_frame_time: DebugGraph,
+ predicted_wake_up_time: DebugGraph,
+ predicted_gpu_done_time: DebugGraph,
+ predicted_display_period: DebugGraph,
+ display_time: DebugGraph,
+ when_predicted: DebugGraph,
+ when_wait_woke: DebugGraph,
+ when_begin: DebugGraph,
+ when_delivered: DebugGraph,
+ when_gpu_done: DebugGraph,
+}
+
+type SessionsMap = HashMap;
+
+struct SubtabDebugTimings {
+ #[allow(dead_code)]
+ state: ParserState,
+
+ graphs: Option,
+ session_list: DebugSessionList,
+ selected_session_id: Option,
+
+ id_sessions_list_parent: WidgetID,
+ id_timings_parent: WidgetID,
+
+ sessions: SessionsMap,
+}
+
+#[allow(dead_code)]
+#[allow(clippy::large_enum_variant)]
+enum Subtab {
+ Empty,
+ GeneralSettings(SubtabGeneralSettings),
+ ProcessList(SubtabProcessList),
+ DebugTimings(SubtabDebugTimings),
}
pub struct TabMonado {
@@ -29,10 +124,8 @@ pub struct TabMonado {
marker: PhantomData,
- globals: WguiGlobals,
- id_list_parent: WidgetID,
-
- cells: Vec,
+ id_content: WidgetID,
+ subtab: Subtab,
ticks: u32,
}
@@ -45,15 +138,64 @@ impl Tab for TabMonado {
fn update(&mut self, frontend: &mut Frontend, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
- Task::Refresh => self.refresh(frontend, data)?,
- Task::FocusClient(name) => self.focus_client(frontend, data, name)?,
+ Task::ProcessListRefresh => {
+ if let Subtab::ProcessList(process_list) = &mut self.subtab {
+ process_list.refresh(frontend, data, &self.tasks)?;
+ }
+ }
+ Task::ProcessListFocusClient(client_name) => {
+ if let Subtab::ProcessList(process_list) = &mut self.subtab {
+ process_list.focus_client(frontend, data, client_name, &self.tasks)?;
+ }
+ }
+ Task::DebugTimingsRefreshSessionList => {
+ if let Subtab::DebugTimings(tab) = &mut self.subtab {
+ tab.refresh_session_list(&mut frontend.layout, &self.tasks)?;
+ }
+ }
+ Task::DebugTimingsSetSessionId(session_id) => {
+ if let Subtab::DebugTimings(tab) = &mut self.subtab {
+ tab.set_session_id(&mut frontend.layout, session_id)?;
+ }
+ }
Task::SetBrightness(brightness) => self.set_brightness(frontend, data, brightness),
+ Task::SetTab(tab) => {
+ frontend.layout.remove_children(self.id_content);
+ match tab {
+ TabNameEnum::GeneralSettings => {
+ self.subtab = Subtab::GeneralSettings(SubtabGeneralSettings::new(
+ self.id_content,
+ frontend,
+ data,
+ &self.tasks,
+ )?)
+ }
+ TabNameEnum::ProcessList => {
+ self.tasks.push(Task::ProcessListRefresh);
+ self.subtab = Subtab::ProcessList(SubtabProcessList::new(self.id_content, frontend)?)
+ }
+ TabNameEnum::DebugTimings => {
+ self.subtab = Subtab::DebugTimings(SubtabDebugTimings::new(self.id_content, frontend, &self.tasks)?)
+ }
+ }
+ }
}
}
- // every few seconds
- if self.ticks.is_multiple_of(500) {
- self.tasks.push(Task::Refresh);
+ match &mut self.subtab {
+ Subtab::Empty => {}
+ Subtab::GeneralSettings(_) => {}
+ Subtab::ProcessList(_) => {
+ // every few seconds
+ if let Subtab::ProcessList(_) = &self.subtab
+ && self.ticks.is_multiple_of(500)
+ {
+ self.tasks.push(Task::ProcessListRefresh);
+ }
+ }
+ Subtab::DebugTimings(timings) => {
+ timings.update(&self.tasks, data, frontend)?;
+ }
}
self.ticks += 1;
@@ -62,7 +204,7 @@ impl Tab for TabMonado {
}
}
-fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
+fn doc_params_monado(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/monado.xml"),
@@ -70,6 +212,30 @@ fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
}
}
+fn doc_params_tab_process_list(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
+ ParseDocumentParams {
+ globals: globals.clone(),
+ path: AssetPath::BuiltIn("gui/tab/monado_tab_process_list.xml"),
+ extra: Default::default(),
+ }
+}
+
+fn doc_params_tab_general_settings(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
+ ParseDocumentParams {
+ globals: globals.clone(),
+ path: AssetPath::BuiltIn("gui/tab/monado_tab_general_settings.xml"),
+ extra: Default::default(),
+ }
+}
+
+fn doc_params_tab_debug_timings(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
+ ParseDocumentParams {
+ globals: globals.clone(),
+ path: AssetPath::BuiltIn("gui/tab/monado_tab_debug_timings.xml"),
+ extra: Default::default(),
+ }
+}
+
fn yesno(n: bool) -> &'static str {
match n {
true => "yes",
@@ -77,29 +243,320 @@ fn yesno(n: bool) -> &'static str {
}
}
-impl TabMonado {
- pub fn new(frontend: &mut Frontend, parent_id: WidgetID) -> anyhow::Result {
- let globals = frontend.layout.state.globals.clone();
- let state = wgui::parser::parse_from_assets(&doc_params(&globals), &mut frontend.layout, parent_id)?;
+impl SubtabGeneralSettings {
+ fn new(
+ parent_id: WidgetID,
+ frontend: &mut Frontend,
+ data: &mut T,
+ tasks: &Tasks,
+ ) -> anyhow::Result {
+ let state = wgui::parser::parse_from_assets(
+ &doc_params_tab_general_settings(&frontend.globals),
+ &mut frontend.layout,
+ parent_id,
+ )?;
- let id_list_parent = state.get_widget_id("list_parent")?;
+ // get brightness
+ let slider_brightness = state.fetch_component_as::("slider_brightness")?;
+ if let Some(brightness) = frontend.interface.monado_brightness_get(data) {
+ let mut c = frontend.layout.start_common();
+ slider_brightness.set_value(&mut c.common(), brightness * 100.0);
+ c.finish()?;
- let tasks = Tasks::::new();
+ slider_brightness.on_value_changed({
+ let tasks = tasks.clone();
+ Box::new(move |_common, e| {
+ tasks.push(Task::SetBrightness(e.value / 100.0));
+ Ok(())
+ })
+ });
+ }
- tasks.push(Task::Refresh);
+ Ok(Self { state })
+ }
+}
+
+fn mount_sessions_list(
+ state: &mut ParserState,
+ layout: &mut Layout,
+ tasks: &Tasks,
+ id_parent: WidgetID,
+ sessions: &SessionsMap,
+) -> anyhow::Result {
+ let mut buttons = Vec::new();
+ let globals = layout.state.globals.clone();
+ layout.remove_children(id_parent);
+
+ for (session_id, session) in sessions {
+ let mut params = HashMap::new();
+
+ params.insert(
+ Rc::from("text"),
+ Rc::from(format!(
+ "{} (ID {})",
+ session.resolved_name.as_ref().map_or("Unknown", |s| s.as_str()),
+ session_id,
+ )),
+ );
+
+ let data = state.realize_template(
+ &doc_params_tab_debug_timings(&globals),
+ "SessionButton",
+ layout,
+ id_parent,
+ params,
+ )?;
+
+ let button = data.fetch_component_as::("button")?;
+
+ button.on_click({
+ let tasks = tasks.clone();
+ let session_id = *session_id;
+ Rc::new(move |_, _| {
+ tasks.push(Task::DebugTimingsSetSessionId(session_id));
+ Ok(())
+ })
+ });
+
+ buttons.push(button);
+ }
+
+ Ok(DebugSessionList { buttons })
+}
+
+fn mount_graph(
+ state: &mut ParserState,
+ layout: &mut Layout,
+ id_parent: WidgetID,
+ name: &'static str,
+ limits: (f32, f32),
+) -> anyhow::Result {
+ let globals = layout.state.globals.clone();
+ let mut params = HashMap::new();
+ params.insert(Rc::from("name"), Rc::from(name));
+ params.insert(Rc::from("limit_min"), Rc::from(limits.0.to_string()));
+ params.insert(Rc::from("limit_max"), Rc::from(limits.1.to_string()));
+
+ let data = state.realize_template(
+ &doc_params_tab_debug_timings(&globals),
+ "DebugGraph",
+ layout,
+ id_parent,
+ params,
+ )?;
+
+ let graph = data.fetch_component_as::("graph")?;
+ Ok(DebugGraph { graph })
+}
+
+fn ns_to_ms(ns: i64) -> f32 {
+ (ns / 1000) as f32 / 1000.0
+}
+
+impl SubtabDebugTimings {
+ fn new(parent_id: WidgetID, frontend: &mut Frontend, tasks: &Tasks) -> anyhow::Result {
+ let mut state = wgui::parser::parse_from_assets(
+ &doc_params_tab_debug_timings(&frontend.globals),
+ &mut frontend.layout,
+ parent_id,
+ )?;
+
+ let id_timings_parent = state.get_widget_id("timings_parent")?;
+ let id_sessions_list_parent = state.get_widget_id("session_list_parent")?;
+
+ let sessions = Default::default();
+
+ let session_list = mount_sessions_list(
+ &mut state,
+ &mut frontend.layout,
+ tasks,
+ id_sessions_list_parent,
+ &sessions,
+ )?;
Ok(Self {
state,
- marker: PhantomData,
+ graphs: None,
+ session_list,
+ id_sessions_list_parent,
+ id_timings_parent,
+ sessions,
+ selected_session_id: None,
+ })
+ }
+
+ fn set_session_id(&mut self, layout: &mut Layout, session_id: i64) -> anyhow::Result<()> {
+ layout.remove_children(self.id_timings_parent);
+
+ let mut graph = |name: &'static str, limits: (f32, f32)| -> anyhow::Result {
+ mount_graph(&mut self.state, layout, self.id_timings_parent, name, limits)
+ };
+
+ // populate graphs
+ self.graphs = Some(Graphs {
+ predicted_display_time: graph("Predicted display time", (0.0, 30.0))?,
+ predicted_frame_time: graph("Predicted frame time", (0.0, 30.0))?,
+ predicted_wake_up_time: graph("Predicted wake-up time", (0.0, 30.0))?,
+ predicted_gpu_done_time: graph("Predicted GPU done time", (0.0, 30.0))?,
+ predicted_display_period: graph("Predicted display period", (0.0, 30.0))?,
+ display_time: graph("Display time", (0.0, 30.0))?,
+ when_predicted: graph("When predicted", (0.0, 30.0))?,
+ when_wait_woke: graph("When wait woke", (0.0, 30.0))?,
+ when_begin: graph("When begin", (0.0, 30.0))?,
+ when_delivered: graph("When delivered", (0.0, 30.0))?,
+ when_gpu_done: graph("When GPU done", (0.0, 30.0))?,
+ });
+
+ self.selected_session_id = Some(session_id);
+
+ Ok(())
+ }
+
+ fn refresh_session_list(&mut self, layout: &mut Layout, tasks: &Tasks) -> anyhow::Result<()> {
+ self.session_list = mount_sessions_list(
+ &mut self.state,
+ layout,
tasks,
- globals,
+ self.id_sessions_list_parent,
+ &self.sessions,
+ )?;
+ Ok(())
+ }
+
+ fn update(&mut self, tasks: &Tasks, data: &mut T, frontend: &mut Frontend) -> anyhow::Result<()> {
+ if !frontend.interface.monado_metrics_set_enabled(data, true) {
+ return Ok(());
+ }
+
+ let frames = frontend.interface.monado_metrics_dump_session_frames(data);
+ if frames.is_empty() {
+ return Ok(());
+ }
+
+ let col_green = Color::new(0.0, 1.0, 0.0, 1.0);
+
+ for frame in frames {
+ //log::info!("{:?}", frame);
+
+ match self.sessions.get_mut(&frame.session_id) {
+ Some(session) => {
+ if let Some(graphs) = &mut self.graphs
+ && let Some(selected_session_id) = self.selected_session_id
+ && selected_session_id == frame.session_id
+ {
+ let predicted_display_time = ns_to_ms(session.last_frame.predicted_display_time_ns as i64);
+ let predicted_frame_time = ns_to_ms(frame.predicted_frame_time_ns as i64);
+ let predicted_wake_up_time =
+ ns_to_ms(frame.predicted_wake_up_time_ns as i64 - session.last_frame.predicted_wake_up_time_ns as i64);
+ let predicted_gpu_done_time =
+ ns_to_ms(frame.predicted_gpu_done_time_ns as i64 - session.last_frame.predicted_gpu_done_time_ns as i64);
+ let predicted_display_period = ns_to_ms(session.last_frame.predicted_display_period_ns as i64); // 6.944 ms for 144Hz
+ let display_time = ns_to_ms(frame.display_time_ns as i64 - session.last_frame.display_time_ns as i64);
+ let when_predicted = ns_to_ms(frame.when_predicted_ns as i64 - session.last_frame.when_predicted_ns as i64);
+ let when_wait_woke = ns_to_ms(frame.when_wait_woke_ns as i64 - session.last_frame.when_wait_woke_ns as i64);
+ let when_begin = ns_to_ms(frame.when_begin_ns as i64 - session.last_frame.when_begin_ns as i64);
+ let when_delivered = ns_to_ms(frame.when_delivered_ns as i64 - session.last_frame.when_delivered_ns as i64);
+ let when_gpu_done = ns_to_ms(frame.when_gpu_done_ns as i64 - session.last_frame.when_gpu_done_ns as i64);
+
+ graphs.predicted_display_time.graph.push_value(ValueCell {
+ value: predicted_display_time,
+ color: col_green,
+ });
+
+ graphs.predicted_frame_time.graph.push_value(ValueCell {
+ value: predicted_frame_time,
+ color: col_green,
+ });
+
+ graphs.predicted_wake_up_time.graph.push_value(ValueCell {
+ value: predicted_wake_up_time,
+ color: col_green,
+ });
+
+ graphs.predicted_gpu_done_time.graph.push_value(ValueCell {
+ value: predicted_gpu_done_time,
+ color: col_green,
+ });
+
+ graphs.predicted_display_period.graph.push_value(ValueCell {
+ value: predicted_display_period,
+ color: col_green,
+ });
+
+ graphs.display_time.graph.push_value(ValueCell {
+ value: display_time,
+ color: col_green,
+ });
+
+ graphs.when_predicted.graph.push_value(ValueCell {
+ value: when_predicted,
+ color: col_green,
+ });
+
+ graphs.when_wait_woke.graph.push_value(ValueCell {
+ value: when_wait_woke,
+ color: col_green,
+ });
+
+ graphs.when_begin.graph.push_value(ValueCell {
+ value: when_begin,
+ color: col_green,
+ });
+
+ graphs.when_delivered.graph.push_value(ValueCell {
+ value: when_delivered,
+ color: col_green,
+ });
+
+ graphs.when_gpu_done.graph.push_value(ValueCell {
+ value: when_gpu_done,
+ color: col_green,
+ });
+ }
+
+ session.last_frame = frame;
+ }
+ None => {
+ self.sessions.insert(
+ frame.session_id,
+ TimingsSession {
+ last_frame: frame,
+ resolved_name: None, /* TODO! find client ID from session ID */
+ },
+ );
+ tasks.push(Task::DebugTimingsRefreshSessionList);
+ }
+ }
+ }
+
+ frontend.layout.mark_redraw();
+
+ Ok(())
+ }
+}
+
+impl SubtabProcessList {
+ fn new(parent_id: WidgetID, frontend: &mut Frontend) -> anyhow::Result {
+ let state = wgui::parser::parse_from_assets(
+ &doc_params_tab_process_list(&frontend.globals),
+ &mut frontend.layout,
+ parent_id,
+ )?;
+ let id_list_parent = state.get_widget_id("list_parent")?;
+
+ Ok(Self {
+ state,
id_list_parent,
- ticks: 0,
cells: Vec::new(),
})
}
- fn mount_client(&mut self, frontend: &mut Frontend, client: &dash_interface::MonadoClient) -> anyhow::Result<()> {
+ fn mount_client(
+ &mut self,
+ layout: &mut Layout,
+ client: &dash_interface::MonadoClient,
+ tasks: &Tasks,
+ ) -> anyhow::Result<()> {
let mut par = HashMap::, Rc>::new();
par.insert(
"checked".into(),
@@ -109,7 +566,10 @@ impl TabMonado {
Rc::from("0")
},
);
- par.insert("name".into(), client.name.clone().into());
+ par.insert(
+ "name".into(),
+ format!("{} (Client ID: {})", client.name, client.id).into(),
+ );
par.insert("flag_active".into(), yesno(client.is_active).into());
par.insert("flag_focused".into(), yesno(client.is_focused).into());
par.insert("flag_io_active".into(), yesno(client.is_io_active).into());
@@ -117,21 +577,23 @@ impl TabMonado {
par.insert("flag_primary".into(), yesno(client.is_primary).into());
par.insert("flag_visible".into(), yesno(client.is_visible).into());
- let state_cell = self.state.parse_template(
- &doc_params(&self.globals),
+ let globals = layout.state.globals.clone();
+
+ let state_cell = self.state.realize_template(
+ &doc_params_tab_process_list(&globals),
"Cell",
- &mut frontend.layout,
+ layout,
self.id_list_parent,
par,
)?;
let checkbox = state_cell.fetch_component_as::("checkbox")?;
checkbox.on_toggle({
- let tasks = self.tasks.clone();
+ let tasks = tasks.clone();
let client_name = client.name.clone();
Box::new(move |_common, e| {
if e.checked {
- tasks.push(Task::FocusClient(client_name.clone()));
+ tasks.push(Task::ProcessListFocusClient(client_name.clone()));
}
Ok(())
})
@@ -142,41 +604,63 @@ impl TabMonado {
Ok(())
}
- fn refresh(&mut self, frontend: &mut Frontend, data: &mut T) -> anyhow::Result<()> {
+ fn focus_client(
+ &mut self,
+ frontend: &mut Frontend,
+ data: &mut T,
+ name: String,
+ tasks: &Tasks,
+ ) -> anyhow::Result<()> {
+ frontend.interface.monado_client_focus(data, &name)?;
+ tasks.push(Task::ProcessListRefresh);
+ Ok(())
+ }
+
+ fn refresh(&mut self, frontend: &mut Frontend, data: &mut T, tasks: &Tasks) -> anyhow::Result<()> {
log::debug!("refreshing monado client list");
- let clients = frontend.interface.monado_client_list(data)?;
+ let clients = frontend.interface.monado_client_list(data, true)?;
frontend.layout.remove_children(self.id_list_parent);
self.cells.clear();
for client in clients {
- self.mount_client(frontend, &client)?;
- }
-
- // get brightness
- let slider_brightness = self.state.fetch_component_as::("slider_brightness")?;
- if let Some(brightness) = frontend.interface.monado_brightness_get(data) {
- let mut c = frontend.layout.start_common();
- slider_brightness.set_value(&mut c.common(), brightness * 100.0);
- c.finish()?;
-
- slider_brightness.on_value_changed({
- let tasks = self.tasks.clone();
- Box::new(move |_common, e| {
- tasks.push(Task::SetBrightness(e.value / 100.0));
- Ok(())
- })
- });
+ self.mount_client(&mut frontend.layout, &client, tasks)?;
}
Ok(())
}
+}
- fn focus_client(&mut self, frontend: &mut Frontend, data: &mut T, name: String) -> anyhow::Result<()> {
- frontend.interface.monado_client_focus(data, &name)?;
- self.tasks.push(Task::Refresh);
- Ok(())
+impl TabMonado {
+ pub fn new(frontend: &mut Frontend, parent_id: WidgetID) -> anyhow::Result {
+ let globals = frontend.layout.state.globals.clone();
+ let state = wgui::parser::parse_from_assets(&doc_params_monado(&globals), &mut frontend.layout, parent_id)?;
+ let id_content = state.get_widget_id("content")?;
+ let tabs = state.fetch_component_as::("tabs")?;
+
+ let tasks = Tasks::::new();
+
+ tabs.on_select({
+ let tasks = tasks.clone();
+ Rc::new(move |_common, evt| {
+ if let Some(tab) = TabNameEnum::from_string(&evt.name) {
+ tasks.push(Task::SetTab(tab));
+ }
+ Ok(())
+ })
+ });
+
+ tasks.push(Task::SetTab(TabNameEnum::ProcessList));
+
+ Ok(Self {
+ state,
+ marker: PhantomData,
+ tasks,
+ id_content,
+ ticks: 0,
+ subtab: Subtab::Empty,
+ })
}
fn set_brightness(&mut self, frontend: &mut Frontend, data: &mut T, brightness: f32) {
diff --git a/dash-frontend/src/util/cached_fetcher.rs b/dash-frontend/src/util/cached_fetcher.rs
index d1a2ee33..5c7f84bc 100644
--- a/dash-frontend/src/util/cached_fetcher.rs
+++ b/dash-frontend/src/util/cached_fetcher.rs
@@ -1,8 +1,8 @@
use anyhow::Context;
use serde::Deserialize;
-use wlx_common::cache_dir;
+use wlx_common::{async_executor::AsyncExecutor, cache_dir};
-use crate::util::{http_client, steam_utils::AppID, various::AsyncExecutor};
+use crate::util::{http_client, steam_utils::AppID};
pub struct CoverArt {
// can be empty in case if data couldn't be fetched (use a fallback image then)
diff --git a/dash-frontend/src/util/http_client.rs b/dash-frontend/src/util/http_client.rs
index b6af702c..875d509e 100644
--- a/dash-frontend/src/util/http_client.rs
+++ b/dash-frontend/src/util/http_client.rs
@@ -13,8 +13,7 @@ use smol::{net::TcpStream, prelude::*};
use std::convert::TryInto;
use std::pin::Pin;
use std::task::{Context, Poll};
-
-use crate::util::various::AsyncExecutor;
+use wlx_common::async_executor::AsyncExecutor;
pub struct HttpClientResponse {
pub data: Vec,
}
diff --git a/dash-frontend/src/util/mod.rs b/dash-frontend/src/util/mod.rs
index 67d18d3b..e5290607 100644
--- a/dash-frontend/src/util/mod.rs
+++ b/dash-frontend/src/util/mod.rs
@@ -4,5 +4,4 @@ pub mod pactl_wrapper;
pub mod popup_manager;
pub mod steam_utils;
pub mod toast_manager;
-pub mod various;
pub mod wgui_simple;
diff --git a/dash-frontend/src/views/audio_settings.rs b/dash-frontend/src/views/audio_settings.rs
index f2c43f45..61061d33 100644
--- a/dash-frontend/src/views/audio_settings.rs
+++ b/dash-frontend/src/views/audio_settings.rs
@@ -750,7 +750,7 @@ impl View {
let data = self
.state
- .parse_template(&doc_params(&self.globals), "Card", params.layout, self.id_devices, par)?;
+ .realize_template(&doc_params(&self.globals), "Card", params.layout, self.id_devices, par)?;
let btn_card = data.fetch_component_as::("btn_card")?;
btn_card.on_click({
@@ -764,7 +764,6 @@ impl View {
})
});
- log::info!("mount card TODO: {}", params.card.name);
Ok(())
}
@@ -794,7 +793,7 @@ impl View {
},
);
- let data = self.state.parse_template(
+ let data = self.state.realize_template(
&doc_params(&self.globals),
"DeviceSlider",
params.layout,
@@ -941,7 +940,7 @@ impl View {
layout.remove_children(self.id_devices);
{
- let data = self.state.parse_template(
+ let data = self.state.realize_template(
&doc_params(&self.globals),
"SelectAudioProfileText",
layout,
diff --git a/dash-frontend/src/views/game_cover.rs b/dash-frontend/src/views/game_cover.rs
index 5221ea1d..6adc4110 100644
--- a/dash-frontend/src/views/game_cover.rs
+++ b/dash-frontend/src/views/game_cover.rs
@@ -25,11 +25,11 @@ use wgui::{
util::WLength,
},
};
+use wlx_common::async_executor::AsyncExecutor;
use crate::util::{
cached_fetcher::{self, CoverArt},
steam_utils::{self, AppID},
- various::AsyncExecutor,
};
pub struct ViewCommon {
diff --git a/dash-frontend/src/views/game_launcher.rs b/dash-frontend/src/views/game_launcher.rs
index 2267e069..03d26d77 100644
--- a/dash-frontend/src/views/game_launcher.rs
+++ b/dash-frontend/src/views/game_launcher.rs
@@ -5,7 +5,6 @@ use crate::{
util::{
cached_fetcher::{self, CoverArt},
steam_utils::{self, AppID, AppManifest},
- various::AsyncExecutor,
},
views::game_cover,
};
@@ -19,6 +18,7 @@ use wgui::{
task::Tasks,
widget::{ConstructEssentials, label::WidgetLabel},
};
+use wlx_common::async_executor::AsyncExecutor;
#[derive(Clone)]
enum Task {
diff --git a/dash-frontend/src/views/game_list.rs b/dash-frontend/src/views/game_list.rs
index 9946bdf6..5de3cf64 100644
--- a/dash-frontend/src/views/game_list.rs
+++ b/dash-frontend/src/views/game_list.rs
@@ -13,6 +13,7 @@ use wgui::{
label::{WidgetLabel, WidgetLabelParams},
},
};
+use wlx_common::async_executor::AsyncExecutor;
use crate::{
frontend::{FrontendTask, FrontendTasks},
@@ -20,7 +21,6 @@ use crate::{
cached_fetcher::CoverArt,
popup_manager::{MountPopupParams, PopupHandle},
steam_utils::{self, AppID, SteamUtils},
- various::AsyncExecutor,
},
views::{self, game_cover, game_launcher},
};
diff --git a/dash-frontend/src/views/running_games_list.rs b/dash-frontend/src/views/running_games_list.rs
index 19f96911..6428f5d1 100644
--- a/dash-frontend/src/views/running_games_list.rs
+++ b/dash-frontend/src/views/running_games_list.rs
@@ -127,7 +127,7 @@ impl View {
for game in games {
let game_name = View::extract_name_from_appid(&game.app_id, &self.installed_games);
- let t = self.state.parse_template(
+ let t = self.state.realize_template(
&doc_params(layout.state.globals.clone()),
"RunningGameCell",
layout,
diff --git a/scripts/prost_build/Cargo.toml b/scripts/prost_build/Cargo.toml
new file mode 100644
index 00000000..f748fe75
--- /dev/null
+++ b/scripts/prost_build/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "prost_build"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+prost-build = "0.14.3"
diff --git a/scripts/prost_build/src/main.rs b/scripts/prost_build/src/main.rs
new file mode 100644
index 00000000..12f50c39
--- /dev/null
+++ b/scripts/prost_build/src/main.rs
@@ -0,0 +1,5 @@
+use std::io::Result;
+fn main() -> Result<()> {
+ prost_build::compile_protos(&["src/monado_metrics.proto"], &["src/"])?;
+ Ok(())
+}
diff --git a/scripts/prost_build/src/monado_metrics.proto b/scripts/prost_build/src/monado_metrics.proto
new file mode 100644
index 00000000..27682366
--- /dev/null
+++ b/scripts/prost_build/src/monado_metrics.proto
@@ -0,0 +1,174 @@
+syntax = "proto3";
+
+package monado_metrics;
+
+
+message Version
+{
+ uint32 major = 1;
+ uint32 minor = 2;
+}
+
+message SessionFrame
+{
+ //! Which session this frame belongs to.
+ int64 session_id = 1;
+
+ //! ID of frame.
+ int64 frame_id = 2;
+
+ //! How long we thought the frame would take.
+ uint64 predicted_frame_time_ns = 3;
+
+ //! When we predicted the app should wake up.
+ uint64 predicted_wake_up_time_ns = 4;
+
+ //! When the client's GPU work should have completed.
+ uint64 predicted_gpu_done_time_ns = 5;
+
+ //! When we predicted this frame to be shown.
+ uint64 predicted_display_time_ns = 6;
+
+ //! The selected display period.
+ uint64 predicted_display_period_ns = 7;
+
+ /*!
+ * When the app told us to display this frame, can be different
+ * then the predicted display time so we track that separately.
+ */
+ uint64 display_time_ns = 8;
+
+ //! When the predict call was made (inside of xrWaitFrame).
+ uint64 when_predicted_ns = 9;
+
+ //! When the waiting thread was woken up.
+ uint64 when_wait_woke_ns = 10;
+
+ //! When xrBeginFrame was called.
+ uint64 when_begin_ns = 11;
+
+ //! When the layer information was delivered, (inside of xrEndFrame).
+ uint64 when_delivered_ns = 12;
+
+ //! When the scheduled GPU work was completed.
+ uint64 when_gpu_done_ns = 13;
+
+ //! Was this frame discarded.
+ bool discarded = 14;
+}
+
+message Used
+{
+ //! Owning session of the frame.
+ int64 session_id = 1;
+
+ //! Which session frame was used by the compositor.
+ int64 session_frame_id = 2;
+
+ //! The system frame that the session was used by.
+ int64 system_frame_id = 3;
+
+ //! When the frame was latched.
+ uint64 when_ns = 4;
+}
+
+message SystemFrame
+{
+ //! ID of frame.
+ int64 frame_id = 1;
+
+ //! Projected pixels to photon time.
+ uint64 predicted_display_time_ns = 2;
+
+ //! Current period of displaying of frames.
+ uint64 predicted_display_period_ns = 3;
+
+ //! When the compositor should hand pixels to display hardware.
+ uint64 desired_present_time_ns = 4;
+
+ //! The time that the compositor should wake up.
+ uint64 wake_up_time_ns = 5;
+ uint64 present_slop_ns = 6;
+}
+
+/*!
+ * Out of band info about a GPU timing from a system compositor.
+ */
+message SystemGpuInfo
+{
+ //! ID of frame.
+ int64 frame_id = 1;
+
+ //! Start of GPU work.
+ uint64 gpu_start_ns = 2;
+
+ //! End of GPU work.
+ uint64 gpu_end_ns = 3;
+
+ //! When the information was gathered.
+ uint64 when_ns = 4;
+}
+
+/*!
+ * Information that comes from the vulkan present timing.
+ */
+message SystemPresentInfo
+{
+ //! ID of frame.
+ int64 frame_id = 1;
+
+ //! The total expected time for the compositor to complete a frame.
+ uint64 expected_comp_time_ns = 2;
+
+ //! The time we predicted the compositor to wake up time.
+ uint64 predicted_wake_up_time_ns = 3;
+
+ //! Predicted time for completion of the GPU work.
+ uint64 predicted_done_time_ns = 4;
+
+ //! Predicted display time.
+ uint64 predicted_display_time_ns = 5;
+
+ //! When was this frame predicted.
+ uint64 when_predict_ns = 6;
+
+ //! When we last woke up the compositor after its equivalent of wait_frame.
+ uint64 when_woke_ns = 7;
+
+ //! When the compositor started rendering a frame
+ uint64 when_began_ns = 8;
+
+ //! When the compositor finished rendering a frame
+ uint64 when_submitted_ns = 9;
+
+ //! When new frame timing info was last added.
+ uint64 when_infoed_ns = 10;
+
+ //! When we wanted to start presenting.
+ uint64 desired_present_time_ns = 11;
+
+ //! The slop used for this frame.
+ uint64 present_slop_ns = 12;
+
+ //! Margin of GPU to present time.
+ uint64 present_margin_ns = 13;
+
+ //! When the present time actually happened.
+ uint64 actual_present_time_ns = 14;
+
+ //! The earliest we could have presented.
+ uint64 earliest_present_time_ns = 15;
+}
+
+
+message Record
+{
+ oneof record {
+ Version version = 1;
+ SessionFrame session_frame = 2;
+ Used used = 3;
+ SystemFrame system_frame = 4;
+ SystemGpuInfo system_gpu_info = 5;
+ SystemPresentInfo system_present_info = 6;
+ };
+}
diff --git a/wayvr/Cargo.toml b/wayvr/Cargo.toml
index 22c08795..843ee83f 100644
--- a/wayvr/Cargo.toml
+++ b/wayvr/Cargo.toml
@@ -9,11 +9,11 @@ repository = "https://github.com/wlx-team/wayvr"
keywords = [
"linux",
"openvr",
- "openxr",
- "x11",
- "wayland",
"openvr-overlay",
+ "openxr",
"openxr-overlay",
+ "wayland",
+ "x11",
]
categories = ["games"]
@@ -32,20 +32,19 @@ glam = { workspace = true, features = ["mint", "serde"] }
idmap = { workspace = true, features = ["serde"] }
idmap-derive.workspace = true
log.workspace = true
-rust-embed.workspace = true
regex.workspace = true
-slotmap.workspace = true
+rust-embed.workspace = true
serde = { workspace = true, features = ["rc"] }
serde_json.workspace = true
+slotmap.workspace = true
strum.workspace = true
vulkano.workspace = true
vulkano-shaders.workspace = true
xdg.workspace = true
-ash = "^0.38.0" # must match vulkano
+ash = "^0.38.0" # must match vulkano
bytes = { version = "1.11.1" }
-chrono = { version = "0.4.42", features = [ "unstable-locales"]}
-pure-rust-locales = "0.8.2"
+chrono = { version = "0.4.42", features = ["unstable-locales"] }
chrono-tz = "0.10.4"
config = "0.15.19"
dbus = { version = "0.9.9" }
@@ -58,14 +57,16 @@ interprocess = { version = "2.2.3" }
json = { version = "0.12.4", optional = true }
json5 = "1.3.0"
libc = "0.2.178"
-libmonado = { version = "1.6.0", optional = true }
+libmonado = { git = "https://github.com/oo8dev/libmonado-rs.git", rev = "fc39940a64dea2df080a0d2c974c7d651006241f", optional = true }
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
mint = "0.5.9"
openxr = { version = "0.21.0", features = ["linked", "mint"], optional = true }
-ovr_overlay = { features = [
+ovr_overlay = { git = "https://github.com/galister/ovr_overlay_oyasumi", rev = "e477bd2a9e04293ea68c1e7529ef2cb131f32acc", features = [
"ovr_input",
"ovr_system",
-], git = "https://github.com/galister/ovr_overlay_oyasumi", rev = "e477bd2a9e04293ea68c1e7529ef2cb131f32acc", optional = true }
+], optional = true }
+prost = { version = "0.14.3", optional = true }
+pure-rust-locales = "0.8.2"
rosc = { version = "0.11.4", optional = true }
serde_json5 = "0.2.1"
serde_yaml = "0.9.34"
@@ -74,32 +75,34 @@ smallvec = "1.15.1"
smithay = { version = "0.7.0", default-features = false, features = [
"backend_vulkan",
"desktop",
- "xwayland",
"wayland_frontend",
+ "xwayland",
] }
+smol = { workspace = true }
sysinfo = { version = "0.37" }
thiserror = "2.0"
-tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tracing = "0.1.43"
-uuid = { version = "1.19.0", features = ["v4", "fast-rng"] }
+tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
+uuid = { version = "1.19.0", features = ["fast-rng", "v4"] }
wayland-client = { workspace = true }
winit = { version = "0.30.12", optional = true }
-xcb = { version = "1.6.0", optional = true, features = [
+xcb = { version = "1.6.0", features = [
"as-raw-xcb-connection",
-] }
-xkbcommon = { version = "0.8.0" } # 0.9.0 breaks keymap import on some distros
+], optional = true }
+xkbcommon = { version = "0.8.0" } # 0.9.0 breaks keymap import on some distros
[build-dependencies]
regex.workspace = true
[features]
-default = ["openvr", "openxr", "osc", "x11", "wayland"]
-openvr = ["dep:ovr_overlay", "dep:json"]
-openxr = ["dep:openxr", "dep:libmonado"]
+default = ["openvr", "openxr", "osc", "wayland", "x11"]
+as-raw-xcb-connection = []
+feat-monado-metrics = []
+openvr = ["dep:json", "dep:ovr_overlay"]
+openxr = ["dep:libmonado", "dep:openxr", "dep:prost"]
osc = ["dep:rosc"]
-x11 = ["dep:xcb", "wlx-capture/xshm", "xkbcommon/x11"]
-wayland = ["pipewire", "wlx-capture/wlr", "xkbcommon/wayland"]
pipewire = ["wlx-capture/pipewire"]
uidev = ["dep:winit"]
+wayland = ["pipewire", "wlx-capture/wlr", "xkbcommon/wayland"]
+x11 = ["dep:xcb", "wlx-capture/xshm", "xkbcommon/x11"]
xcb = ["dep:xcb"]
-as-raw-xcb-connection = []
diff --git a/wayvr/src/backend/openxr/blocker.rs b/wayvr/src/backend/openxr/blocker.rs
index 08c6497e..a99dd1f5 100644
--- a/wayvr/src/backend/openxr/blocker.rs
+++ b/wayvr/src/backend/openxr/blocker.rs
@@ -24,7 +24,7 @@ impl InputBlocker {
}
pub fn update(&mut self, app: &mut AppState) {
- let Some(monado) = &mut app.monado else {
+ let Some(monado) = &mut app.monado_state else {
return; // monado not available
};
@@ -50,7 +50,7 @@ impl InputBlocker {
} else {
trace!("Unblocking input");
}
- self.block_inputs(monado, should_block_inputs, should_block_poses);
+ self.block_inputs(&mut monado.ipc, should_block_inputs, should_block_poses);
}
self.inputs_blocked_last_frame = should_block_inputs;
diff --git a/wayvr/src/backend/openxr/input.rs b/wayvr/src/backend/openxr/input.rs
index 47306fac..f37c4673 100644
--- a/wayvr/src/backend/openxr/input.rs
+++ b/wayvr/src/backend/openxr/input.rs
@@ -272,7 +272,7 @@ impl OpenXrInputSource {
}
pub fn update_devices(app: &mut AppState) -> bool {
- let Some(monado) = &mut app.monado else {
+ let Some(monado) = &mut app.monado_state else {
return false; // monado not available
};
@@ -297,7 +297,7 @@ impl OpenXrInputSource {
let mut seen = Vec::::with_capacity(32);
for (mnd_role, wlx_role) in roles {
- let device = monado.device_from_role(mnd_role);
+ let device = monado.ipc.device_from_role(mnd_role);
if let Ok(mut device) = device
&& !seen.contains(&device.index)
{
@@ -305,7 +305,7 @@ impl OpenXrInputSource {
Self::update_device_battery_status(&mut device, wlx_role, &mut app.input_state);
}
}
- if let Ok(devices) = monado.devices() {
+ if let Ok(devices) = monado.ipc.devices() {
for mut device in devices {
if !seen.contains(&device.index) {
let role = if device.name_id >= 4 && device.name_id <= 8 {
diff --git a/wayvr/src/backend/openxr/mod.rs b/wayvr/src/backend/openxr/mod.rs
index 4d43cf94..8e7938f7 100644
--- a/wayvr/src/backend/openxr/mod.rs
+++ b/wayvr/src/backend/openxr/mod.rs
@@ -35,6 +35,7 @@ mod blocker;
mod helpers;
mod input;
mod lines;
+pub mod monado_state;
mod overlay;
mod playspace;
mod skybox;
@@ -85,15 +86,18 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
let mut delete_queue = vec![];
- app.monado_init();
+ app.monado_state_init();
- let mut playspace = app.monado.as_mut().and_then(|m| {
- playspace::PlayspaceMover::new(m)
+ let mut playspace = app.monado_state.as_mut().and_then(|m| {
+ playspace::PlayspaceMover::new(&mut m.ipc)
.map_err(|e| log::warn!("Will not use Monado playspace mover: {e}"))
.ok()
});
- let mut blocker = app.monado.as_ref().map(blocker::InputBlocker::new);
+ let mut blocker = app
+ .monado_state
+ .as_ref()
+ .map(|m| blocker::InputBlocker::new(&m.ipc));
let (session, mut frame_wait, mut frame_stream) = unsafe {
let raw_session = helpers::create_overlay_session(
@@ -211,7 +215,7 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
}
}
- if app.monado.is_some() && next_device_update <= Instant::now() {
+ if app.monado_state.is_some() && next_device_update <= Instant::now() {
let changed = OpenXrInputSource::update_devices(&mut app);
if changed {
overlays.devices_changed(&mut app)?;
@@ -403,6 +407,10 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
futures.wait()?;
// End rendering
+ if let Some(monado_state) = &mut app.monado_state {
+ monado_state.update();
+ }
+
// Layer composition
let mut layers = vec![];
if !main_session_visible && let Some(skybox) = skybox.as_mut() {
@@ -499,8 +507,8 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
watch.config.active_state.as_mut().unwrap().transform = watch_transform;
} // main_loop
- if let (Some(blocker), Some(monado)) = (blocker, app.monado.as_mut()) {
- blocker.unblock(monado);
+ if let (Some(blocker), Some(monado)) = (blocker, app.monado_state.as_mut()) {
+ blocker.unblock(&mut monado.ipc);
}
overlays.persist_layout(&mut app);
diff --git a/wayvr/src/backend/openxr/monado_state.rs b/wayvr/src/backend/openxr/monado_state.rs
new file mode 100644
index 00000000..418e5673
--- /dev/null
+++ b/wayvr/src/backend/openxr/monado_state.rs
@@ -0,0 +1,58 @@
+#[cfg(feature = "feat-monado-metrics")]
+use crate::subsystem::monado_metrics::{self, metrics_fd::MonadoMetricsFd};
+
+pub struct MonadoState {
+ pub ipc: libmonado::Monado,
+
+ #[cfg(feature = "feat-monado-metrics")]
+ pub metrics: Option,
+}
+
+impl MonadoState {
+ pub fn new() -> anyhow::Result {
+ let ipc = libmonado::Monado::auto_connect().map_err(|s| anyhow::anyhow!("{s}"))?;
+ let res = Self {
+ ipc,
+ #[cfg(feature = "feat-monado-metrics")]
+ metrics: None,
+ };
+ Ok(res)
+ }
+
+ pub fn update(&mut self) {
+ #[cfg(feature = "feat-monado-metrics")]
+ if let Some(metrics) = &mut self.metrics {
+ metrics.update();
+
+ if metrics.is_full() {
+ let _ = self.set_metrics_enabled(false); // disable metrics if they aren't used
+ }
+ }
+ }
+
+ pub fn set_metrics_enabled(&mut self, enabled: bool) -> anyhow::Result<()> {
+ #[cfg(feature = "feat-monado-metrics")]
+ {
+ if enabled {
+ if self.metrics.is_none() {
+ log::info!("Starting Monado metrics");
+ self.metrics = Some(monado_metrics::metrics_fd::MonadoMetricsFd::new(
+ &mut self.ipc,
+ )?);
+ }
+ } else {
+ if self.metrics.is_some() {
+ log::info!("Stopping Monado metrics");
+ }
+ self.metrics = None;
+ }
+ }
+ #[cfg(not(feature = "feat-monado-metrics"))]
+ {
+ #[allow(path_statements)]
+ enabled;
+ }
+
+ Ok(())
+ }
+}
diff --git a/wayvr/src/backend/openxr/playspace.rs b/wayvr/src/backend/openxr/playspace.rs
index 24bd3c02..3f02d377 100644
--- a/wayvr/src/backend/openxr/playspace.rs
+++ b/wayvr/src/backend/openxr/playspace.rs
@@ -44,19 +44,19 @@ impl PlayspaceMover {
}
pub fn handle_task(&mut self, app: &mut AppState, task: PlayspaceTask) {
- let Some(monado) = &mut app.monado else {
+ let Some(monado) = &mut app.monado_state else {
return; // monado not available
};
match task {
PlayspaceTask::FixFloor => {
- self.fix_floor(&app.input_state, monado);
+ self.fix_floor(&app.input_state, &mut monado.ipc);
}
PlayspaceTask::Reset => {
- self.reset_offset(monado);
+ self.reset_offset(&mut monado.ipc);
}
PlayspaceTask::Recenter => {
- self.recenter(&app.input_state, monado);
+ self.recenter(&app.input_state, &mut monado.ipc);
}
}
}
@@ -66,7 +66,7 @@ impl PlayspaceMover {
overlays: &mut OverlayWindowManager,
app: &mut AppState,
) {
- let Some(monado) = &mut app.monado else {
+ let Some(monado) = &mut app.monado_state else {
return; // monado not available
};
@@ -74,7 +74,7 @@ impl PlayspaceMover {
if pointer.now.space_reset {
if !pointer.before.space_reset {
log::info!("Space reset");
- self.reset_offset(monado);
+ self.reset_offset(&mut monado.ipc);
}
return;
}
@@ -111,7 +111,7 @@ impl PlayspaceMover {
data.pose *= space_transform;
data.hand_pose = new_hand;
- apply_offset(data.pose, monado);
+ apply_offset(data.pose, &mut monado.ipc);
self.rotate = Some(data);
} else {
for (i, pointer) in app.input_state.pointers.iter().enumerate() {
@@ -167,7 +167,7 @@ impl PlayspaceMover {
data.pose.translation += relative_pos;
data.hand_pose = new_hand;
- apply_offset(data.pose, monado);
+ apply_offset(data.pose, &mut monado.ipc);
self.drag = Some(data);
} else {
for (i, pointer) in app.input_state.pointers.iter().enumerate() {
diff --git a/wayvr/src/overlays/dashboard.rs b/wayvr/src/overlays/dashboard.rs
index 3ee46f7a..4d0b7958 100644
--- a/wayvr/src/overlays/dashboard.rs
+++ b/wayvr/src/overlays/dashboard.rs
@@ -470,20 +470,27 @@ impl DashInterface for DashInterfaceLive {
fn monado_client_list(
&mut self,
app: &mut AppState,
+ filtered: bool,
) -> anyhow::Result> {
- let Some(monado) = &mut app.monado else {
+ let Some(monado) = &mut app.monado_state else {
return Ok(Vec::new()); // no monado available
};
- let clients = monado_list_clients_filtered(monado)?;
+ let clients = if filtered {
+ monado_list_clients_filtered(&mut monado.ipc)?
+ } else {
+ monado.ipc.clients()?.into_iter().collect()
+ };
let mut res = Vec::::new();
for mut client in clients {
+ let client_id = client.id();
let name = client.name()?;
let state = client.state()?;
res.push(dash_interface::MonadoClient {
+ id: client_id as i64,
name,
is_primary: state.contains(libmonado::ClientState::ClientPrimaryApp),
is_active: state.contains(libmonado::ClientState::ClientSessionActive),
@@ -499,35 +506,117 @@ impl DashInterface for DashInterfaceLive {
#[cfg(feature = "openxr")]
fn monado_client_focus(&mut self, app: &mut AppState, name: &str) -> anyhow::Result<()> {
- let Some(monado) = &mut app.monado else {
+ let Some(monado) = &mut app.monado_state else {
return Ok(()); // no monado available
};
- monado_client_focus(monado, name)
+ monado_client_focus(&mut monado.ipc, name)
}
#[cfg(feature = "openxr")]
fn monado_brightness_get(&mut self, app: &mut AppState) -> Option {
- let Some(monado) = &mut app.monado else {
+ let Some(monado) = &mut app.monado_state else {
return None;
};
- monado_get_brightness(monado)
+ monado_get_brightness(&mut monado.ipc)
}
#[cfg(feature = "openxr")]
fn monado_brightness_set(&mut self, app: &mut AppState, brightness: f32) -> Option<()> {
- let Some(monado) = &mut app.monado else {
+ let Some(monado) = &mut app.monado_state else {
return None;
};
- monado_set_brightness(monado, brightness).ok()
+ monado_set_brightness(&mut monado.ipc, brightness).ok()
+ }
+
+ #[cfg(feature = "openxr")]
+ fn monado_metrics_set_enabled(&mut self, app: &mut AppState, enabled: bool) -> bool {
+ #[cfg(feature = "feat-monado-metrics")]
+ {
+ let Some(monado) = &mut app.monado_state else {
+ return false;
+ };
+
+ if let Err(e) = monado.set_metrics_enabled(enabled) {
+ log::error!("failed to enable metrics: {e:?}");
+ return false;
+ }
+ true
+ }
+ #[cfg(not(feature = "feat-monado-metrics"))]
+ #[allow(path_statements)]
+ {
+ app;
+ enabled;
+ false
+ }
+ }
+
+ #[cfg(feature = "openxr")]
+ #[allow(clippy::match_same_arms)]
+ fn monado_metrics_dump_session_frames(
+ &mut self,
+ app: &mut AppState,
+ ) -> Vec {
+ #[cfg(feature = "feat-monado-metrics")]
+ {
+ let Some(monado) = &mut app.monado_state else {
+ return Vec::new();
+ };
+ let Some(metrics) = &mut monado.metrics else {
+ return Vec::new(); // metrics not enabled or not available
+ };
+
+ metrics
+ .dump_records()
+ .iter()
+ .filter_map(|record| {
+ use crate::subsystem::monado_metrics::proto::record;
+ let record = record.record?;
+ match record {
+ record::Record::SessionFrame(sframe) => {
+ // map it to our struct
+ Some(dash_interface::MonadoDumpSessionFrame {
+ session_id: sframe.session_id,
+ frame_id: sframe.frame_id,
+ predicted_frame_time_ns: sframe.predicted_frame_time_ns,
+ predicted_wake_up_time_ns: sframe.predicted_wake_up_time_ns,
+ predicted_gpu_done_time_ns: sframe.predicted_gpu_done_time_ns,
+ predicted_display_time_ns: sframe.predicted_display_time_ns,
+ predicted_display_period_ns: sframe.predicted_display_period_ns,
+ display_time_ns: sframe.display_time_ns,
+ when_predicted_ns: sframe.when_predicted_ns,
+ when_wait_woke_ns: sframe.when_wait_woke_ns,
+ when_begin_ns: sframe.when_begin_ns,
+ when_delivered_ns: sframe.when_delivered_ns,
+ when_gpu_done_ns: sframe.when_gpu_done_ns,
+ discarded: sframe.discarded,
+ })
+ }
+ record::Record::Version(_) => None,
+ record::Record::Used(_) => None,
+ record::Record::SystemFrame(_) => None,
+ record::Record::SystemGpuInfo(_) => None,
+ record::Record::SystemPresentInfo(_) => None,
+ }
+ })
+ .collect()
+ }
+ #[cfg(not(feature = "feat-monado-metrics"))]
+ #[allow(path_statements)]
+ {
+ app;
+ Vec::new()
+ }
}
#[cfg(not(feature = "openxr"))]
fn monado_client_list(
&mut self,
_: &mut AppState,
+ _filtered: bool,
) -> anyhow::Result> {
anyhow::bail!("Not supported in this build.")
}
@@ -543,6 +632,20 @@ impl DashInterface for DashInterfaceLive {
fn monado_brightness_set(&mut self, _: &mut AppState, _: f32) -> Option<()> {
None
}
+
+ #[cfg(not(feature = "openxr"))]
+ fn monado_metrics_set_enabled(&mut self, _: &mut AppState, _enabled: bool) -> bool {
+ // not supported in this build
+ false
+ }
+
+ #[cfg(not(feature = "openxr"))]
+ fn monado_metrics_dump_session_frames(
+ &mut self,
+ _: &mut AppState,
+ ) -> Vec {
+ Vec::new() // not supported in this build
+ }
}
#[cfg(feature = "openxr")]
diff --git a/wayvr/src/state.rs b/wayvr/src/state.rs
index 5567f48f..9e8d7da2 100644
--- a/wayvr/src/state.rs
+++ b/wayvr/src/state.rs
@@ -9,6 +9,7 @@ use wgui::{
drawing, font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals, parser::parse_color_hex,
renderer_vk::context::SharedContext as WSharedContext,
};
+use wlx_common::async_executor::AsyncExecutor;
use wlx_common::locale::WayVRLangProvider;
use wlx_common::{
audio,
@@ -18,7 +19,10 @@ use wlx_common::{
overlays::{ToastDisplayMethod, ToastTopic},
};
+#[cfg(feature = "openxr")]
+use crate::backend;
use crate::backend::wayvr::WvrServerState;
+
#[cfg(feature = "osc")]
use crate::subsystem::osc::OscSender;
@@ -34,6 +38,7 @@ use crate::{
pub struct AppState {
pub session: AppSession,
pub tasks: TaskContainer,
+ pub executor: AsyncExecutor,
pub gfx: Arc,
pub gfx_extras: WGfxExtras,
@@ -67,7 +72,7 @@ pub struct AppState {
pub wvr_server: Option,
#[cfg(feature = "openxr")]
- pub monado: Option,
+ pub monado_state: Option,
}
#[allow(unused_mut)]
@@ -152,9 +157,12 @@ impl AppState {
let lang_provider = WayVRLangProvider::from_config(&session.config);
+ let executor = Rc::new(smol::LocalExecutor::new());
+
Ok(Self {
session,
tasks,
+ executor,
gfx,
gfx_extras,
hid_provider,
@@ -184,17 +192,25 @@ impl AppState {
wvr_server,
#[cfg(feature = "openxr")]
- monado: None,
+ monado_state: None,
})
}
#[cfg(feature = "openxr")]
- pub fn monado_init(&mut self) {
+ pub fn monado_state_init(&mut self) {
+ use crate::backend::openxr::monado_state::MonadoState;
+
log::debug!("Connecting to Monado IPC");
- self.monado = None; // stop connection first
- self.monado = libmonado::Monado::auto_connect()
- .map_err(|e| log::warn!("Will not use libmonado: {e}"))
- .ok();
+ self.monado_state = None; // stop connection first
+
+ match MonadoState::new() {
+ Ok(m) => {
+ self.monado_state = Some(m);
+ }
+ Err(e) => {
+ log::error!("Will not use libmonado: {e:?}");
+ }
+ }
}
}
diff --git a/wayvr/src/subsystem/mod.rs b/wayvr/src/subsystem/mod.rs
index abf66273..04d997b9 100644
--- a/wayvr/src/subsystem/mod.rs
+++ b/wayvr/src/subsystem/mod.rs
@@ -5,3 +5,7 @@ pub mod notifications;
#[cfg(feature = "osc")]
pub mod osc;
+
+#[cfg(feature = "openxr")]
+#[cfg(feature = "feat-monado-metrics")]
+pub mod monado_metrics;
diff --git a/wayvr/src/subsystem/monado_metrics/README.md b/wayvr/src/subsystem/monado_metrics/README.md
new file mode 100644
index 00000000..84590d3f
--- /dev/null
+++ b/wayvr/src/subsystem/monado_metrics/README.md
@@ -0,0 +1,10 @@
+`proto.rs` can be generated via prost_build tool.
+
+To generate a new version of protobuf file:
+
+```
+cd scripts/prost_build
+OUT_DIR=your_directory cargo run
+```
+
+new protobuf files will be generated in `your_directory` directory.
diff --git a/wayvr/src/subsystem/monado_metrics/metrics_fd.rs b/wayvr/src/subsystem/monado_metrics/metrics_fd.rs
new file mode 100644
index 00000000..983a39c9
--- /dev/null
+++ b/wayvr/src/subsystem/monado_metrics/metrics_fd.rs
@@ -0,0 +1,70 @@
+use std::{
+ collections::VecDeque,
+ io::Read,
+ os::{fd::AsFd, unix::net::UnixStream},
+};
+
+use crate::subsystem::monado_metrics::proto;
+
+pub struct MonadoMetricsFd {
+ stream_reader: UnixStream,
+ stream_writer: UnixStream,
+
+ records: VecDeque,
+}
+
+const RECORD_QUEUE_SIZE: usize = 500;
+
+impl MonadoMetricsFd {
+ pub fn new(monado: &mut libmonado::Monado) -> anyhow::Result {
+ let (stream_reader, stream_writer) = std::os::unix::net::UnixStream::pair()?;
+ stream_writer.set_nonblocking(true)?;
+ stream_reader.set_nonblocking(true)?;
+
+ monado.push_metrics_fd(stream_writer.as_fd(), true)?;
+
+ Ok(Self {
+ stream_reader,
+ stream_writer,
+ records: VecDeque::new(),
+ })
+ }
+
+ fn parse_message(&mut self, record: proto::Record) {
+ self.records.push_back(record);
+ if self.records.len() >= RECORD_QUEUE_SIZE {
+ self.records.pop_front();
+ }
+ }
+
+ pub fn dump_records(&mut self) -> Vec {
+ let records = std::mem::take(&mut self.records);
+ records.into_iter().collect()
+ }
+
+ pub fn is_full(&self) -> bool {
+ self.records.len() >= RECORD_QUEUE_SIZE - 1
+ }
+
+ // called every frame
+ pub fn update(&mut self) {
+ let mut buf: [u8; 1024] = [0; 1024];
+
+ while let Ok(byte_count) = self.stream_reader.read(&mut buf) {
+ if byte_count == 0 {
+ debug_assert!(false);
+ break;
+ }
+
+ let res: Result = prost::Message::decode_length_delimited(&buf[..]);
+ match res {
+ Ok(record) => {
+ self.parse_message(record);
+ }
+ Err(e) => {
+ log::error!("decode error: {e}");
+ }
+ }
+ }
+ }
+}
diff --git a/wayvr/src/subsystem/monado_metrics/mod.rs b/wayvr/src/subsystem/monado_metrics/mod.rs
new file mode 100644
index 00000000..82a86c07
--- /dev/null
+++ b/wayvr/src/subsystem/monado_metrics/mod.rs
@@ -0,0 +1,2 @@
+pub mod metrics_fd;
+pub mod proto;
diff --git a/wayvr/src/subsystem/monado_metrics/proto.rs b/wayvr/src/subsystem/monado_metrics/proto.rs
new file mode 100644
index 00000000..0232dc36
--- /dev/null
+++ b/wayvr/src/subsystem/monado_metrics/proto.rs
@@ -0,0 +1,180 @@
+// This file is @generated by prost-build.
+#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct Version {
+ #[prost(uint32, tag = "1")]
+ pub major: u32,
+ #[prost(uint32, tag = "2")]
+ pub minor: u32,
+}
+#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct SessionFrame {
+ /// ! Which session this frame belongs to.
+ #[prost(int64, tag = "1")]
+ pub session_id: i64,
+ /// ! ID of frame.
+ #[prost(int64, tag = "2")]
+ pub frame_id: i64,
+ /// ! How long we thought the frame would take.
+ #[prost(uint64, tag = "3")]
+ pub predicted_frame_time_ns: u64,
+ /// ! When we predicted the app should wake up.
+ #[prost(uint64, tag = "4")]
+ pub predicted_wake_up_time_ns: u64,
+ /// ! When the client's GPU work should have completed.
+ #[prost(uint64, tag = "5")]
+ pub predicted_gpu_done_time_ns: u64,
+ /// ! When we predicted this frame to be shown.
+ #[prost(uint64, tag = "6")]
+ pub predicted_display_time_ns: u64,
+ /// ! The selected display period.
+ #[prost(uint64, tag = "7")]
+ pub predicted_display_period_ns: u64,
+ /// !
+ /// When the app told us to display this frame, can be different
+ /// then the predicted display time so we track that separately.
+ #[prost(uint64, tag = "8")]
+ pub display_time_ns: u64,
+ /// ! When the predict call was made (inside of xrWaitFrame).
+ #[prost(uint64, tag = "9")]
+ pub when_predicted_ns: u64,
+ /// ! When the waiting thread was woken up.
+ #[prost(uint64, tag = "10")]
+ pub when_wait_woke_ns: u64,
+ /// ! When xrBeginFrame was called.
+ #[prost(uint64, tag = "11")]
+ pub when_begin_ns: u64,
+ /// ! When the layer information was delivered, (inside of xrEndFrame).
+ #[prost(uint64, tag = "12")]
+ pub when_delivered_ns: u64,
+ /// ! When the scheduled GPU work was completed.
+ #[prost(uint64, tag = "13")]
+ pub when_gpu_done_ns: u64,
+ /// ! Was this frame discarded.
+ #[prost(bool, tag = "14")]
+ pub discarded: bool,
+}
+#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct Used {
+ /// ! Owning session of the frame.
+ #[prost(int64, tag = "1")]
+ pub session_id: i64,
+ /// ! Which session frame was used by the compositor.
+ #[prost(int64, tag = "2")]
+ pub session_frame_id: i64,
+ /// ! The system frame that the session was used by.
+ #[prost(int64, tag = "3")]
+ pub system_frame_id: i64,
+ /// ! When the frame was latched.
+ #[prost(uint64, tag = "4")]
+ pub when_ns: u64,
+}
+#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct SystemFrame {
+ /// ! ID of frame.
+ #[prost(int64, tag = "1")]
+ pub frame_id: i64,
+ /// ! Projected pixels to photon time.
+ #[prost(uint64, tag = "2")]
+ pub predicted_display_time_ns: u64,
+ /// ! Current period of displaying of frames.
+ #[prost(uint64, tag = "3")]
+ pub predicted_display_period_ns: u64,
+ /// ! When the compositor should hand pixels to display hardware.
+ #[prost(uint64, tag = "4")]
+ pub desired_present_time_ns: u64,
+ /// ! The time that the compositor should wake up.
+ #[prost(uint64, tag = "5")]
+ pub wake_up_time_ns: u64,
+ #[prost(uint64, tag = "6")]
+ pub present_slop_ns: u64,
+}
+/// !
+/// Out of band info about a GPU timing from a system compositor.
+#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct SystemGpuInfo {
+ /// ! ID of frame.
+ #[prost(int64, tag = "1")]
+ pub frame_id: i64,
+ /// ! Start of GPU work.
+ #[prost(uint64, tag = "2")]
+ pub gpu_start_ns: u64,
+ /// ! End of GPU work.
+ #[prost(uint64, tag = "3")]
+ pub gpu_end_ns: u64,
+ /// ! When the information was gathered.
+ #[prost(uint64, tag = "4")]
+ pub when_ns: u64,
+}
+/// !
+/// Information that comes from the vulkan present timing.
+#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct SystemPresentInfo {
+ /// ! ID of frame.
+ #[prost(int64, tag = "1")]
+ pub frame_id: i64,
+ /// ! The total expected time for the compositor to complete a frame.
+ #[prost(uint64, tag = "2")]
+ pub expected_comp_time_ns: u64,
+ /// ! The time we predicted the compositor to wake up time.
+ #[prost(uint64, tag = "3")]
+ pub predicted_wake_up_time_ns: u64,
+ /// ! Predicted time for completion of the GPU work.
+ #[prost(uint64, tag = "4")]
+ pub predicted_done_time_ns: u64,
+ /// ! Predicted display time.
+ #[prost(uint64, tag = "5")]
+ pub predicted_display_time_ns: u64,
+ /// ! When was this frame predicted.
+ #[prost(uint64, tag = "6")]
+ pub when_predict_ns: u64,
+ /// ! When we last woke up the compositor after its equivalent of wait_frame.
+ #[prost(uint64, tag = "7")]
+ pub when_woke_ns: u64,
+ /// ! When the compositor started rendering a frame
+ #[prost(uint64, tag = "8")]
+ pub when_began_ns: u64,
+ /// ! When the compositor finished rendering a frame
+ #[prost(uint64, tag = "9")]
+ pub when_submitted_ns: u64,
+ /// ! When new frame timing info was last added.
+ #[prost(uint64, tag = "10")]
+ pub when_infoed_ns: u64,
+ /// ! When we wanted to start presenting.
+ #[prost(uint64, tag = "11")]
+ pub desired_present_time_ns: u64,
+ /// ! The slop used for this frame.
+ #[prost(uint64, tag = "12")]
+ pub present_slop_ns: u64,
+ /// ! Margin of GPU to present time.
+ #[prost(uint64, tag = "13")]
+ pub present_margin_ns: u64,
+ /// ! When the present time actually happened.
+ #[prost(uint64, tag = "14")]
+ pub actual_present_time_ns: u64,
+ /// ! The earliest we could have presented.
+ #[prost(uint64, tag = "15")]
+ pub earliest_present_time_ns: u64,
+}
+#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
+pub struct Record {
+ #[prost(oneof = "record::Record", tags = "1, 2, 3, 4, 5, 6")]
+ pub record: ::core::option::Option,
+}
+/// Nested message and enum types in `Record`.
+pub mod record {
+ #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Oneof)]
+ pub enum Record {
+ #[prost(message, tag = "1")]
+ Version(super::Version),
+ #[prost(message, tag = "2")]
+ SessionFrame(super::SessionFrame),
+ #[prost(message, tag = "3")]
+ Used(super::Used),
+ #[prost(message, tag = "4")]
+ SystemFrame(super::SystemFrame),
+ #[prost(message, tag = "5")]
+ SystemGpuInfo(super::SystemGpuInfo),
+ #[prost(message, tag = "6")]
+ SystemPresentInfo(super::SystemPresentInfo),
+ }
+}
diff --git a/wgui/doc/widgets.md b/wgui/doc/widgets.md
index 24817b15..2c094160 100644
--- a/wgui/doc/widgets.md
+++ b/wgui/doc/widgets.md
@@ -6,7 +6,7 @@
## [Built-in components](#components)
-[Button](#button-component), [Slider](#slider-component), [CheckBox](#checkbox-component), [Tabs](#tabs-component) ([Tab](#tab-component)), [EditBox](#editbox-component)
+[Button](#button-component), [Slider](#slider-component), [CheckBox](#checkbox-component), [Tabs](#tabs-component) ([Tab](#tab-component)), [EditBox](#editbox-component), [BarGraph](#bargraph-component)
## [Examples](#examples)
@@ -448,6 +448,30 @@ _Image path (see [sprite](#sprite-widget)) for src descriptions_
_Initial text content_
+## BarGraph component
+
+### ``
+
+### A bar graph widget
+
+#### Parameters
+
+`limit_min`: **float**
+
+_Minimum limit value_
+
+`limit_max`: **float**
+
+_Maximum limit value_
+
+`unit`: **string** (default: "")
+
+_Unit type, for example "%" or "ms"_
+
+`capacity`: **int** (default: 50)
+
+_Value count_
+
---
# Examples
diff --git a/wgui/src/components/bar_graph.rs b/wgui/src/components/bar_graph.rs
new file mode 100644
index 00000000..083369a4
--- /dev/null
+++ b/wgui/src/components/bar_graph.rs
@@ -0,0 +1,237 @@
+use std::{cell::RefCell, collections::VecDeque, rc::Rc};
+
+use glam::Vec2;
+use taffy::{
+ FlexDirection, JustifyContent,
+ prelude::{auto, length, percent},
+};
+
+use crate::{
+ components::{Component, ComponentBase, ComponentTrait, RefreshData},
+ drawing::{self, GradientMode, PrimitiveExtent, RenderPrimitive},
+ event::CallbackDataCommon,
+ i18n::Translation,
+ layout::{WidgetID, WidgetPair},
+ renderer_vk::text::{FontWeight, HorizontalAlign, TextStyle},
+ widget::{
+ ConstructEssentials,
+ custom_draw::{WidgetCustomDraw, WidgetCustomDrawParams},
+ div::WidgetDiv,
+ label::{WidgetLabel, WidgetLabelParams},
+ rectangle::{WidgetRectangle, WidgetRectangleParams},
+ util::WLength,
+ },
+};
+
+#[derive(Default)]
+pub struct Params {
+ pub style: taffy::Style,
+ pub limits: (f32, f32),
+ pub unit: String,
+ pub capacity: u32,
+}
+
+pub struct ValueCell {
+ pub value: f32,
+ pub color: drawing::Color,
+}
+
+struct State {
+ limits: (f32, f32), /* min - max */
+ values: VecDeque,
+}
+
+#[allow(clippy::struct_field_names)]
+struct Data {
+ #[allow(dead_code)]
+ id_root: WidgetID,
+
+ id_label_val_min: WidgetID,
+ id_label_val_max: WidgetID,
+
+ unit: String,
+ capacity: u32,
+}
+
+pub struct ComponentBarGraph {
+ base: ComponentBase,
+ data: Rc,
+ state: Rc>,
+}
+
+impl ComponentTrait for ComponentBarGraph {
+ fn base(&self) -> &ComponentBase {
+ &self.base
+ }
+
+ fn base_mut(&mut self) -> &mut ComponentBase {
+ &mut self.base
+ }
+
+ fn refresh(&self, data: &mut RefreshData) {
+ let state = self.state.borrow();
+ self.update_limits_text(&state, data.common);
+ }
+}
+
+impl ComponentBarGraph {
+ fn update_limits_text(&self, state: &State, c: &mut CallbackDataCommon) -> Option<()> {
+ let mut label_val_min = c.state.widgets.get_as::(self.data.id_label_val_min)?;
+ let mut label_val_max = c.state.widgets.get_as::(self.data.id_label_val_max)?;
+
+ label_val_min.set_text(
+ c,
+ Translation::from_raw_text_string(format!("{}{}", state.limits.0, self.data.unit)),
+ );
+ label_val_max.set_text(
+ c,
+ Translation::from_raw_text_string(format!("{}{}", state.limits.1, self.data.unit)),
+ );
+
+ Some(())
+ }
+
+ pub fn set_limits(&self, c: &mut CallbackDataCommon, limits: (f32, f32)) {
+ let mut state = self.state.borrow_mut();
+ state.limits = limits;
+ self.update_limits_text(&state, c);
+ }
+
+ pub fn push_value(&self, cell: ValueCell) {
+ let mut state = self.state.borrow_mut();
+ if state.values.len() > self.data.capacity as usize {
+ state.values.pop_front();
+ }
+ state.values.push_back(cell);
+ }
+}
+
+pub fn construct(
+ ess: &mut ConstructEssentials,
+ mut params: Params,
+) -> anyhow::Result<(WidgetPair, Rc)> {
+ params.style.flex_direction = FlexDirection::Row;
+ params.style.gap = length(4.0);
+
+ // override style
+ let (root, _) = ess.layout.add_child(ess.parent, WidgetDiv::create(), params.style)?;
+
+ let (vertical_texts, _) = ess.layout.add_child(
+ root.id,
+ WidgetDiv::create(),
+ taffy::Style {
+ justify_content: Some(JustifyContent::SpaceBetween),
+ flex_direction: FlexDirection::Column,
+ size: taffy::Size {
+ width: auto(),
+ height: percent(1.0),
+ },
+ ..Default::default()
+ },
+ )?;
+
+ let (rect, _) = ess.layout.add_child(
+ root.id,
+ WidgetRectangle::create(WidgetRectangleParams {
+ border: 2.0,
+ border_color: drawing::Color::new(1.0, 1.0, 1.0, 0.5),
+ round: WLength::Units(3.0),
+ gradient: GradientMode::Vertical,
+ color: drawing::Color::new(0.0, 0.0, 0.0, 0.6),
+ ..Default::default()
+ }),
+ taffy::Style {
+ position: taffy::Position::Relative,
+ size: taffy::Size {
+ width: percent(1.0),
+ height: percent(1.0),
+ },
+ ..Default::default()
+ },
+ )?;
+
+ let state = Rc::new(RefCell::new(State {
+ limits: params.limits,
+ values: VecDeque::new(),
+ }));
+
+ let (_, _) = ess.layout.add_child(
+ rect.id,
+ WidgetCustomDraw::create(WidgetCustomDrawParams {
+ func: {
+ let state = state.clone();
+ Box::new(move |info| {
+ let state = state.borrow();
+ let (limit_min, limit_max) = state.limits;
+
+ let box_width = info.boundary.width();
+ let box_height = info.boundary.height();
+
+ let bar_width = box_width / state.values.len() as f32;
+
+ for (idx, cell) in state.values.iter().enumerate() {
+ let norm_value = ((cell.value - limit_min) / (limit_max - limit_min)).clamp(0.0, 1.0);
+ let bar_height = norm_value * box_height;
+ let bar_x = bar_width * idx as f32;
+ let bar_y = box_height - bar_height;
+
+ info.primitives.push(RenderPrimitive::Rectangle(
+ PrimitiveExtent {
+ boundary: drawing::Boundary {
+ pos: Vec2::new(bar_x, bar_y),
+ size: Vec2::new(bar_width, bar_height),
+ },
+ transform: info.transform.transform,
+ },
+ drawing::Rectangle {
+ color: cell.color,
+ ..Default::default()
+ },
+ ));
+ }
+ })
+ },
+ }),
+ taffy::Style {
+ size: taffy::Size {
+ width: percent(1.0),
+ height: percent(1.0),
+ },
+ ..Default::default()
+ },
+ )?;
+
+ let label_params = WidgetLabelParams {
+ style: TextStyle {
+ align: Some(HorizontalAlign::Right),
+ weight: Some(FontWeight::Bold),
+ size: Some(11.0),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+
+ let label = WidgetLabel::create(&mut ess.layout.state, label_params.clone());
+ let (label_val_max, _) = ess.layout.add_child(vertical_texts.id, label, Default::default())?;
+
+ let label = WidgetLabel::create(&mut ess.layout.state, label_params);
+ let (label_val_min, _) = ess.layout.add_child(vertical_texts.id, label, Default::default())?;
+
+ let data = Rc::new(Data {
+ id_root: root.id,
+ id_label_val_min: label_val_min.id,
+ id_label_val_max: label_val_max.id,
+ unit: params.unit,
+ capacity: params.capacity,
+ });
+
+ let base = ComponentBase {
+ id: root.id,
+ lhandles: Vec::new(),
+ };
+
+ let bar_graph = Rc::new(ComponentBarGraph { base, data, state });
+
+ ess.layout.defer_component_refresh(Component(bar_graph.clone()));
+ Ok((root, bar_graph))
+}
diff --git a/wgui/src/components/mod.rs b/wgui/src/components/mod.rs
index 57ef7398..b5916f18 100644
--- a/wgui/src/components/mod.rs
+++ b/wgui/src/components/mod.rs
@@ -7,6 +7,7 @@ use crate::{
layout::WidgetID,
};
+pub mod bar_graph;
pub mod button;
pub mod checkbox;
pub mod editbox;
diff --git a/wgui/src/drawing.rs b/wgui/src/drawing.rs
index 6546fe5a..71a66e43 100644
--- a/wgui/src/drawing.rs
+++ b/wgui/src/drawing.rs
@@ -225,11 +225,13 @@ pub struct ImagePrimitive {
pub round_units: u8,
}
+#[derive(Clone)]
pub struct PrimitiveExtent {
pub(super) boundary: Boundary,
pub(super) transform: Mat4,
}
+#[derive(Clone)]
pub enum RenderPrimitive {
NewPass,
Rectangle(PrimitiveExtent, Rectangle),
diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs
index 2efb3398..2569f8b7 100644
--- a/wgui/src/layout.rs
+++ b/wgui/src/layout.rs
@@ -377,7 +377,7 @@ impl Layout {
self.registered_components_to_refresh.insert(*node_id, component.weak());
}
- /// Convenience function to avoid repeated `WidgetID` → `WidgetState` lookups.
+ /// Convenience function to avoid repeated `WidgetID` → `WidgetState` look-ups.
pub fn add_event_listener(
&self,
widget_id: WidgetID,
@@ -883,6 +883,7 @@ impl Layout {
widget::WidgetType::Label => drawing::Color::new(0.4, 1.0, 0.0, 1.0),
widget::WidgetType::Sprite => drawing::Color::new(0.0, 0.8, 1.0, 1.0),
widget::WidgetType::Rectangle => drawing::Color::new(1.0, 0.5, 0.2, 1.0),
+ widget::WidgetType::CustomDraw => drawing::Color::new(1.0, 1.0, 1.0, 1.0),
};
let line = format!(
diff --git a/wgui/src/parser/component_bar_graph.rs b/wgui/src/parser/component_bar_graph.rs
new file mode 100644
index 00000000..0ae4385b
--- /dev/null
+++ b/wgui/src/parser/component_bar_graph.rs
@@ -0,0 +1,56 @@
+use crate::{
+ components::{Component, bar_graph},
+ layout::WidgetID,
+ parser::{AttribPair, ParserContext, process_component, style::parse_style},
+ widget::ConstructEssentials,
+};
+
+pub fn parse_component_bar_graph(
+ ctx: &mut ParserContext,
+ parent_id: WidgetID,
+ attribs: &[AttribPair],
+ tag_name: &str,
+) -> anyhow::Result {
+ let style = parse_style(ctx, attribs, tag_name);
+ let mut limit_min = 0.0;
+ let mut capacity = 50;
+ let mut limit_max = 100.0;
+ let mut unit = String::new();
+
+ for pair in attribs {
+ let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref());
+ #[allow(clippy::single_match)]
+ match key {
+ "capacity" => {
+ ctx.parse_check_i32(tag_name, key, value, &mut capacity);
+ }
+ "limit_min" => {
+ ctx.parse_check_f32(tag_name, key, value, &mut limit_min);
+ }
+ "limit_max" => {
+ ctx.parse_check_f32(tag_name, key, value, &mut limit_max);
+ }
+ "unit" => {
+ unit = value.to_string();
+ }
+ _ => {}
+ }
+ }
+
+ let (widget, component) = bar_graph::construct(
+ &mut ConstructEssentials {
+ layout: ctx.layout,
+ parent: parent_id,
+ },
+ bar_graph::Params {
+ style,
+ limits: (limit_min, limit_max),
+ unit,
+ capacity: capacity.try_into().unwrap_or(50),
+ },
+ )?;
+
+ process_component(ctx, Component(component), widget.id, attribs);
+
+ Ok(widget.id)
+}
diff --git a/wgui/src/parser/mod.rs b/wgui/src/parser/mod.rs
index b5f5fa89..a715b4df 100644
--- a/wgui/src/parser/mod.rs
+++ b/wgui/src/parser/mod.rs
@@ -1,3 +1,4 @@
+mod component_bar_graph;
mod component_button;
mod component_checkbox;
mod component_editbox;
@@ -21,6 +22,7 @@ use crate::{
layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair},
log::LogErr,
parser::{
+ component_bar_graph::parse_component_bar_graph,
component_button::parse_component_button,
component_checkbox::{CheckboxKind, parse_component_checkbox},
component_editbox::parse_component_editbox,
@@ -201,7 +203,6 @@ impl Fetchable for ParserData {
let casted = widget
.get_as::()
.ok_or_else(|| anyhow::anyhow!("fetch_widget_as({id}): failed to cast"))?;
-
Ok(casted)
}
}
@@ -217,16 +218,40 @@ pub struct ParserState {
}
impl ParserState {
- /// This function is suitable in cases if you don't want to pollute main parser state with dynamic IDs
- /// Use `instantiate_template` instead unless you want to handle `components` results yourself.
- /// Make sure not to drop them if you want to have your listener handles valid
- pub fn parse_template(
+ /// Parse named tag and process it.
+ /// Preferred method of parsing templates. Same as `parse_template_only`,
+ /// but it keeps components data in this `ParserState` object for you.
+ /// The result can be safely dropped, all required event listeners and components
+ /// will be kept intact in this `ParserState`.
+ /// Resulting ParserData::components Vec will be left empty (they are moved into this `ParserState::data`)
+ pub fn realize_template(
&mut self,
doc_params: &ParseDocumentParams,
template_name: &str,
layout: &mut Layout,
widget_id: WidgetID,
template_parameters: HashMap, Rc>,
+ ) -> anyhow::Result {
+ let mut parser_data =
+ self.parse_template_only(doc_params, template_name, layout, widget_id, template_parameters)?;
+ // Collect components contained in this freshly-parsed template
+ self.data.components.append(&mut parser_data.components);
+ Ok(parser_data)
+ }
+
+ /// Parse named tag and process it.
+ /// Semi-internal - This function is suitable in cases if you don't want to pollute
+ /// the main parser state state with dynamic IDs (this won't propagate components!)
+ /// Use `realize_template` (or in some rare cases: `instantiate_template`) instead unless you want to handle `components` results yourself.
+ /// Make sure not to drop resulting ParserData if you want to have your listener handles valid
+ /// (they are contained in components). Use `realize_template` instead if you don't want to think about it.
+ pub fn parse_template_only(
+ &self,
+ doc_params: &ParseDocumentParams,
+ template_name: &str,
+ layout: &mut Layout,
+ widget_id: WidgetID,
+ template_parameters: HashMap, Rc>,
) -> anyhow::Result {
let Some(template) = self.data.templates.get(template_name) else {
anyhow::bail!(
@@ -252,7 +277,13 @@ impl ParserState {
Ok(ctx.data_local)
}
- /// Instantiate template by saving all the results into the main `ParserState`
+ /// Parse named tag and process it.
+ /// Instantiate template by saving all the results into the main `ParserState`.
+ /// Be aware you this function will save ALL parsed IDs and other metadata
+ /// into your main ParserState context (deep move).
+ /// You shouldn't instantiate the same template twice, to prevent ID name clash.
+ /// Consider using `parse_template_only` or `realize_template` instead if you want
+ /// to instantiate more than a single template of the same type.
pub fn instantiate_template(
&mut self,
doc_params: &ParseDocumentParams,
@@ -261,7 +292,7 @@ impl ParserState {
widget_id: WidgetID,
template_parameters: HashMap, Rc>,
) -> anyhow::Result<()> {
- let mut data_local = self.parse_template(doc_params, template_name, layout, widget_id, template_parameters)?;
+ let mut data_local = self.parse_template_only(doc_params, template_name, layout, widget_id, template_parameters)?;
self.data.take_results_from(&mut data_local);
Ok(())
@@ -1056,6 +1087,7 @@ fn parse_child<'a>(
)?);
}
"EditBox" => new_widget_id = Some(parse_component_editbox(ctx, parent_id, &attribs, tag_name)?),
+ "BarGraph" => new_widget_id = Some(parse_component_bar_graph(ctx, parent_id, &attribs, tag_name)?),
"Tabs" => {
new_widget_id = Some(parse_component_tabs(ctx, child_node, parent_id, &attribs, tag_name)?);
}
diff --git a/wgui/src/renderer_vk/text/text_atlas.rs b/wgui/src/renderer_vk/text/text_atlas.rs
index 2b91d9ed..dc40d02e 100644
--- a/wgui/src/renderer_vk/text/text_atlas.rs
+++ b/wgui/src/renderer_vk/text/text_atlas.rs
@@ -187,7 +187,7 @@ impl InnerAtlas {
// Grow each dimension by a factor of 2. The growth factor was chosen to match the growth
// factor of `Vec`.`
let new_size = (self.size * GROWTH_FACTOR).min(self.max_texture_dimension_2d);
- log::info!("Grow {:?} atlas {} → {new_size}", self.kind, self.size);
+ log::debug!("Grow {:?} atlas {} → {new_size}", self.kind, self.size);
self.packer.grow(size2(new_size as i32, new_size as i32));
diff --git a/wgui/src/widget/custom_draw.rs b/wgui/src/widget/custom_draw.rs
new file mode 100644
index 00000000..e262bca4
--- /dev/null
+++ b/wgui/src/widget/custom_draw.rs
@@ -0,0 +1,65 @@
+use slotmap::Key;
+
+use crate::{
+ drawing::{self},
+ layout::WidgetID,
+ stack,
+ widget::WidgetStateFlags,
+};
+
+use super::{WidgetObj, WidgetState};
+
+pub struct CustomDrawArgs<'a> {
+ pub boundary: &'a drawing::Boundary,
+ pub transform: &'a stack::Transform,
+ pub primitives: &'a mut Vec,
+}
+
+pub struct WidgetCustomDrawParams {
+ pub func: Box,
+}
+
+// FIXME: bring up a better name for this
+pub struct WidgetCustomDraw {
+ pub params: WidgetCustomDrawParams,
+ id: WidgetID,
+}
+
+impl WidgetCustomDraw {
+ pub fn create(params: WidgetCustomDrawParams) -> WidgetState {
+ WidgetState::new(
+ WidgetStateFlags::default(),
+ Box::new(Self {
+ params,
+ id: WidgetID::null(),
+ }),
+ )
+ }
+}
+
+impl WidgetObj for WidgetCustomDraw {
+ fn draw(&mut self, state: &mut super::DrawState, _params: &super::DrawParams) {
+ let boundary = drawing::Boundary::construct_relative(state.transform_stack);
+ (*self.params.func)(CustomDrawArgs {
+ primitives: state.primitives,
+ boundary: &boundary,
+ transform: state.transform_stack.get(),
+ });
+ }
+
+ fn get_id(&self) -> WidgetID {
+ self.id
+ }
+
+ fn set_id(&mut self, id: WidgetID) {
+ self.id = id;
+ }
+
+ fn get_type(&self) -> super::WidgetType {
+ super::WidgetType::Rectangle
+ }
+
+ fn debug_print(&self) -> String {
+ String::default()
+ }
+}
diff --git a/wgui/src/widget/label.rs b/wgui/src/widget/label.rs
index e83bb46f..8e4cf0b2 100644
--- a/wgui/src/widget/label.rs
+++ b/wgui/src/widget/label.rs
@@ -17,7 +17,7 @@ use crate::{
use super::{WidgetObj, WidgetState};
-#[derive(Debug, Default)]
+#[derive(Debug, Clone, Default)]
pub struct WidgetLabelParams {
pub content: Translation,
pub style: TextStyle,
diff --git a/wgui/src/widget/mod.rs b/wgui/src/widget/mod.rs
index d07a34eb..08f7aeda 100644
--- a/wgui/src/widget/mod.rs
+++ b/wgui/src/widget/mod.rs
@@ -17,6 +17,7 @@ use crate::{
stack::{ScissorStack, TransformStack},
};
+pub mod custom_draw;
pub mod div;
pub mod image;
pub mod label;
@@ -152,6 +153,7 @@ pub enum WidgetType {
Label,
Sprite,
Rectangle,
+ CustomDraw,
}
impl WidgetType {
@@ -161,6 +163,7 @@ impl WidgetType {
WidgetType::Label => "label",
WidgetType::Sprite => "sprite",
WidgetType::Rectangle => "rectangle",
+ WidgetType::CustomDraw => "custom_draw",
}
}
}
diff --git a/wgui/src/windowing/context_menu.rs b/wgui/src/windowing/context_menu.rs
index 6a139dfe..15982d7d 100644
--- a/wgui/src/windowing/context_menu.rs
+++ b/wgui/src/windowing/context_menu.rs
@@ -117,7 +117,7 @@ impl ContextMenu {
par.insert(Rc::from("tooltip_str"), tooltip.generate(&mut globals.i18n()));
}
- let mut data_cell = inner_parser.parse_template(&doc_params, "Cell", layout, id_buttons, par)?;
+ let mut data_cell = inner_parser.realize_template(&doc_params, "Cell", layout, id_buttons, par)?;
let button = data_cell.fetch_component_as::("button")?;
let button_id = button.base().get_id();
@@ -136,7 +136,7 @@ impl ContextMenu {
}
if idx < cells.len() - 1 {
- inner_parser.parse_template(&doc_params, "Separator", layout, id_buttons, Default::default())?;
+ inner_parser.realize_template(&doc_params, "Separator", layout, id_buttons, Default::default())?;
}
}
Ok(())
diff --git a/wlx-common/Cargo.toml b/wlx-common/Cargo.toml
index e9c54ae1..ee5c9d68 100644
--- a/wlx-common/Cargo.toml
+++ b/wlx-common/Cargo.toml
@@ -22,7 +22,7 @@ strum.workspace = true
xdg.workspace = true
chrono = "0.4.42"
-smol = "2.0.2"
+smol = { workspace = true }
wgui = { path = "../wgui/" }
rodio = { version = "0.21.1", default-features = false, features = [
"playback",
diff --git a/dash-frontend/src/util/various.rs b/wlx-common/src/async_executor.rs
similarity index 100%
rename from dash-frontend/src/util/various.rs
rename to wlx-common/src/async_executor.rs
diff --git a/wlx-common/src/dash_interface.rs b/wlx-common/src/dash_interface.rs
index 821ab1eb..7f80d557 100644
--- a/wlx-common/src/dash_interface.rs
+++ b/wlx-common/src/dash_interface.rs
@@ -7,6 +7,7 @@ use crate::{config::GeneralConfig, desktop_finder::DesktopFinder};
#[derive(Clone)]
pub struct MonadoClient {
+ pub id: i64,
pub name: String,
pub is_primary: bool,
pub is_active: bool,
@@ -16,6 +17,25 @@ pub struct MonadoClient {
pub is_io_active: bool,
}
+// see `wayvr/src/subsystem/monado_metrics/proto.rs` for documentation
+#[derive(Debug)]
+pub struct MonadoDumpSessionFrame {
+ pub session_id: i64,
+ pub frame_id: i64,
+ pub predicted_frame_time_ns: u64,
+ pub predicted_wake_up_time_ns: u64,
+ pub predicted_gpu_done_time_ns: u64,
+ pub predicted_display_time_ns: u64,
+ pub predicted_display_period_ns: u64,
+ pub display_time_ns: u64,
+ pub when_predicted_ns: u64,
+ pub when_wait_woke_ns: u64,
+ pub when_begin_ns: u64,
+ pub when_delivered_ns: u64,
+ pub when_gpu_done_ns: u64,
+ pub discarded: bool,
+}
+
#[derive(Clone, Copy)]
pub enum RecenterMode {
FixFloor,
@@ -36,10 +56,12 @@ pub trait DashInterface {
) -> anyhow::Result;
fn process_list(&mut self, data: &mut T) -> anyhow::Result>;
fn process_terminate(&mut self, data: &mut T, handle: WvrProcessHandle) -> anyhow::Result<()>;
- fn monado_client_list(&mut self, data: &mut T) -> anyhow::Result>;
+ fn monado_client_list(&mut self, data: &mut T, filtered: bool) -> anyhow::Result>;
fn monado_client_focus(&mut self, data: &mut T, name: &str) -> anyhow::Result<()>;
fn monado_brightness_get(&mut self, data: &mut T) -> Option;
fn monado_brightness_set(&mut self, data: &mut T, brightness: f32) -> Option<()>;
+ fn monado_metrics_set_enabled(&mut self, data: &mut T, enabled: bool) -> bool;
+ fn monado_metrics_dump_session_frames(&mut self, data: &mut T) -> Vec;
fn recenter_playspace(&mut self, data: &mut T, mode: RecenterMode) -> anyhow::Result<()>;
fn desktop_finder<'a>(&'a mut self, data: &'a mut T) -> &'a mut DesktopFinder;
fn general_config<'a>(&'a mut self, data: &'a mut T) -> &'a mut GeneralConfig;
diff --git a/wlx-common/src/dash_interface_emulated.rs b/wlx-common/src/dash_interface_emulated.rs
index 017e7bfb..3526de94 100644
--- a/wlx-common/src/dash_interface_emulated.rs
+++ b/wlx-common/src/dash_interface_emulated.rs
@@ -63,6 +63,8 @@ pub struct DashInterfaceEmulated {
general_config: GeneralConfig,
monado_clients: Vec,
brightness: f32,
+ debug_frame_id: u64,
+ debug_counter: u64,
}
impl DashInterfaceEmulated {
@@ -86,6 +88,7 @@ impl DashInterfaceEmulated {
let monado_clients = vec![
dash_interface::MonadoClient {
+ id: 1,
name: String::from("The Best VR Game 3000"),
is_active: true,
is_focused: true,
@@ -95,6 +98,7 @@ impl DashInterfaceEmulated {
is_visible: true,
},
dash_interface::MonadoClient {
+ id: 2,
name: String::from("Second app"),
is_active: true,
is_focused: false,
@@ -104,6 +108,7 @@ impl DashInterfaceEmulated {
is_visible: true,
},
dash_interface::MonadoClient {
+ id: 3,
name: String::from("Third app"),
is_active: true,
is_focused: false,
@@ -121,6 +126,8 @@ impl DashInterfaceEmulated {
general_config,
monado_clients,
brightness: 1.0,
+ debug_counter: 0,
+ debug_frame_id: 0,
}
}
}
@@ -229,7 +236,11 @@ impl DashInterface<()> for DashInterfaceEmulated {
fn toggle_dashboard(&mut self, _data: &mut ()) {}
- fn monado_client_list(&mut self, _data: &mut ()) -> anyhow::Result> {
+ fn monado_client_list(
+ &mut self,
+ _data: &mut (),
+ _filtered: bool,
+ ) -> anyhow::Result> {
Ok(self.monado_clients.clone())
}
@@ -256,4 +267,32 @@ impl DashInterface<()> for DashInterfaceEmulated {
self.brightness = brightness;
Some(())
}
+
+ fn monado_metrics_set_enabled(&mut self, _: &mut (), _enabled: bool) -> bool {
+ true
+ }
+
+ fn monado_metrics_dump_session_frames(&mut self, _: &mut ()) -> Vec {
+ // simulate """""typical""""" wired hmd timing graphs
+ self.debug_frame_id += 1;
+ self.debug_counter ^= self.debug_frame_id * 383;
+ self.debug_counter ^= 0xdeadbeefbaddcafe;
+ self.debug_counter >>= 1;
+ vec![dash_interface::MonadoDumpSessionFrame {
+ session_id: 123,
+ frame_id: self.debug_frame_id as _,
+ predicted_frame_time_ns: (((self.debug_frame_id) % 15) * 1000 * 1000) as _,
+ predicted_wake_up_time_ns: (((self.debug_frame_id * 2) % 16) * 1000 * 1000) as _,
+ predicted_gpu_done_time_ns: (((self.debug_frame_id * 3) % 17) * 1000 * 1000) as _,
+ predicted_display_time_ns: (((self.debug_frame_id * 4) % 14) * 1000 * 1000) as _,
+ predicted_display_period_ns: (((self.debug_frame_id * 5) % 13) * 1000 * 1000) as _,
+ display_time_ns: (((self.debug_frame_id * 7) % 12) * 1000 * 1000) as _,
+ when_predicted_ns: (((self.debug_frame_id * 53) % 11) * 1000 * 1000) as _,
+ when_wait_woke_ns: (((self.debug_frame_id * 23) % 10) * 1000 * 1000) as _,
+ when_begin_ns: (((self.debug_frame_id * 13) % 9) * 1000 * 1000) as _,
+ when_delivered_ns: (((self.debug_frame_id * 11) % 8) * 1000 * 1000) as _,
+ when_gpu_done_ns: (((self.debug_frame_id * 12) % 7) * 1000 * 1000) as _,
+ discarded: self.debug_frame_id.is_multiple_of(5),
+ }]
+ }
}
diff --git a/wlx-common/src/lib.rs b/wlx-common/src/lib.rs
index 698a2d29..c2c46177 100644
--- a/wlx-common/src/lib.rs
+++ b/wlx-common/src/lib.rs
@@ -1,4 +1,5 @@
pub mod astr_containers;
+pub mod async_executor;
pub mod audio;
pub mod cache_dir;
pub mod common;