use std::{collections::HashMap, marker::PhantomData, rc::Rc}; use wgui::{ assets::AssetPath, components::{ bar_graph::{ComponentBarGraph, ValueCell}, button::ComponentButton, checkbox::ComponentCheckbox, color_selector::{ColorSelectorChangedCallback, ComponentColorSelector}, slider::{ComponentSlider, SliderValueChangedCallback}, tabs::ComponentTabs, }, drawing::Color, globals::WguiGlobals, layout::{Layout, WidgetID}, parser::{self, Fetchable, ParseDocumentParams, ParserState, TemplateParams}, task::Tasks, }; use wlx_common::{ config::GeneralConfig, dash_interface::{self, ConfigChangeKind, MonadoDumpSessionFrame}, }; use crate::{ frontend::Frontend, tab::{Tab, TabType}, }; #[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, } } } #[derive(Clone)] enum Task { SetBrightness(f32), SetTab(TabNameEnum), // `ProcessList` tab ProcessListRefresh, ProcessListFocusClient(String), // `DebugTimings` tab DebugTimingsRefreshSessionList, DebugTimingsSetSessionId(i64), GeneralSettingsChromaUpdate, } struct SubtabProcessList { id_list_parent: WidgetID, state: ParserState, cells: Vec, } struct SubtabGeneralSettings { #[allow(dead_code)] state: ParserState, slider_keying_curve: Rc, slider_keying_despill: Rc, slider_keying_hue_range: Rc, slider_keying_saturation_range: Rc, slider_keying_value_range: Rc, cs_keying: Rc, } 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 { #[allow(dead_code)] state: ParserState, tasks: Tasks, marker: PhantomData, id_content: WidgetID, subtab: Subtab, ticks: u32, } impl Tab for TabMonado { fn get_type(&self) -> TabType { TabType::Games } fn update(&mut self, frontend: &mut Frontend, _time_ms: u32, data: &mut T) -> anyhow::Result<()> { for task in self.tasks.drain() { match task { 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::GeneralSettingsChromaUpdate => { if let Subtab::GeneralSettings(tab) = &mut self.subtab { tab.chroma_update(frontend.interface.general_config(data)); frontend .interface .config_changed(data, ConfigChangeKind::EnvironmentBlend); } } 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)?) } } } } } 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; Ok(()) } } fn doc_params_monado(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> { ParseDocumentParams { globals: globals.clone(), path: AssetPath::BuiltIn("gui/tab/monado.xml"), extra: Default::default(), } } 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", false => "no", } } const SLIDER_MULTIPLIER: f32 = 100.0; 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, )?; // get brightness let slider_brightness = state.fetch_component_as::("slider_brightness")?; if let Some(brightness) = frontend.interface.monado_brightness_get(data) { slider_brightness.set_value_primary(&mut frontend.layout.common(), brightness * 100.0); slider_brightness.on_value_changed({ let tasks = tasks.clone(); Box::new(move |_common, e| { tasks.push(Task::SetBrightness(e.value / 100.0)); }) }); } let config = frontend.interface.general_config(data); let cs_keying = state.fetch_component_as::("cs_keying")?; let slider_keying_despill = state.fetch_component_as::("slider_keying_despill")?; let slider_keying_curve = state.fetch_component_as::("slider_keying_curve")?; let slider_keying_hue_range = state.fetch_component_as::("slider_keying_hue_range")?; let slider_keying_saturation_range = state.fetch_component_as::("slider_keying_saturation_range")?; let slider_keying_value_range = state.fetch_component_as::("slider_keying_value_range")?; { let mut common = frontend.layout.common(); // set initial values let (rgb, range_h, range_s, range_v) = config.chroma_key_params.get_rgb_and_hsv_ranges(); slider_keying_curve.set_value_primary(&mut common, config.chroma_key_params.curve * SLIDER_MULTIPLIER); slider_keying_despill.set_value_primary(&mut common, config.chroma_key_params.despill * SLIDER_MULTIPLIER); slider_keying_hue_range.set_value_primary(&mut common, range_h * SLIDER_MULTIPLIER); slider_keying_saturation_range.set_value_primary(&mut common, range_s * SLIDER_MULTIPLIER); slider_keying_value_range.set_value_primary(&mut common, range_v * SLIDER_MULTIPLIER); cs_keying.set_color(&mut common, rgb); // prepare callbacks fn get_slider_callback(tasks: &Tasks) -> SliderValueChangedCallback { Box::new({ let tasks = tasks.clone(); move |_, _| { tasks.push(Task::GeneralSettingsChromaUpdate); } }) } fn get_color_selector_callback(tasks: &Tasks) -> ColorSelectorChangedCallback { Box::new({ let tasks = tasks.clone(); move |_, _| { tasks.push(Task::GeneralSettingsChromaUpdate); } }) } slider_keying_curve.on_value_changed(get_slider_callback(tasks)); slider_keying_despill.on_value_changed(get_slider_callback(tasks)); slider_keying_hue_range.on_value_changed(get_slider_callback(tasks)); slider_keying_saturation_range.on_value_changed(get_slider_callback(tasks)); slider_keying_value_range.on_value_changed(get_slider_callback(tasks)); cs_keying.on_changed(get_color_selector_callback(tasks)); } Ok(Self { state, slider_keying_curve, slider_keying_despill, slider_keying_hue_range, slider_keying_saturation_range, slider_keying_value_range, cs_keying, }) } fn chroma_update(&mut self, config: &mut GeneralConfig) { let val_curve = self.slider_keying_curve.get_value_primary(); let val_despill = self.slider_keying_despill.get_value_primary(); let val_range_h = self.slider_keying_hue_range.get_value_primary(); let val_range_s = self.slider_keying_saturation_range.get_value_primary(); let val_range_v = self.slider_keying_value_range.get_value_primary(); let val_rgb = self.cs_keying.get_color(); config.chroma_key_params.despill = val_despill / SLIDER_MULTIPLIER; config.chroma_key_params.curve = val_curve / SLIDER_MULTIPLIER; config.chroma_key_params.update_hsv_range_from_rgb( val_rgb, val_range_h / SLIDER_MULTIPLIER, val_range_s / SLIDER_MULTIPLIER, val_range_v / SLIDER_MULTIPLIER, ); } } 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 = TemplateParams::new(); params.insert_rc( "text", format!( "{} (ID {})", session.resolved_name.as_ref().map_or("Unknown", |s| s.as_str()), session_id, ) .into(), ); 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 = TemplateParams::new(); params.insert("name", name); params.insert_rc("limit_min", limits.0.to_string().into()); params.insert_rc("limit_max", limits.1.to_string().into()); 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, 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, 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, cells: Vec::new(), }) } fn mount_client( &mut self, layout: &mut Layout, client: &dash_interface::MonadoClient, tasks: &Tasks, ) -> anyhow::Result<()> { let mut par = TemplateParams::new(); par.insert("checked", if client.is_primary { "1" } else { "0" }); par.insert_rc("name", format!("{} (Client ID: {})", client.name, client.id).into()); par.insert("flag_active", yesno(client.is_active)); par.insert("flag_focused", yesno(client.is_focused)); par.insert("flag_io_active", yesno(client.is_io_active)); par.insert("flag_overlay", yesno(client.is_overlay)); par.insert("flag_primary", yesno(client.is_primary)); par.insert("flag_visible", yesno(client.is_visible)); let globals = layout.state.globals.clone(); let state_cell = self.state.realize_template( &doc_params_tab_process_list(&globals), "Cell", layout, self.id_list_parent, par, )?; let checkbox = state_cell.fetch_component_as::("checkbox")?; checkbox.on_toggle({ let tasks = tasks.clone(); let client_name = client.name.clone(); Box::new(move |_common, e| { if e.checked { tasks.push(Task::ProcessListFocusClient(client_name.clone())); } Ok(()) }) }); self.cells.push(state_cell); Ok(()) } 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, true)?; frontend.layout.remove_children(self.id_list_parent); self.cells.clear(); for client in clients { self.mount_client(&mut frontend.layout, &client, tasks)?; } 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) { frontend.interface.monado_brightness_set(data, brightness); } }