feat: wivrn config editor

This commit is contained in:
GabMus 2023-12-27 09:03:52 +00:00
parent 1ace5564de
commit afd51184ef
6 changed files with 372 additions and 105 deletions

View file

@ -9,6 +9,7 @@ use super::job_worker::JobWorker;
use super::libsurvive_setup_window::LibsurviveSetupWindow;
use super::main_view::MainViewMsg;
use super::util::open_with_default_handler;
use super::wivrn_conf_editor::{WivrnConfEditor, WivrnConfEditorInit, WivrnConfEditorMsg};
use crate::builders::build_basalt::get_build_basalt_jobs;
use crate::builders::build_libsurvive::get_build_libsurvive_jobs;
use crate::builders::build_mercury::get_build_mercury_job;
@ -102,6 +103,9 @@ pub struct App {
fbt_config_editor: Option<Controller<FbtConfigEditor>>,
#[tracker::do_not_track]
libmonado: Option<libmonado_rs::Monado>,
#[tracker::do_not_track]
wivrn_conf_editor: Option<Controller<WivrnConfEditor>>,
}
#[derive(Debug)]
@ -128,6 +132,7 @@ pub enum Msg {
ConfigFbt,
DebugOpenPrefix,
DebugOpenData,
OpenWivrnConfig,
}
impl App {
@ -632,6 +637,15 @@ impl SimpleComponent for App {
self.get_selected_profile().prefix
));
}
Msg::OpenWivrnConfig => {
let editor = WivrnConfEditor::builder()
.launch(WivrnConfEditorInit {
root_win: self.app_win.clone().upcast::<gtk::Window>(),
})
.detach();
editor.emit(WivrnConfEditorMsg::Present);
self.wivrn_conf_editor = Some(editor);
}
}
}
@ -669,6 +683,7 @@ impl SimpleComponent for App {
}
let mut model = App {
tracker: 0,
application: init.application,
app_win: root.clone(),
inhibit_id: None,
@ -687,6 +702,7 @@ impl SimpleComponent for App {
MainViewOutMsg::DeleteProfile => Msg::DeleteProfile,
MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p),
MainViewOutMsg::OpenLibsurviveSetup => Msg::OpenLibsurviveSetup,
MainViewOutMsg::OpenWivrnConfig => Msg::OpenWivrnConfig,
}),
debug_view: DebugView::builder().launch(DebugViewInit {}).forward(
sender.input_sender(),
@ -712,7 +728,6 @@ impl SimpleComponent for App {
setcap_confirm_dialog,
enable_debug_view: config.debug_view_enabled,
config,
tracker: 0,
profiles,
xrservice_worker: None,
build_worker: None,
@ -720,6 +735,7 @@ impl SimpleComponent for App {
fbt_config_editor: None,
restart_xrservice: false,
libmonado: None,
wivrn_conf_editor: None,
};
let widgets = view_output!();

View file

@ -1,3 +1,4 @@
pub mod device_row_factory;
pub mod env_var_row_factory;
pub mod tracker_role_group_factory;
pub mod wivrn_encoder_group_factory;

View file

@ -0,0 +1,196 @@
use crate::{
file_builders::wivrn_config::{Codec, Encoder, WivrnConfEncoder},
ui::{
preference_rows::{combo_row, number_entry_row, spin_row},
wivrn_conf_editor::WivrnConfEditorMsg,
},
};
use relm4::{
adw::prelude::*, factory::AsyncFactoryComponent, gtk::prelude::*, prelude::*,
AsyncFactorySender,
};
use uuid::Uuid;
#[derive(Debug)]
pub struct WivrnEncoderModel {
pub encoder_conf: WivrnConfEncoder,
pub uid: String,
}
#[derive(Debug)]
pub enum WivrnEncoderModelMsg {
EncoderChanged(u32),
CodecChanged(u32),
BitrateChanged(Option<u32>),
WidthChanged(Option<f32>),
HeightChanged(Option<f32>),
GroupChanged(Option<i32>),
Delete,
}
#[derive(Debug)]
pub enum WivrnEncoderModelOutMsg {
Delete(String),
}
#[derive(Debug, Default)]
pub struct WivrnEncoderModelInit {
pub encoder_conf: Option<WivrnConfEncoder>,
}
#[relm4::factory(async pub)]
impl AsyncFactoryComponent for WivrnEncoderModel {
type Init = WivrnEncoderModelInit;
type Input = WivrnEncoderModelMsg;
type Output = WivrnEncoderModelOutMsg;
type CommandOutput = ();
type ParentInput = WivrnConfEditorMsg;
type ParentWidget = adw::PreferencesPage;
view! {
root = adw::PreferencesGroup {
set_title: "Encoder",
#[wrap(Some)]
set_header_suffix: delete_btn = &gtk::Button {
set_label: "Delete",
add_css_class: "destructive-action",
connect_clicked[sender] => move |_| {
sender.input(Self::Input::Delete)
},
},
add: encoder_combo = &combo_row(
"Encoder",
Some("x264: CPU based h264 encoding\nNVEnc: Nvidia GPU encoding\nVAAPI: Intel or AMD GPU encoding"),
&self.encoder_conf.encoder.to_string(),
Encoder::iter().map(Encoder::to_string).collect::<Vec<String>>(),
{
let sender = sender.clone();
move |row| {
sender.input(Self::Input::EncoderChanged(row.selected()));
}
}
) -> adw::ComboRow,
add: codec_combo = &combo_row(
"Codec",
None,
&self.encoder_conf.codec.to_string(),
Codec::iter().map(Codec::to_string).collect::<Vec<String>>(),
{
let sender = sender.clone();
move |row| {
sender.input(Self::Input::CodecChanged(row.selected()));
}
}
) -> adw::ComboRow,
add: bitrate_row = &number_entry_row(
"Bitrate",
&self.encoder_conf.bitrate
.and_then(|n| Some(n.to_string()))
.unwrap_or_default(),
false,
{
let sender = sender.clone();
move |row| {
let txt = row.text();
sender.input(Self::Input::BitrateChanged(
if txt.is_empty() {
None
} else {
Some(txt.parse::<u32>().unwrap())
}
));
}
}
) -> adw::EntryRow,
add: width_row = &spin_row(
"Width",
None,
self.encoder_conf.width.unwrap_or(1.0).into(),
0.0,
1.0,
0.01,
{
let sender = sender.clone();
move |adj| {
sender.input(Self::Input::WidthChanged(
Some(adj.value() as f32)
));
}
}
) -> adw::SpinRow,
add: height_row = &spin_row(
"Height",
None,
self.encoder_conf.height.unwrap_or(1.0).into(),
0.0,
1.0,
0.01,
{
let sender = sender.clone();
move |adj| {
sender.input(Self::Input::HeightChanged(
Some(adj.value() as f32)
));
}
}
) -> adw::SpinRow,
add: group_row = &spin_row(
"Group",
None,
self.encoder_conf.group.unwrap_or(0).into(),
0.0,
99999.0,
1.0,
{
let sender = sender.clone();
move |adj| {
sender.input(Self::Input::GroupChanged(
Some(adj.value().trunc() as i32)
));
}
}
) -> adw::SpinRow,
}
}
async fn update(&mut self, message: Self::Input, sender: AsyncFactorySender<Self>) {
match message {
Self::Input::EncoderChanged(idx) => {
self.encoder_conf.encoder = Encoder::as_vec().get(idx as usize).unwrap().clone();
}
Self::Input::CodecChanged(idx) => {
self.encoder_conf.codec = Codec::as_vec().get(idx as usize).unwrap().clone();
}
Self::Input::BitrateChanged(val) => {
self.encoder_conf.bitrate = val;
}
Self::Input::WidthChanged(val) => {
self.encoder_conf.width = val;
}
Self::Input::HeightChanged(val) => {
self.encoder_conf.height = val;
}
Self::Input::GroupChanged(val) => {
self.encoder_conf.group = val;
}
Self::Input::Delete => sender.output(Self::Output::Delete(self.uid.clone())),
}
}
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
Some(match output {
Self::Output::Delete(id) => Self::ParentInput::DeleteEncoder(id),
})
}
async fn init_model(
init: Self::Init,
_index: &DynamicIndex,
_sender: AsyncFactorySender<Self>,
) -> Self {
Self {
encoder_conf: init.encoder_conf.unwrap_or_default(),
uid: Uuid::new_v4().to_string(),
}
}
}

View file

@ -11,7 +11,7 @@ use crate::gpu_profile::{
get_amd_gpu_power_profile, get_first_amd_gpu, get_set_amd_vr_pow_prof_cmd, GpuPowerProfile,
GpuSysDrm,
};
use crate::profile::{LighthouseDriver, Profile};
use crate::profile::{LighthouseDriver, Profile, XRServiceType};
use crate::steamvr_utils::chaperone_info_exists;
use crate::ui::app::{
AboutAction, BuildProfileAction, BuildProfileCleanAction, BuildProfileCleanDebugAction,
@ -84,6 +84,7 @@ pub enum MainViewOutMsg {
DeleteProfile,
SaveProfile(Profile),
OpenLibsurviveSetup,
OpenWivrnConfig,
}
pub struct MainViewInit {
@ -338,6 +339,32 @@ impl SimpleComponent for MainView {
},
model.steam_launch_options_box.widget(),
model.install_wivrn_box.widget(),
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
set_vexpand: false,
set_spacing: 12,
add_css_class: "card",
add_css_class: "padded",
#[track = "model.changed(Self::selected_profile())"]
set_visible: model.selected_profile.xrservice_type == XRServiceType::Wivrn,
gtk::Label {
add_css_class: "heading",
set_hexpand: true,
set_xalign: 0.0,
set_label: "Configure WiVRn",
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
},
gtk::Button {
add_css_class: "suggested-action",
set_label: "Configure",
set_halign: gtk::Align::End,
connect_clicked[sender] => move |_| {
sender.output(Self::Output::OpenWivrnConfig).expect("Sender output failed");
}
},
},
model.steamvr_calibration_box.widget(),
gtk::Box {
set_orientation: gtk::Orientation::Vertical,

View file

@ -122,6 +122,40 @@ pub fn number_entry_row<F: Fn(&adw::EntryRow) + 'static>(
row
}
pub fn spin_row<F: Fn(&gtk::Adjustment) + 'static>(
title: &str,
subtitle: Option<&str>,
val: f64,
min: f64,
max: f64,
min_increment: f64,
cb: F,
) -> adw::SpinRow {
let adj = gtk::Adjustment::builder()
.lower(min)
.upper(max)
.step_increment(min_increment)
.page_increment(min_increment)
.value(val)
.build();
let row = adw::SpinRow::builder()
.title(title)
.adjustment(&adj)
.digits(if min_increment == 1.0 {
0
} else {
min_increment.to_string().len() - 2
} as u32)
.build();
if let Some(sub) = subtitle {
row.set_subtitle(sub);
}
adj.connect_value_changed(cb);
row
}
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
title: &str,
description: Option<&str>,

View file

@ -1,8 +1,10 @@
use crate::file_builders::wivrn_config::{
dump_wivrn_config, get_wivrn_config, Encoder, WivrnConfig,
use super::factories::wivrn_encoder_group_factory::{WivrnEncoderModel, WivrnEncoderModelInit};
use crate::{
file_builders::wivrn_config::{dump_wivrn_config, get_wivrn_config, WivrnConfig},
ui::preference_rows::spin_row,
};
use adw::prelude::*;
use relm4::prelude::*;
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
#[tracker::track]
pub struct WivrnConfEditor {
@ -10,19 +12,19 @@ pub struct WivrnConfEditor {
#[tracker::do_not_track]
win: Option<adw::Window>,
#[tracker::do_not_track]
scalex_entry: Option<adw::EntryRow>,
pub encoder_models: Option<AsyncFactoryVecDeque<WivrnEncoderModel>>,
#[tracker::do_not_track]
scaley_entry: Option<adw::EntryRow>,
pub scalex_row: Option<adw::SpinRow>,
#[tracker::do_not_track]
encoder_combo: Option<adw::ComboRow>,
#[tracker::do_not_track]
bitrate_entry: Option<adw::EntryRow>,
pub scaley_row: Option<adw::SpinRow>,
}
#[derive(Debug)]
pub enum WivrnConfEditorMsg {
Present,
Save,
AddEncoder,
DeleteEncoder(String),
}
pub struct WivrnConfEditorInit {
@ -70,73 +72,50 @@ impl SimpleComponent for WivrnConfEditor {
set_content: pref_page = &adw::PreferencesPage {
set_hexpand: true,
set_vexpand: true,
set_description: "<a href=\"https://github.com/Meumeu/WiVRn#encoders\">WiVRn Configuration Documentation</a>",
add: scalegrp = &adw::PreferencesGroup {
set_title: "Scale",
set_description: Some("Render resolution scale. 1.0 is 100%."),
#[name(scalex_entry)]
adw::EntryRow {
set_title: "Scale X",
#[track = "model.changed(Self::conf())"]
set_text: match model.conf.scale {
Some([x, _]) => x.to_string(),
None => "".to_string(),
}.as_str(),
set_input_purpose: gtk::InputPurpose::Number,
},
#[name(scaley_entry)]
adw::EntryRow {
set_title: "Scale Y",
#[track = "model.changed(Self::conf())"]
set_text: match model.conf.scale {
Some([_, y]) => y.to_string(),
None => "".to_string(),
}.as_str(),
set_input_purpose: gtk::InputPurpose::Number,
},
},
add: encgrp = &adw::PreferencesGroup {
set_title: "Encoder",
#[name(encoder_combo)]
adw::ComboRow {
set_title: "Encoder",
set_subtitle: "x264: CPU based h264 encoding\n\nNVEnc: Nvidia GPU encoding\n\nVAAPI: Intel or AMD GPU encoding",
set_model: Some(&gtk::StringList::new(
Encoder::iter()
.map(Encoder::to_string)
.collect::<Vec<String>>()
.iter()
.map(String::as_str)
.collect::<Vec<&str>>()
.as_slice()
)),
#[track = "model.changed(Self::conf())"]
set_selected: model.conf.encoders.get(0).unwrap().encoder.as_number(),
},
#[name(bitrate_entry)]
adw::EntryRow {
set_title: "Bitrate",
#[track = "model.changed(Self::conf())"]
set_text: match model.conf.encoders.get(0).unwrap().bitrate {
Some(br) => br.to_string(),
None => "".to_string()
}.as_str(),
set_input_purpose: gtk::InputPurpose::Number,
},
},
add: save_grp = &adw::PreferencesGroup {
add: save_box = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
gtk::Button {
set_halign: gtk::Align::Center,
set_label: "Save",
add_css_class: "pill",
add_css_class: "suggested-action",
connect_clicked[sender] => move |_| {
sender.input(Self::Input::Save);
},
add: scalex_row = &spin_row(
"Scale X",
None,
match model.conf.scale {
Some([x, _]) => x.into(),
None => 1.0,
},
}
0.0,
1.0,
0.01,
move |_| {}
) -> adw::SpinRow,
add: scaley_row = &spin_row(
"Scale Y",
None,
match model.conf.scale {
Some([_, y]) => y.into(),
None => 1.0,
},
0.0,
1.0,
0.01,
move |_| {}
) -> adw::SpinRow,
},
add: encodersrgp = &adw::PreferencesGroup {
set_title: "Encoders",
adw::ActionRow {
set_title: "Add encoder",
add_suffix: add_encoder_btn = &gtk::Button {
set_halign: gtk::Align::Center,
set_valign: gtk::Align::Center,
add_css_class: "suggested-action",
set_icon_name: "list-add-symbolic",
set_tooltip_text: Some("Add encoder"),
connect_clicked[sender] => move |_| {
sender.input(Self::Input::AddEncoder)
}
},
},
},
}
}
@ -152,34 +131,41 @@ impl SimpleComponent for WivrnConfEditor {
self.win.as_ref().unwrap().present();
}
Self::Input::Save => {
let scalex = self.scalex_entry.as_ref().unwrap().text().parse::<f32>();
let scaley = self.scaley_entry.as_ref().unwrap().text().parse::<f32>();
if scalex.is_ok() && scaley.is_ok() {
self.conf.scale = Some([*scalex.as_ref().unwrap(), *scaley.as_ref().unwrap()]);
}
if scalex.is_ok() || scaley.is_ok() {
let scale = scalex.unwrap_or(scaley.unwrap());
self.conf.scale = Some([scale, scale]);
} else {
self.conf.scale = None
}
let mut enc = self.conf.encoders.remove(0);
let bitrate = self.bitrate_entry.as_ref().unwrap().text().parse::<u32>();
if let Ok(br) = bitrate {
enc.bitrate = Some(br);
}
let encoders = Encoder::as_vec();
let encoder =
encoders.get(self.encoder_combo.as_ref().unwrap().selected() as usize);
if let Some(e) = encoder {
enc.encoder = e.clone();
}
self.conf.encoders.insert(0, enc);
let x = self.scalex_row.as_ref().unwrap().adjustment().value();
let y = self.scaley_row.as_ref().unwrap().adjustment().value();
Some([x as f32, y as f32]);
self.conf.encoders = self
.encoder_models
.as_ref()
.unwrap()
.iter()
.filter(Option::is_some)
.map(|m| m.as_ref().unwrap().encoder_conf.clone())
.collect();
dump_wivrn_config(&self.conf);
self.win.as_ref().unwrap().close();
}
Self::Input::AddEncoder => {
self.encoder_models
.as_mut()
.unwrap()
.guard()
.push_back(WivrnEncoderModelInit::default());
}
Self::Input::DeleteEncoder(id) => {
let idx_opt = self
.encoder_models
.as_ref()
.unwrap()
.iter()
.position(|m_opt| m_opt.is_some_and(|m| m.uid == id));
if let Some(idx) = idx_opt {
self.encoder_models.as_mut().unwrap().guard().remove(idx);
} else {
eprintln!("Couldn't find encoder model with id {id}");
}
}
}
}
@ -190,21 +176,28 @@ impl SimpleComponent for WivrnConfEditor {
) -> ComponentParts<Self> {
let mut model = Self {
conf: get_wivrn_config(),
encoder_models: None,
win: None,
scalex_entry: None,
scaley_entry: None,
bitrate_entry: None,
encoder_combo: None,
scalex_row: None,
scaley_row: None,
tracker: 0,
};
let widgets = view_output!();
model.scalex_row = Some(widgets.scalex_row.clone());
model.scaley_row = Some(widgets.scaley_row.clone());
let mut encoder_models: AsyncFactoryVecDeque<WivrnEncoderModel> =
AsyncFactoryVecDeque::new(widgets.pref_page.clone(), sender.input_sender());
for encoder_conf in model.conf.encoders.clone() {
encoder_models.guard().push_back(WivrnEncoderModelInit {
encoder_conf: Some(encoder_conf),
});
}
model.encoder_models = Some(encoder_models);
model.win = Some(widgets.win.clone());
model.scalex_entry = Some(widgets.scalex_entry.clone());
model.scaley_entry = Some(widgets.scaley_entry.clone());
model.bitrate_entry = Some(widgets.bitrate_entry.clone());
model.encoder_combo = Some(widgets.encoder_combo.clone());
ComponentParts { model, widgets }
}