diff --git a/lsfg-vk-common/include/lsfg-vk-common/configuration/config.hpp b/lsfg-vk-common/include/lsfg-vk-common/configuration/config.hpp index 7b5f333..93fe37b 100644 --- a/lsfg-vk-common/include/lsfg-vk-common/configuration/config.hpp +++ b/lsfg-vk-common/include/lsfg-vk-common/configuration/config.hpp @@ -25,19 +25,19 @@ namespace ls { /// game profile configuration struct GameConf { /// name of the profile - std::string name; + std::string name{"Profile"}; /// optional activation string/array std::vector active_in; /// gpu to use (in case of multiple) std::optional gpu; /// multiplier for frame generation - size_t multiplier; + size_t multiplier{2}; /// non-inverted flow scale - float flow_scale; + float flow_scale{1.00F}; /// use performance mode - bool performance_mode; + bool performance_mode{false}; /// pacing method - Pacing pacing; + Pacing pacing{Pacing::None}; }; /// automatically updating configuration diff --git a/lsfg-vk-ui/CMakeLists.txt b/lsfg-vk-ui/CMakeLists.txt index c566051..1e22b62 100644 --- a/lsfg-vk-ui/CMakeLists.txt +++ b/lsfg-vk-ui/CMakeLists.txt @@ -5,6 +5,7 @@ set(UI_SOURCES "src/main.cpp") set(UI_RESOURCES + "rsc/panes/CenteredDialog.qml" "rsc/panes/Group.qml" "rsc/panes/GroupEntry.qml" "rsc/panes/Pane.qml" diff --git a/lsfg-vk-ui/rsc/UI.qml b/lsfg-vk-ui/rsc/UI.qml index 529c905..221f0b9 100644 --- a/lsfg-vk-ui/rsc/UI.qml +++ b/lsfg-vk-ui/rsc/UI.qml @@ -13,48 +13,41 @@ ApplicationWindow { minimumHeight: 400 visible: true - Dialog { - id: dialog_name - title: "(...)" - standardButtons: Dialog.Ok | Dialog.Cancel + CenteredDialog { + id: create_dialog + name: "Create New Profile" + onConfirm: backend.createProfile(create_name.text) - modal: true - dim: true - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - - contentItem: ColumnLayout { - spacing: 8 - - TextField { - Layout.fillWidth: true - - id: nameField - placeholderText: "Choose a profile name" - selectByMouse: true - focus: true - } + TextField { + Layout.fillWidth: true + id: create_name + placeholderText: "Choose a profile name" + focus: true } } - Dialog { - id: dialog_confirm - title: "Confirm Deletion" - standardButtons: Dialog.Ok | Dialog.Cancel + CenteredDialog { + id: rename_dialog + name: "Rename Profile" + onConfirm: backend.renameProfile(rename_name.text) - modal: true - dim: true - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 + TextField { + Layout.fillWidth: true + id: rename_name + placeholderText: "Choose a profile name" + focus: true + } + } - contentItem: ColumnLayout { - spacing: 8 + CenteredDialog { + id: delete_dialog + name: "Confirm Deletion" + onConfirm: backend.deleteProfile() - Label { - Layout.fillWidth: true - text: "Are you sure you want to delete the selected profile?" - horizontalAlignment: Text.AlignHCenter - } + Label { + Layout.fillWidth: true + text: "Are you sure you want to delete the selected profile?" + horizontalAlignment: Text.AlignHCenter } } @@ -84,27 +77,24 @@ ApplicationWindow { Layout.fillWidth: true text: "Create New Profile" onClicked: { - dialog_name.title = "Create New Profile" - nameField.text = "" - - dialog_name.open() + create_name.text = "" + create_dialog.open() } } Button { Layout.fillWidth: true text: "Rename Profile" onClicked: { - dialog_name.title = "Rename Profile" - nameField.text = "(...)" - - dialog_name.open() + var idx = backend.profiles.index(backend.profile_index, 0); + rename_name.text = backend.profiles.data(idx); + rename_dialog.open() } } Button { Layout.fillWidth: true text: "Delete Profile" onClicked: { - dialog_confirm.open() + delete_dialog.open() } } } diff --git a/lsfg-vk-ui/rsc/panes/CenteredDialog.qml b/lsfg-vk-ui/rsc/panes/CenteredDialog.qml new file mode 100644 index 0000000..6ee06e6 --- /dev/null +++ b/lsfg-vk-ui/rsc/panes/CenteredDialog.qml @@ -0,0 +1,24 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Dialog { + property string name + default property alias content: inner.children + signal confirm() + + id: root + title: name + standardButtons: Dialog.Ok | Dialog.Cancel + onAccepted: root.confirm() + + modal: true + dim: true + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + contentItem: ColumnLayout { + id: inner + spacing: 8 + } +} diff --git a/lsfg-vk-ui/src/backend.cpp b/lsfg-vk-ui/src/backend.cpp index 711460a..5a3ed83 100644 --- a/lsfg-vk-ui/src/backend.cpp +++ b/lsfg-vk-ui/src/backend.cpp @@ -12,7 +12,7 @@ using namespace lsfgvk::ui; Backend::Backend() { // load configuration - ls::Configuration config{false}; + ls::Configuration config; config.reload(); this->m_global = config.getGlobalConf(); diff --git a/lsfg-vk-ui/src/backend.hpp b/lsfg-vk-ui/src/backend.hpp index 1bc324b..d8b4192 100644 --- a/lsfg-vk-ui/src/backend.hpp +++ b/lsfg-vk-ui/src/backend.hpp @@ -139,6 +139,40 @@ namespace lsfgvk::ui { MARK_DIRTY() } + Q_INVOKABLE void createProfile(const QString& name) { + ls::GameConf conf; + conf.name = name.toStdString(); + this->m_profiles.push_back(std::move(conf)); + + auto& model = this->m_profile_list_model; + model->insertRow(model->rowCount()); + model->setData(model->index(model->rowCount() - 1), name); + + this->m_profile_index = static_cast(this->m_profiles.size() - 1); + MARK_DIRTY() + } + Q_INVOKABLE void renameProfile(const QString& name) { + VALIDATE_AND_GET_PROFILE() + conf.name = name.toStdString(); + auto& model = this->m_profile_list_model; + model->setData(model->index(this->m_profile_index), name); + MARK_DIRTY() + } + Q_INVOKABLE void deleteProfile() { + if (!isValidProfileIndex()) + return; + + auto& profiles = this->m_profiles; + profiles.erase(profiles.begin() + this->m_profile_index); + auto& model = this->m_profile_list_model; + model->removeRow(this->m_profile_index); + if (!this->m_profiles.empty()) + this->m_profile_index = 0; + else + this->m_profile_index = -1; + MARK_DIRTY() + } + #undef VALIDATE_AND_GET_PROFILE #undef MARK_DIRTY diff --git a/ui/src/config/structs.rs b/ui/src/config/structs.rs deleted file mode 100644 index 0c74df0..0000000 --- a/ui/src/config/structs.rs +++ /dev/null @@ -1,95 +0,0 @@ -use serde::{Deserialize, Serialize}; - -// multiplier -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Multiplier(i64); -impl Default for Multiplier { - fn default() -> Self { Multiplier(2) } -} -impl From for Multiplier { - fn from(value: i64) -> Self { Multiplier(value) } -} -impl Into for Multiplier { - fn into(self) -> f64 { self.0 as f64 } -} - -// flow scale -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct FlowScale(f64); -impl Default for FlowScale { - fn default() -> Self { FlowScale(1.0) } -} -impl From for FlowScale { - fn from(value: f64) -> Self { FlowScale(value) } -} -impl Into for FlowScale { - fn into(self) -> f64 { self.0 } -} - -// present mode -#[derive(Debug, Clone, Deserialize, Serialize)] -pub enum PresentMode { - #[serde(rename = "fifo", alias = "vsync")] - Vsync, - #[serde(rename = "immediate")] - Immediate, - #[serde(rename = "mailbox")] - Mailbox, -} -impl Default for PresentMode { - fn default() -> Self { PresentMode::Vsync } -} -impl From for PresentMode { - fn from(value: i64) -> Self { - match value { - 0 => PresentMode::Vsync, - 1 => PresentMode::Mailbox, - 2 => PresentMode::Immediate, - _ => PresentMode::Vsync, - } - } -} -impl Into for PresentMode { - fn into(self) -> u32 { - match self { - PresentMode::Vsync => 0, - PresentMode::Mailbox => 1, - PresentMode::Immediate => 2, - } - } -} - -/// Global configuration for the application -#[derive(Debug, Default, Clone, Deserialize, Serialize)] -pub struct TomlGlobal { - pub dll: Option, - #[serde(default)] - pub no_fp16: bool -} - -/// Game-specific configuration -#[derive(Debug, Default, Clone, Deserialize, Serialize)] -pub struct TomlGame { - pub exe: String, - - #[serde(default)] - pub multiplier: Multiplier, - #[serde(default)] - pub flow_scale: FlowScale, - #[serde(default)] - pub performance_mode: bool, - #[serde(default)] - pub hdr_mode: bool, - #[serde(default)] - pub experimental_present_mode: PresentMode -} - -/// Main configuration structure -#[derive(Debug, Default, Clone, Deserialize, Serialize)] -pub struct TomlConfig { - pub version: i64, - #[serde(default)] - pub global: TomlGlobal, - #[serde(default)] - pub game: Vec -} diff --git a/ui/src/ui.rs b/ui/src/ui.rs deleted file mode 100644 index 7951abf..0000000 --- a/ui/src/ui.rs +++ /dev/null @@ -1,43 +0,0 @@ -use adw::{self, subclass::prelude::ObjectSubclassIsExt}; -use gtk::prelude::{WidgetExt, EditableExt, GtkWindowExt}; - -use crate::config; -use crate::wrapper; - -pub mod entry_handler; -pub mod main_handler; -pub mod sidebar_handler; - -pub fn build(app: &adw::Application) { - // create the main window - let window = wrapper::Window::new(app); - window.set_application(Some(app)); - let imp = window.imp(); - - // load profiles from configuration - let config = config::get_config().unwrap(); - for game in config.game.iter() { - let entry = wrapper::entry::Entry::new(); - entry.set_exe(game.exe.clone()); - entry_handler::add_entry(entry, imp.sidebar.imp().profiles.clone()); - } - - if let Some(dll_path) = config.global.dll { - imp.main.imp().dll.imp().entry.set_text(&dll_path); - } - imp.main.imp().no_fp16.imp().switch.set_active(config.global.no_fp16); - - // register handlers on sidebar pane. - sidebar_handler::register_signals(&imp.sidebar, imp.main.clone()); - - // register handlers on main pane. - main_handler::register_signals(imp.sidebar.clone(), &imp.main); - - // activate the first profile if available - if let Some(entry) = imp.sidebar.imp().profiles.row_at_index(0) { - entry.activate(); - } - - // present the window - window.present(); -} diff --git a/ui/src/ui/entry_handler.rs b/ui/src/ui/entry_handler.rs deleted file mode 100644 index 707fc86..0000000 --- a/ui/src/ui/entry_handler.rs +++ /dev/null @@ -1,54 +0,0 @@ -use adw::subclass::prelude::ObjectSubclassIsExt; -use gtk::{gio, glib::object::CastNone, prelude::{ButtonExt, ListBoxRowExt, WidgetExt}}; - -use crate::{config, wrapper::entry, STATE}; - -/// -/// Register signals for removing presets when adding a new entry. -/// -pub fn add_entry(entry_: entry::Entry, profiles_: gtk::ListBox) { - let entry = entry_.clone(); - let profiles = profiles_.clone(); - entry_.imp().delete.connect_clicked(move |btn| { - // prompt for confirmation - let dialog = gtk::AlertDialog::builder() - .message("Delete Profile") - .detail("Are you sure you want to delete this profile?") - .buttons(vec!["Cancel".to_string(), "Delete".to_string()]) - .cancel_button(0) - .default_button(1) - .modal(true) - .build(); - let window = btn.root() - .and_downcast::() - .expect("Button root is not a Window"); - - let profiles = profiles.clone(); - let entry = entry.clone(); - dialog.choose(Some(&window), gio::Cancellable::NONE, move |result| { - if result.is_err() || result.unwrap() != 1 { - return; - } - - // remove config entry - let _ = config::edit_config(|config| { - config.game.remove(entry.index() as usize); - }); - - // remove ui entry - profiles.remove(&entry); - - // select next entry - let state = STATE.get().unwrap().clone(); - if let Ok(mut state) = state.write() { - state.selected_game = None; - } - - if let Some(entry) = profiles.row_at_index(0) { - entry.activate(); - } - }); - }); - - profiles_.append(&entry_); -} diff --git a/ui/src/ui/main_handler.rs b/ui/src/ui/main_handler.rs deleted file mode 100644 index 6693bf6..0000000 --- a/ui/src/ui/main_handler.rs +++ /dev/null @@ -1,176 +0,0 @@ -use adw::subclass::prelude::ObjectSubclassIsExt; -use gtk::{gio::{self, prelude::FileExt}, glib::object::CastNone, prelude::{ButtonExt, EditableExt, GtkWindowExt, ListBoxRowExt, RangeExt, WidgetExt}}; - -use crate::{config, utils, wrapper::{entry, pane, popup}, STATE}; - -// update the currently selected game configuration -fn update_game(update: F) { - if let Ok(state) = STATE.get().unwrap().try_read() { - if let Some(selected_game) = state.selected_game { - let _ = config::edit_config(|config| { - update(&mut config.game[selected_game]) - }); - } - } -} - -/// -/// Register signals for preset preferences. -/// -pub fn register_signals(sidebar_: pane::PaneSidebar, main: &pane::PaneMain) { - let main = main.imp(); - let exe = main.profile_name.imp(); - let multiplier = main.multiplier.imp(); - let flow_scale = main.flow_scale.imp(); - let performance_mode = main.performance_mode.imp(); - let hdr_mode = main.hdr_mode.imp(); - let experimental_present_mode = main.experimental_present_mode.imp(); - - // preset opts - let sidebar = sidebar_.clone(); - exe.entry.connect_changed(move |entry| { - let mut exe = entry.text().to_string(); - if exe.trim().is_empty() { - exe = "new preset".to_string(); - } - - // rename list entry - let row_option = sidebar.imp().profiles.selected_row() - .and_downcast::(); - - if let Some(row) = row_option { - row.set_exe(exe.clone()); - } - - // update the game configuration - update_game(|conf| { - conf.exe = exe; - }); - }); - multiplier.number.connect_value_changed(|dropdown| { - update_game(|conf| { - conf.multiplier = (dropdown.value() as i64).into(); - }) - }); - flow_scale.slider.connect_value_changed(|slider| { - update_game(|conf| { - conf.flow_scale = (slider.value() / 100.0).into(); - }); - }); - performance_mode.switch.connect_state_notify(|switch| { - update_game(|conf| { - conf.performance_mode = switch.state(); - }); - }); - hdr_mode.switch.connect_state_notify(|switch| { - update_game(|conf| { - conf.hdr_mode = switch.state(); - }); - }); - experimental_present_mode.dropdown.connect_selected_notify(|dropdown| { - update_game(|conf| { - conf.experimental_present_mode = match dropdown.selected() { - 0 => config::PresentMode::Vsync, - 1 => config::PresentMode::Mailbox, - 2 => config::PresentMode::Immediate, - _ => config::PresentMode::Vsync, - }; - }); - }); - - // global opts - let dll = main.dll.imp(); - dll.entry.connect_changed(|entry| { - let _ = config::edit_config(|config| { - let mut text = entry.text().to_string(); - if text.trim().is_empty() { - config.global.dll = None; - } else { - if text.contains("~") { - let home = std::env::var("HOME").unwrap_or_else(|_| String::from("/")); - text = text.replace("~", &home); - } - config.global.dll = Some(text); - } - }); - }); - let no_fp16 = main.no_fp16.imp(); - no_fp16.switch.connect_state_notify(|switch| { - let _ = config::edit_config(|config| { - config.global.no_fp16 = switch.state(); - }); - }); - - // utility buttons - let entry = dll.entry.clone(); - dll.btn.connect_clicked(move |btn| { - let dialog = gtk::FileDialog::new(); - dialog.set_title("Select Lossless.dll"); - - let filter = gtk::FileFilter::new(); - filter.set_name(Some("Lossless.dll")); - filter.add_pattern("Lossless.dll"); - - let filters = gio::ListStore::new::(); - filters.append(&filter); - dialog.set_filters(Some(&filters)); - dialog.set_default_filter(Some(&filter)); - - let window = btn.root() - .and_downcast::() - .unwrap(); - let entry = entry.clone(); - dialog.open(Some(&window), gio::Cancellable::NONE, move |result| { - if result.is_err() || result.as_ref().unwrap().path().is_none() { - return; - } - - let path = result.unwrap().path().unwrap(); - let path_str = path.to_string_lossy().to_string(); - - entry.set_text(&path_str); - let _ = config::edit_config(|config| { - config.global.dll = Some(path_str); - }); - }); - }); - - let entry = exe.entry.clone(); - exe.btn.connect_clicked(move |btn| { - let window = btn.root() - .and_downcast::() - .unwrap() - .application() - .unwrap(); - let picker = popup::ProcessPicker::new(); - picker.set_application(Some(&window)); - - let list = picker.imp().processes.clone(); - let processes = utils::find_vulkan_processes().unwrap_or_default(); - for process in &processes { - let entry = popup::ProcessEntry::new(); - entry.set_exe(process.0.clone()); - list.append(&entry); - } - - let entry = entry.clone(); - let picker_ = picker.clone(); - picker.imp().processes.connect_row_activated(move |_, row| { - let comm_str = processes[row.index() as usize].1.clone(); - - entry.set_text(&comm_str); - update_game(|conf| { - conf.exe = comm_str; - }); - - picker_.close(); - }); - - let picker_ = picker.clone(); - picker.imp().close.connect_clicked(move |_| { - picker_.close(); - }); - - picker.present(); - }); -} diff --git a/ui/src/ui/sidebar_handler.rs b/ui/src/ui/sidebar_handler.rs deleted file mode 100644 index 7d18dc2..0000000 --- a/ui/src/ui/sidebar_handler.rs +++ /dev/null @@ -1,69 +0,0 @@ -use adw::subclass::prelude::ObjectSubclassIsExt; -use gtk::prelude::{ButtonExt, EditableExt, ListBoxRowExt, RangeExt, WidgetExt}; - -use crate::{config, ui::entry_handler, wrapper::{pane, entry}, STATE}; - -/// -/// Register signals for adding and selecting presets. -/// -pub fn register_signals(sidebar_: &pane::PaneSidebar, main: pane::PaneMain) { - // activate signal - let state = STATE.get().unwrap().clone(); - sidebar_.imp().profiles.connect_row_activated(move |_, entry| { - // find config entry by index - let index = entry.index() as usize; - let config = config::get_config(); - if config.is_err() { - return; - } - let config = config.unwrap(); - let conf = config.game[index].clone(); - - // update main pane - let main = main.imp(); - let exe = main.profile_name.imp(); - let multiplier = main.multiplier.imp(); - let flow_scale = main.flow_scale.imp(); - let performance_mode = main.performance_mode.imp(); - let hdr_mode = main.hdr_mode.imp(); - let experimental_present_mode = main.experimental_present_mode.imp(); - - // (lock state early, so the ui update doesn't override the config) - if let Ok(mut state) = state.write() { - exe.entry.set_text(&conf.exe); - multiplier.number.set_value(conf.multiplier.into()); - flow_scale.slider.set_value(Into::::into(conf.flow_scale) * 100.0); - performance_mode.switch.set_active(conf.performance_mode); - hdr_mode.switch.set_active(conf.hdr_mode); - experimental_present_mode.dropdown.set_selected(conf.experimental_present_mode.into()); - - // update state - state.selected_game = Some(index); - } - }); - - // create signal - let sidebar = sidebar_.clone(); - sidebar_.imp().create.connect_clicked(move |_| { - // ensure no config entry with the same name exist - let config = config::get_config().unwrap(); - if config.game.iter().any(|e| e.exe == "new profile") { - return; - } - - // create config entry - let mut conf_entry = config::TomlGame::default(); - conf_entry.exe = "new profile".to_string(); - let _ = config::edit_config(|config| { - config.game.push(conf_entry.clone()); - }); - - // add entry to sidebar - let entry = entry::Entry::new(); - entry.set_exe(conf_entry.exe); - entry_handler::add_entry(entry.clone(), sidebar.imp().profiles.clone()); - - // select the new entry - entry.activate(); - }); -} diff --git a/ui/src/utils.rs b/ui/src/utils.rs deleted file mode 100644 index eb0fe04..0000000 --- a/ui/src/utils.rs +++ /dev/null @@ -1,46 +0,0 @@ -use procfs::{process, ProcResult}; - -pub fn find_vulkan_processes() -> ProcResult> { - let mut processes = Vec::new(); - let apps = process::all_processes()?; - for app in apps { - let Ok(prc) = app else { continue; }; - - // ensure vulkan is loaded - let Ok(maps) = proc_maps::get_process_maps(prc.pid()) else { - continue; - }; - let result = maps.iter() - .filter_map(|map| map.filename()) - .map(|filename| filename.to_string_lossy().to_string()) - .any(|filename| filename.to_lowercase().contains("vulkan")); - if !result { - continue; - } - - // find executed binary - let mut exe = prc.exe()?.to_string_lossy().to_string(); - - // replace binary with exe for wine apps - if exe.contains("wine") || exe.contains("proton") { - let result = maps.iter() - .filter_map(|map| map.filename()) - .map(|filename| filename.to_string_lossy().to_string()) - .find(|filename| filename.ends_with(".exe")); - - if let Some(exe_name) = result { - exe = exe_name; - } - } - - // split off last part of the path - exe = exe.split('/').last().unwrap_or(&exe).to_string(); - - // format process information - let pid = prc.pid(); - let process_info = format!("PID {}: {}", pid, exe); - processes.push((process_info, exe)); - } - - Ok(processes) -} diff --git a/ui/src/wrapper/popup.rs b/ui/src/wrapper/popup.rs deleted file mode 100644 index 64b5bc1..0000000 --- a/ui/src/wrapper/popup.rs +++ /dev/null @@ -1,38 +0,0 @@ -use gtk::glib; -use gtk; -use adw; - -pub mod process; -pub mod process_entry; - -glib::wrapper! { - pub struct ProcessPicker(ObjectSubclass) - @extends - adw::ApplicationWindow, adw::Window, - gtk::ApplicationWindow, gtk::Window, gtk::Widget, - @implements - gtk::gio::ActionGroup, gtk::gio::ActionMap, - gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, - gtk::Native, gtk::Root, gtk::ShortcutManager; -} - -glib::wrapper! { - pub struct ProcessEntry(ObjectSubclass) - @extends - gtk::ListBoxRow, gtk::Widget, - @implements - gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget; -} - - -impl ProcessPicker { - pub fn new() -> Self { - glib::Object::new() - } -} - -impl ProcessEntry { - pub fn new() -> Self { - glib::Object::new() - } -}