Merge pull request #445 from wayvr-org/feat-monado-timings

feat: Monado debug timings graph
This commit is contained in:
oo8dev 2026-04-02 17:57:46 +02:00 committed by GitHub
commit 543f157895
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 1942 additions and 197 deletions

80
Cargo.lock generated
View File

@ -2855,8 +2855,7 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libmonado"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0df3183760954d877b894d42f5c6954837d8a7d7bd5f042409c4175afeaf24ea"
source = "git+https://github.com/oo8dev/libmonado-rs.git?rev=fc39940a64dea2df080a0d2c974c7d651006241f#fc39940a64dea2df080a0d2c974c7d651006241f"
dependencies = [
"dlopen2",
"flagset",
@ -3108,6 +3107,12 @@ dependencies = [
"pxfm",
]
[[package]]
name = "multimap"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
[[package]]
name = "native-tls"
version = "0.2.14"
@ -3905,6 +3910,17 @@ dependencies = [
"sha2",
]
[[package]]
name = "petgraph"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
"indexmap 2.12.1",
]
[[package]]
name = "phf"
version = "0.12.1"
@ -4167,6 +4183,64 @@ dependencies = [
"syn 2.0.113",
]
[[package]]
name = "prost"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
dependencies = [
"heck 0.5.0",
"itertools 0.14.0",
"log",
"multimap",
"petgraph",
"prettyplease",
"prost",
"prost-types",
"regex",
"syn 2.0.113",
"tempfile",
]
[[package]]
name = "prost-derive"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
dependencies = [
"anyhow",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.113",
]
[[package]]
name = "prost-types"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7"
dependencies = [
"prost",
]
[[package]]
name = "prost_build"
version = "0.1.0"
dependencies = [
"prost-build",
]
[[package]]
name = "pure-rust-locales"
version = "0.8.2"
@ -6293,6 +6367,7 @@ dependencies = [
"mint",
"openxr",
"ovr_overlay",
"prost",
"pure-rust-locales",
"regex",
"rosc",
@ -6305,6 +6380,7 @@ dependencies = [
"slotmap",
"smallvec",
"smithay",
"smol",
"strum",
"sysinfo",
"thiserror 2.0.17",

View File

@ -30,6 +30,7 @@ members = [
"dash-frontend",
"wayvr-ipc",
"wayvrctl",
"scripts/prost_build",
]
resolver = "3"
@ -38,6 +39,7 @@ vulkano = { git = "https://github.com/galister/vulkano.git", rev = "cf7f92867928
[workspace.dependencies]
anyhow = "1.0.100"
smol = "2.0.2"
glam = { version = "0.30.9", features = ["mint", "serde"] }
clap = { version = "4.5.53", features = ["derive"] }
xdg = "3.0.0"

View File

@ -23,12 +23,12 @@ strum.workspace = true
chrono = "0.4.42"
keyvalues-parser = { git = "https://github.com/CosmicHorrorDev/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" }
smol = "2.0.2"
smol = { workspace = true }
hyper = { version = "1.8.1", features = ["client", "http1", "http2"] }
http-body-util = "0.1.3"
async-native-tls = "0.5.0"
smol-hyper = "0.1.1"
[features]
default = ["monado" ]
default = ["monado"]
monado = []

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE -->
<path fill="currentColor" d="M12 19q1.65 0 2.825-1.175T16 15v-4q0-1.65-1.175-2.825T12 7T9.175 8.175T8 11v4q0 1.65 1.175 2.825T12 19m-2-3h4v-2h-4zm0-4h4v-2h-4zm2 9q-1.625 0-3.012-.8T6.8 18H4v-2h2.1q-.075-.5-.088-1T6 14H4v-2h2q0-.5.012-1t.088-1H4V8h2.8q.35-.575.788-1.075T8.6 6.05L7 4.4L8.4 3l2.15 2.15q.7-.225 1.425-.225t1.425.225L15.6 3L17 4.4l-1.65 1.65q.575.375 1.038.862T17.2 8H20v2h-2.1q.075.5.088 1T18 12h2v2h-2q0 .5-.013 1t-.087 1H20v2h-2.8q-.8 1.4-2.187 2.2T12 21" />
</svg>

After

Width:  |  Height:  |  Size: 682 B

View File

@ -11,10 +11,10 @@
gap="8"
round="8" />
<!-- src, text, translation -->
<!-- src_builtin, text, translation -->
<template name="GroupBoxTitle">
<div flex_direction="row" align_items="center" gap="8">
<sprite src="${src}" src_builtin="${src_builtin}" width="24" height="24" />
<sprite src_builtin="${src_builtin}" width="24" height="24" />
<label text="${text}" translation="${translation}" weight="bold" size="18" />
</div>
<rectangle color="#FFFFFF44" width="100%" height="2" />

View File

@ -1,37 +1,14 @@
<layout>
<include src="../t_group_box.xml" />
<!-- key: str, value: str -->
<template name="BoolFlag">
<div flex_direction="row" gap="4">
<label text="${key}" />
<label weight="bold" text="${value}" />
</div>
</template>
<!-- name, checked, flag_* -->
<template name="Cell">
<rectangle macro="group_box">
<CheckBox id="checkbox" text="${name}" checked="${checked}" />
<div flex_direction="row" gap="8">
<BoolFlag key="Active:" value="${flag_active}" />
<BoolFlag key="Focused:" value="${flag_focused}" />
<BoolFlag key="IO active:" value="${flag_io_active}" />
<BoolFlag key="Overlay:" value="${flag_overlay}" />
<BoolFlag key="Primary:" value="${flag_primary}" />
<BoolFlag key="Visible:" value="${flag_visible}" />
</div>
</rectangle>
</template>
<elements>
<label translation="DISPLAY_BRIGHTNESS" />
<Slider id="slider_brightness" width="300" height="24" min_value="0" max_value="140" />
<label translation="PROCESS_LIST" />
<div id="list_parent" flex_direction="column" gap="8">
<!-- filled at runtime -->
<div gap="4">
<Tabs id="tabs">
<Tab name="general_settings" translation="GENERAL_SETTINGS" sprite_src_builtin="dashboard/settings.svg" />
<Tab name="process_list" translation="PROCESS_LIST" sprite_src_builtin="dashboard/cpu.svg" />
<Tab name="debug_timings" translation="DEBUG_INFO" sprite_src_builtin="dashboard/not_a_bug.svg" />
</Tabs>
<div gap="4" id="content" width="100%">
<!-- filled-in at runtime -->
</div>
</div>
</elements>
</layout>

View File

@ -0,0 +1,27 @@
<layout>
<include src="../t_group_box.xml" />
<!-- name, limit_min, limit_max -->
<template name="DebugGraph">
<rectangle flex_direction="column" align_items="center" gap="8" border="2" color="#ffffff11" color2="#ffffff22" gradient="vertical" border_color="#ffffff33" round="4" padding="4">
<label text="${name}" size="9" weight="bold" />
<BarGraph id="graph" width="180" height="70" unit="ms" limit_min="${limit_min}" limit_max="${limit_max}" capacity="50" />
</rectangle>
</template>
<!-- text -->
<template name="SessionButton">
<Button id="button" text="${text}" />
</template>
<elements>
<rectangle macro="group_box">
<GroupBoxTitle src_builtin="dashboard/apps.svg" text="Sessions" />
<div id="session_list_parent" flex_wrap="wrap" flex_direction="row" gap="8" />
<GroupBoxTitle src_builtin="dashboard/not_a_bug.svg" text="Debug timings" />
<div id="timings_parent" flex_wrap="wrap" flex_direction="row" gap="8">
<label text="Please select which session you want to monitor" />
</div>
</rectangle>
</elements>
</layout>

View File

@ -0,0 +1,12 @@
<layout>
<include src="../t_group_box.xml" />
<elements>
<rectangle macro="group_box">
<GroupBoxTitle src_builtin="dashboard/settings.svg" translation="GENERAL_SETTINGS" />
<label translation="DISPLAY_BRIGHTNESS" />
<Slider id="slider_brightness" width="300" height="24" min_value="0" max_value="140" />
</rectangle>
</elements>
</layout>

View File

@ -0,0 +1,35 @@
<layout>
<include src="../t_group_box.xml" />
<!-- key: str, value: str -->
<template name="BoolFlag">
<div flex_direction="row" gap="4">
<label text="${key}" />
<label weight="bold" text="${value}" />
</div>
</template>
<!-- name, checked, flag_* -->
<template name="Cell">
<rectangle macro="group_box">
<CheckBox id="checkbox" text="${name}" checked="${checked}" />
<div flex_direction="row" gap="8" flex_wrap="wrap">
<BoolFlag key="Active:" value="${flag_active}" />
<BoolFlag key="Focused:" value="${flag_focused}" />
<BoolFlag key="IO active:" value="${flag_io_active}" />
<BoolFlag key="Overlay:" value="${flag_overlay}" />
<BoolFlag key="Primary:" value="${flag_primary}" />
<BoolFlag key="Visible:" value="${flag_visible}" />
</div>
</rectangle>
</template>
<elements>
<rectangle macro="group_box">
<GroupBoxTitle src_builtin="dashboard/cpu.svg" translation="PROCESS_LIST" />
<div id="list_parent" flex_direction="column" gap="8">
<!-- filled at runtime -->
</div>
</rectangle>
</elements>
</layout>

View File

@ -54,7 +54,9 @@
<Tab name="autostart_apps" translation="APP_SETTINGS.AUTOSTART_APPS" sprite_src_builtin="dashboard/apps.svg" />
<Tab name="troubleshooting" translation="APP_SETTINGS.TROUBLESHOOTING" sprite_src_builtin="dashboard/cpu.svg" />
</Tabs>
<div flex_wrap="wrap" justify_content="stretch" gap="4" id="settings_root" width="100%" />
<div flex_wrap="wrap" justify_content="stretch" gap="4" id="settings_root" width="100%">
<!-- filled-in at runtime -->
</div>
</div>
</elements>
</layout>

View File

@ -1,16 +1,16 @@
<layout>
<include src="../t_group_box.xml" />
<include src_builtin="../t_group_box.xml" />
<!-- device_name, device_icon -->
<template name="DeviceSlider">
<rectangle macro="group_box">
<div width="100%" align_items="center" justify_content="center" gap="8">
<sprite src="${device_icon}" width="16" height="16" />
<sprite src_builtin="${device_icon}" width="16" height="16" />
<label text="${device_name}" margin_right="8" size="12" weight="bold" />
</div>
<div width="100%" align_items="center">
<CheckBox id="checkbox" />
<Button sprite_src="${volume_icon}" id="btn_mute" width="32" />
<Button sprite_src_builtin="${volume_icon}" id="btn_mute" width="32" />
<Slider id="slider" flex_grow="1" height="16" min_value="0" max_value="150" margin_left="8" />
</div>
</rectangle>
@ -41,7 +41,7 @@
flex_grow="1"
id="${id}"
translation="${translation}"
sprite_src="${src}">
sprite_src_builtin="${src}">
</Button>
</template>
@ -67,4 +67,4 @@
</div>
</div>
</elements>
</layout>
</layout>

View File

@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "Stopp",
"FORCE_KILL": "Erzwinge Beenden"
}
},
"DEBUG_INFO": "Debug-Informationen"
}

View File

@ -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",

View File

@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "Detener",
"FORCE_KILL": "Forzar cierre"
}
},
"DEBUG_INFO": "Información de depuración"
}

View File

@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "Interrompi",
"FORCE_KILL": "Uccidi forzatamente"
}
},
"DEBUG_INFO": "Informazioni di debug"
}

View File

@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "停止",
"FORCE_KILL": "強制終了"
}
},
"DEBUG_INFO": "デバッグ情報"
}

View File

@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "Zatrzymaj",
"FORCE_KILL": "Wymuś zakończenie"
}
},
"DEBUG_INFO": "Informacje debugowania"
}

View File

@ -161,5 +161,6 @@
"PROCESS": {
"STOP": "停止",
"FORCE_KILL": "强制关闭"
}
},
"DEBUG_INFO": "调试信息"
}

View File

@ -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<FrontendTask>;
pub struct Frontend<T> {
pub layout: Layout,
globals: WguiGlobals,
pub globals: WguiGlobals,
pub interface: BoxDashInterface<T>,

View File

@ -286,7 +286,7 @@ impl AppList {
let mut params = HashMap::<Rc<str>, Rc<str>>::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,

View File

@ -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<Self> {
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<parser::ParserData>,
}
struct SubtabGeneralSettings {
#[allow(dead_code)]
state: ParserState,
}
struct DebugGraph {
graph: Rc<ComponentBarGraph>,
}
struct DebugSessionList {
#[allow(dead_code)]
buttons: Vec<Rc<ComponentButton>>,
}
struct TimingsSession {
resolved_name: Option<String>,
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<i64 /* session id */, TimingsSession>;
struct SubtabDebugTimings {
#[allow(dead_code)]
state: ParserState,
graphs: Option<Graphs>,
session_list: DebugSessionList,
selected_session_id: Option<i64>,
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<T> {
@ -29,10 +124,8 @@ pub struct TabMonado<T> {
marker: PhantomData<T>,
globals: WguiGlobals,
id_list_parent: WidgetID,
cells: Vec<parser::ParserData>,
id_content: WidgetID,
subtab: Subtab,
ticks: u32,
}
@ -45,15 +138,64 @@ impl<T> Tab<T> for TabMonado<T> {
fn update(&mut self, frontend: &mut Frontend<T>, _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<T> Tab<T> for TabMonado<T> {
}
}
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<T> TabMonado<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
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<T>(
parent_id: WidgetID,
frontend: &mut Frontend<T>,
data: &mut T,
tasks: &Tasks<Task>,
) -> anyhow::Result<Self> {
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::<ComponentSlider>("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::<Task>::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<Task>,
id_parent: WidgetID,
sessions: &SessionsMap,
) -> anyhow::Result<DebugSessionList> {
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::<ComponentButton>("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<DebugGraph> {
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::<ComponentBarGraph>("graph")?;
Ok(DebugGraph { graph })
}
fn ns_to_ms(ns: i64) -> f32 {
(ns / 1000) as f32 / 1000.0
}
impl SubtabDebugTimings {
fn new<T>(parent_id: WidgetID, frontend: &mut Frontend<T>, tasks: &Tasks<Task>) -> anyhow::Result<Self> {
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<DebugGraph> {
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<Task>) -> anyhow::Result<()> {
self.session_list = mount_sessions_list(
&mut self.state,
layout,
tasks,
globals,
self.id_sessions_list_parent,
&self.sessions,
)?;
Ok(())
}
fn update<T>(&mut self, tasks: &Tasks<Task>, data: &mut T, frontend: &mut Frontend<T>) -> 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<T>(parent_id: WidgetID, frontend: &mut Frontend<T>) -> anyhow::Result<Self> {
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<T>, client: &dash_interface::MonadoClient) -> anyhow::Result<()> {
fn mount_client(
&mut self,
layout: &mut Layout,
client: &dash_interface::MonadoClient,
tasks: &Tasks<Task>,
) -> anyhow::Result<()> {
let mut par = HashMap::<Rc<str>, Rc<str>>::new();
par.insert(
"checked".into(),
@ -109,7 +566,10 @@ impl<T> TabMonado<T> {
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<T> TabMonado<T> {
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::<ComponentCheckbox>("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<T> TabMonado<T> {
Ok(())
}
fn refresh(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
fn focus_client<T>(
&mut self,
frontend: &mut Frontend<T>,
data: &mut T,
name: String,
tasks: &Tasks<Task>,
) -> anyhow::Result<()> {
frontend.interface.monado_client_focus(data, &name)?;
tasks.push(Task::ProcessListRefresh);
Ok(())
}
fn refresh<T>(&mut self, frontend: &mut Frontend<T>, data: &mut T, tasks: &Tasks<Task>) -> 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::<ComponentSlider>("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<T>, data: &mut T, name: String) -> anyhow::Result<()> {
frontend.interface.monado_client_focus(data, &name)?;
self.tasks.push(Task::Refresh);
Ok(())
impl<T> TabMonado<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
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::<ComponentTabs>("tabs")?;
let tasks = Tasks::<Task>::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<T>, data: &mut T, brightness: f32) {

View File

@ -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)

View File

@ -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<u8>,
}

View File

@ -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;

View File

@ -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::<ComponentButton>("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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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},
};

View File

@ -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,

View File

@ -0,0 +1,7 @@
[package]
name = "prost_build"
version = "0.1.0"
edition = "2024"
[dependencies]
prost-build = "0.14.3"

View File

@ -0,0 +1,5 @@
use std::io::Result;
fn main() -> Result<()> {
prost_build::compile_protos(&["src/monado_metrics.proto"], &["src/"])?;
Ok(())
}

View File

@ -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;
};
}

View File

@ -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 = []

View File

@ -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;

View File

@ -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::<u32>::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 {

View File

@ -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);

View File

@ -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<MonadoMetricsFd>,
}
impl MonadoState {
pub fn new() -> anyhow::Result<Self> {
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(())
}
}

View File

@ -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<OpenXrOverlayData>,
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() {

View File

@ -470,20 +470,27 @@ impl DashInterface<AppState> for DashInterfaceLive {
fn monado_client_list(
&mut self,
app: &mut AppState,
filtered: bool,
) -> anyhow::Result<Vec<dash_interface::MonadoClient>> {
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::<dash_interface::MonadoClient>::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<AppState> 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<f32> {
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<dash_interface::MonadoDumpSessionFrame> {
#[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<Vec<dash_interface::MonadoClient>> {
anyhow::bail!("Not supported in this build.")
}
@ -543,6 +632,20 @@ impl DashInterface<AppState> 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<dash_interface::MonadoDumpSessionFrame> {
Vec::new() // not supported in this build
}
}
#[cfg(feature = "openxr")]

View File

@ -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<WGfx>,
pub gfx_extras: WGfxExtras,
@ -67,7 +72,7 @@ pub struct AppState {
pub wvr_server: Option<WvrServerState>,
#[cfg(feature = "openxr")]
pub monado: Option<libmonado::Monado>,
pub monado_state: Option<backend::openxr::monado_state::MonadoState>,
}
#[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:?}");
}
}
}
}

View File

@ -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;

View File

@ -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.

View File

@ -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<proto::Record>,
}
const RECORD_QUEUE_SIZE: usize = 500;
impl MonadoMetricsFd {
pub fn new(monado: &mut libmonado::Monado) -> anyhow::Result<Self> {
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<proto::Record> {
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<proto::Record, _> = prost::Message::decode_length_delimited(&buf[..]);
match res {
Ok(record) => {
self.parse_message(record);
}
Err(e) => {
log::error!("decode error: {e}");
}
}
}
}
}

View File

@ -0,0 +1,2 @@
pub mod metrics_fd;
pub mod proto;

View File

@ -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<record::Record>,
}
/// 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),
}
}

View File

@ -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
### `<BarGraph>`
### 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

View File

@ -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<ValueCell>,
}
#[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<Data>,
state: Rc<RefCell<State>>,
}
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::<WidgetLabel>(self.data.id_label_val_min)?;
let mut label_val_max = c.state.widgets.get_as::<WidgetLabel>(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<ComponentBarGraph>)> {
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))
}

View File

@ -7,6 +7,7 @@ use crate::{
layout::WidgetID,
};
pub mod bar_graph;
pub mod button;
pub mod checkbox;
pub mod editbox;

View File

@ -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),

View File

@ -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<U1: 'static, U2: 'static>(
&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!(

View File

@ -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<WidgetID> {
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)
}

View File

@ -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::<T>()
.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 <template> 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<str>, Rc<str>>,
) -> anyhow::Result<ParserData> {
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 <template> 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<str>, Rc<str>>,
) -> anyhow::Result<ParserData> {
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 <template> 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<str>, Rc<str>>,
) -> 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)?);
}

View File

@ -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));

View File

@ -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<drawing::RenderPrimitive>,
}
pub struct WidgetCustomDrawParams {
pub func: Box<dyn Fn(CustomDrawArgs)>,
}
// 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()
}
}

View File

@ -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,

View File

@ -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",
}
}
}

View File

@ -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::<ComponentButton>("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(())

View File

@ -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",

View File

@ -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<T> {
) -> anyhow::Result<WvrProcessHandle>;
fn process_list(&mut self, data: &mut T) -> anyhow::Result<Vec<WvrProcess>>;
fn process_terminate(&mut self, data: &mut T, handle: WvrProcessHandle) -> anyhow::Result<()>;
fn monado_client_list(&mut self, data: &mut T) -> anyhow::Result<Vec<MonadoClient>>;
fn monado_client_list(&mut self, data: &mut T, filtered: bool) -> anyhow::Result<Vec<MonadoClient>>;
fn monado_client_focus(&mut self, data: &mut T, name: &str) -> anyhow::Result<()>;
fn monado_brightness_get(&mut self, data: &mut T) -> Option<f32>;
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<MonadoDumpSessionFrame>;
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;

View File

@ -63,6 +63,8 @@ pub struct DashInterfaceEmulated {
general_config: GeneralConfig,
monado_clients: Vec<dash_interface::MonadoClient>,
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<Vec<dash_interface::MonadoClient>> {
fn monado_client_list(
&mut self,
_data: &mut (),
_filtered: bool,
) -> anyhow::Result<Vec<dash_interface::MonadoClient>> {
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<dash_interface::MonadoDumpSessionFrame> {
// 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),
}]
}
}

View File

@ -1,4 +1,5 @@
pub mod astr_containers;
pub mod async_executor;
pub mod audio;
pub mod cache_dir;
pub mod common;