622 lines
22 KiB
Rust
622 lines
22 KiB
Rust
mod settings;
|
|
|
|
pub use settings::*;
|
|
pub use settings_schema;
|
|
|
|
use alvr_common::{
|
|
anyhow::{bail, Result},
|
|
semver::Version,
|
|
ToAny, ALVR_VERSION,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json as json;
|
|
use settings_schema::{NumberType, SchemaNode};
|
|
use std::{
|
|
collections::{HashMap, HashSet},
|
|
net::IpAddr,
|
|
path::PathBuf,
|
|
};
|
|
|
|
// SessionSettings is similar to Settings but it contains every branch, even unused ones. This is
|
|
// the settings representation that the UI uses.
|
|
pub type SessionSettings = settings::SettingsDefault;
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
pub struct DriversBackup {
|
|
pub alvr_path: PathBuf,
|
|
pub other_paths: Vec<PathBuf>,
|
|
}
|
|
|
|
// This structure is used to store the minimum configuration data that ALVR driver needs to
|
|
// initialize OpenVR before having the chance to communicate with a client. When a client is
|
|
// connected, a new OpenvrConfig instance is generated, then the connection is accepted only if that
|
|
// instance is equivalent to the one stored in the session, otherwise SteamVR is restarted.
|
|
// Other components (like the encoder, audio recorder) don't need this treatment and are initialized
|
|
// dynamically.
|
|
// todo: properties that can be set after the OpenVR initialization should be removed and set with
|
|
// UpdateForStream.
|
|
#[derive(Serialize, Deserialize, PartialEq, Default, Clone, Debug)]
|
|
pub struct OpenvrConfig {
|
|
pub eye_resolution_width: u32,
|
|
pub eye_resolution_height: u32,
|
|
pub target_eye_resolution_width: u32,
|
|
pub target_eye_resolution_height: u32,
|
|
pub tracking_ref_only: bool,
|
|
pub enable_vive_tracker_proxy: bool,
|
|
pub aggressive_keyframe_resend: bool,
|
|
pub adapter_index: u32,
|
|
pub codec: u32,
|
|
pub refresh_rate: u32,
|
|
pub use_10bit_encoder: bool,
|
|
pub enable_vbaq: bool,
|
|
pub use_preproc: bool,
|
|
pub preproc_sigma: u32,
|
|
pub preproc_tor: u32,
|
|
pub amd_encoder_quality_preset: u32,
|
|
pub rate_control_mode: u32,
|
|
pub filler_data: bool,
|
|
pub entropy_coding: u32,
|
|
pub force_sw_encoding: bool,
|
|
pub sw_thread_count: u32,
|
|
pub controller_is_tracker: bool,
|
|
pub controllers_enabled: bool,
|
|
pub enable_foveated_rendering: bool,
|
|
pub foveation_center_size_x: f32,
|
|
pub foveation_center_size_y: f32,
|
|
pub foveation_center_shift_x: f32,
|
|
pub foveation_center_shift_y: f32,
|
|
pub foveation_edge_ratio_x: f32,
|
|
pub foveation_edge_ratio_y: f32,
|
|
pub enable_color_correction: bool,
|
|
pub brightness: f32,
|
|
pub contrast: f32,
|
|
pub saturation: f32,
|
|
pub gamma: f32,
|
|
pub sharpening: f32,
|
|
pub linux_async_reprojection: bool,
|
|
pub nvenc_quality_preset: u32,
|
|
pub nvenc_tuning_preset: u32,
|
|
pub nvenc_multi_pass: u32,
|
|
pub nvenc_adaptive_quantization_mode: u32,
|
|
pub nvenc_low_delay_key_frame_scale: i64,
|
|
pub nvenc_refresh_rate: i64,
|
|
pub enable_intra_refresh: bool,
|
|
pub intra_refresh_period: i64,
|
|
pub intra_refresh_count: i64,
|
|
pub max_num_ref_frames: i64,
|
|
pub gop_length: i64,
|
|
pub p_frame_strategy: i64,
|
|
pub nvenc_rate_control_mode: i64,
|
|
pub rc_buffer_size: i64,
|
|
pub rc_initial_delay: i64,
|
|
pub rc_max_bitrate: i64,
|
|
pub rc_average_bitrate: i64,
|
|
pub nvenc_enable_weighted_prediction: bool,
|
|
pub capture_frame_dir: String,
|
|
pub amd_bitrate_corruption_fix: bool,
|
|
|
|
// these settings are not used on the C++ side, but we need them to correctly trigger a SteamVR
|
|
// restart
|
|
pub _controller_profile: i32,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
|
pub enum ConnectionState {
|
|
Disconnected,
|
|
Connecting,
|
|
Connected,
|
|
Streaming,
|
|
Disconnecting { should_be_removed: bool },
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
pub struct ClientConnectionConfig {
|
|
pub display_name: String,
|
|
pub current_ip: Option<IpAddr>,
|
|
pub manual_ips: HashSet<IpAddr>,
|
|
pub trusted: bool,
|
|
pub connection_state: ConnectionState,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
pub struct SessionConfig {
|
|
pub server_version: Version,
|
|
pub drivers_backup: Option<DriversBackup>,
|
|
pub openvr_config: OpenvrConfig,
|
|
// The hashmap key is the hostname
|
|
pub client_connections: HashMap<String, ClientConnectionConfig>,
|
|
pub session_settings: SessionSettings,
|
|
}
|
|
|
|
impl Default for SessionConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
server_version: ALVR_VERSION.clone(),
|
|
drivers_backup: None,
|
|
openvr_config: OpenvrConfig {
|
|
// avoid realistic resolutions, as on first start, on Linux, it
|
|
// could trigger direct mode on an existing monitor
|
|
eye_resolution_width: 800,
|
|
eye_resolution_height: 900,
|
|
target_eye_resolution_width: 800,
|
|
target_eye_resolution_height: 900,
|
|
adapter_index: 0,
|
|
refresh_rate: 60,
|
|
controllers_enabled: false,
|
|
enable_foveated_rendering: false,
|
|
enable_color_correction: false,
|
|
linux_async_reprojection: false,
|
|
capture_frame_dir: "/tmp".into(),
|
|
..<_>::default()
|
|
},
|
|
client_connections: HashMap::new(),
|
|
session_settings: settings::session_settings_default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SessionConfig {
|
|
// If json_value is not a valid representation of SessionConfig (because of version upgrade),
|
|
// use some fuzzy logic to extrapolate as much information as possible.
|
|
// Since SessionConfig cannot have a schema (because SessionSettings would need to also have a
|
|
// schema, but it is generated out of our control), we only do basic name checking on fields and
|
|
// deserialization will fail if the type of values does not match. Because of this,
|
|
// `session_settings` must be handled separately to do a better job of retrieving data using the
|
|
// settings schema.
|
|
pub fn merge_from_json(&mut self, json_value: &json::Value) -> Result<()> {
|
|
const SESSION_SETTINGS_STR: &str = "session_settings";
|
|
|
|
if let Ok(session_desc) = json::from_value(json_value.clone()) {
|
|
*self = session_desc;
|
|
return Ok(());
|
|
}
|
|
|
|
// Note: unwrap is safe because current session is expected to serialize correctly
|
|
let old_session_json = json::to_value(&self).unwrap();
|
|
let old_session_fields = old_session_json.as_object().unwrap();
|
|
|
|
let maybe_session_settings_json =
|
|
json_value
|
|
.get(SESSION_SETTINGS_STR)
|
|
.map(|new_session_settings_json| {
|
|
extrapolate_session_settings_from_session_settings(
|
|
&old_session_fields[SESSION_SETTINGS_STR],
|
|
new_session_settings_json,
|
|
&Settings::schema(settings::session_settings_default()),
|
|
)
|
|
});
|
|
|
|
let new_fields = old_session_fields
|
|
.iter()
|
|
.map(|(name, json_field_value)| {
|
|
let new_json_field_value = if name == SESSION_SETTINGS_STR {
|
|
json::to_value(settings::session_settings_default()).unwrap()
|
|
} else {
|
|
json_value.get(name).unwrap_or(json_field_value).clone()
|
|
};
|
|
(name.clone(), new_json_field_value)
|
|
})
|
|
.collect();
|
|
// Failure to extrapolate other session_desc fields is not notified.
|
|
let mut session_desc_mut =
|
|
json::from_value::<SessionConfig>(json::Value::Object(new_fields)).unwrap_or_default();
|
|
|
|
match maybe_session_settings_json
|
|
.to_any()
|
|
.and_then(|s| serde_json::from_value::<SessionSettings>(s).map_err(|e| e.into()))
|
|
{
|
|
Ok(session_settings) => {
|
|
session_desc_mut.session_settings = session_settings;
|
|
*self = session_desc_mut;
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
*self = session_desc_mut;
|
|
|
|
bail!("Error while deserializing extrapolated session settings: {e}")
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn to_settings(&self) -> Settings {
|
|
let session_settings_json = json::to_value(&self.session_settings).unwrap();
|
|
let schema = Settings::schema(settings::session_settings_default());
|
|
|
|
json::from_value::<Settings>(json_session_settings_to_settings(
|
|
&session_settings_json,
|
|
&schema,
|
|
))
|
|
.map_err(|e| dbg!(e))
|
|
.unwrap()
|
|
}
|
|
}
|
|
|
|
// Current data extrapolation strategy: match both field name and value type exactly.
|
|
// Integer bounds are not validated, if they do not match the schema, deserialization will fail and
|
|
// all data is lost.
|
|
// Future strategies: check if value respects schema constraints, fuzzy field name matching, accept
|
|
// integer to float and float to integer, tree traversal.
|
|
fn extrapolate_session_settings_from_session_settings(
|
|
old_session_settings: &json::Value,
|
|
new_session_settings: &json::Value,
|
|
schema: &SchemaNode,
|
|
) -> json::Value {
|
|
match schema {
|
|
SchemaNode::Section(entries) => json::Value::Object({
|
|
let mut entries: json::Map<String, json::Value> = entries
|
|
.iter()
|
|
.map(|named_entry| {
|
|
let value_json =
|
|
if let Some(new_value_json) = new_session_settings.get(&named_entry.name) {
|
|
extrapolate_session_settings_from_session_settings(
|
|
&old_session_settings[&named_entry.name],
|
|
new_value_json,
|
|
&named_entry.content,
|
|
)
|
|
} else {
|
|
old_session_settings[&named_entry.name].clone()
|
|
};
|
|
(named_entry.name.clone(), value_json)
|
|
})
|
|
.collect();
|
|
|
|
let collapsed_state = new_session_settings
|
|
.get("gui_collapsed")
|
|
.and_then(|val| val.as_bool())
|
|
.unwrap_or_else(|| {
|
|
old_session_settings
|
|
.get("gui_collapsed")
|
|
.unwrap()
|
|
.as_bool()
|
|
.unwrap()
|
|
});
|
|
entries.insert("gui_collapsed".into(), json::Value::Bool(collapsed_state));
|
|
|
|
entries
|
|
}),
|
|
|
|
SchemaNode::Choice { variants, .. } => {
|
|
let variant_json = new_session_settings
|
|
.get("variant")
|
|
.cloned()
|
|
.filter(|new_variant_json| {
|
|
new_variant_json
|
|
.as_str()
|
|
.map(|variant_str| {
|
|
variants
|
|
.iter()
|
|
.any(|named_entry| variant_str == named_entry.name)
|
|
})
|
|
.unwrap_or(false)
|
|
})
|
|
.unwrap_or_else(|| old_session_settings["variant"].clone());
|
|
|
|
let mut fields: json::Map<_, _> = variants
|
|
.iter()
|
|
.filter_map(|named_entry| {
|
|
named_entry.content.as_ref().map(|data_schema| {
|
|
let value_json = if let Some(new_value_json) =
|
|
new_session_settings.get(&named_entry.name)
|
|
{
|
|
extrapolate_session_settings_from_session_settings(
|
|
&old_session_settings[&named_entry.name],
|
|
new_value_json,
|
|
data_schema,
|
|
)
|
|
} else {
|
|
old_session_settings[&named_entry.name].clone()
|
|
};
|
|
(named_entry.name.clone(), value_json)
|
|
})
|
|
})
|
|
.collect();
|
|
fields.insert("variant".into(), variant_json);
|
|
|
|
json::Value::Object(fields)
|
|
}
|
|
|
|
SchemaNode::Optional { content, .. } => {
|
|
let set_json = new_session_settings
|
|
.get("set")
|
|
.cloned()
|
|
.filter(|new_set_json| new_set_json.is_boolean())
|
|
.unwrap_or_else(|| old_session_settings["set"].clone());
|
|
|
|
let content_json = new_session_settings
|
|
.get("content")
|
|
.map(|new_content_json| {
|
|
extrapolate_session_settings_from_session_settings(
|
|
&old_session_settings["content"],
|
|
new_content_json,
|
|
content,
|
|
)
|
|
})
|
|
.unwrap_or_else(|| old_session_settings["content"].clone());
|
|
|
|
json::json!({
|
|
"set": set_json,
|
|
"content": content_json
|
|
})
|
|
}
|
|
|
|
SchemaNode::Switch { content, .. } => {
|
|
let enabled_json = new_session_settings
|
|
.get("enabled")
|
|
.cloned()
|
|
.filter(|new_enabled_json| new_enabled_json.is_boolean())
|
|
.unwrap_or_else(|| old_session_settings["enabled"].clone());
|
|
|
|
let content_json = new_session_settings
|
|
.get("content")
|
|
.map(|new_content_json| {
|
|
extrapolate_session_settings_from_session_settings(
|
|
&old_session_settings["content"],
|
|
new_content_json,
|
|
content,
|
|
)
|
|
})
|
|
.unwrap_or_else(|| old_session_settings["content"].clone());
|
|
|
|
json::json!({
|
|
"enabled": enabled_json,
|
|
"content": content_json
|
|
})
|
|
}
|
|
|
|
SchemaNode::Boolean { .. } => {
|
|
if new_session_settings.is_boolean() {
|
|
new_session_settings.clone()
|
|
} else {
|
|
old_session_settings.clone()
|
|
}
|
|
}
|
|
|
|
SchemaNode::Number { ty, .. } => {
|
|
if let Some(value) = new_session_settings.as_f64() {
|
|
match ty {
|
|
NumberType::UnsignedInteger => json::Value::from(value.abs() as u64),
|
|
NumberType::SignedInteger => json::Value::from(value as i64),
|
|
NumberType::Float => new_session_settings.clone(),
|
|
}
|
|
} else {
|
|
old_session_settings.clone()
|
|
}
|
|
}
|
|
|
|
SchemaNode::Text { .. } => {
|
|
if new_session_settings.is_string() {
|
|
new_session_settings.clone()
|
|
} else {
|
|
old_session_settings.clone()
|
|
}
|
|
}
|
|
|
|
SchemaNode::Array(array_schema) => {
|
|
let array_vec = (0..array_schema.len())
|
|
.map(|idx| {
|
|
new_session_settings
|
|
.get(idx)
|
|
.cloned()
|
|
.unwrap_or_else(|| old_session_settings[idx].clone())
|
|
})
|
|
.collect();
|
|
json::Value::Array(array_vec)
|
|
}
|
|
|
|
SchemaNode::Vector {
|
|
default_element, ..
|
|
} => {
|
|
let element_json = new_session_settings
|
|
.get("element")
|
|
.map(|new_element_json| {
|
|
extrapolate_session_settings_from_session_settings(
|
|
&old_session_settings["element"],
|
|
new_element_json,
|
|
default_element,
|
|
)
|
|
})
|
|
.unwrap_or_else(|| old_session_settings["element"].clone());
|
|
|
|
// todo: content field cannot be properly validated until I implement plain settings
|
|
// validation (not to be confused with session/session_settings validation). Any
|
|
// problem inside this new_session_settings content will result in the loss all data in the new
|
|
// session_settings.
|
|
let content_json = new_session_settings
|
|
.get("content")
|
|
.cloned()
|
|
.unwrap_or_else(|| old_session_settings["content"].clone());
|
|
|
|
json::json!({
|
|
"element": element_json,
|
|
"content": content_json
|
|
})
|
|
}
|
|
|
|
SchemaNode::Dictionary { default_value, .. } => {
|
|
let key_json = new_session_settings
|
|
.get("key")
|
|
.cloned()
|
|
.filter(|new_key| new_key.is_string())
|
|
.unwrap_or_else(|| old_session_settings["key"].clone());
|
|
|
|
let value_json = new_session_settings
|
|
.get("value")
|
|
.map(|new_value_json| {
|
|
extrapolate_session_settings_from_session_settings(
|
|
&old_session_settings["value"],
|
|
new_value_json,
|
|
default_value,
|
|
)
|
|
})
|
|
.unwrap_or_else(|| old_session_settings["value"].clone());
|
|
|
|
// todo: validate content using settings validation
|
|
let content_json = new_session_settings
|
|
.get("content")
|
|
.cloned()
|
|
.unwrap_or_else(|| old_session_settings["content"].clone());
|
|
|
|
json::json!({
|
|
"key": key_json,
|
|
"value": value_json,
|
|
"content": content_json
|
|
})
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
// session_settings does not get validated here, it must be already valid
|
|
fn json_session_settings_to_settings(
|
|
session_settings: &json::Value,
|
|
schema: &SchemaNode,
|
|
) -> json::Value {
|
|
match schema {
|
|
SchemaNode::Section(entries) => json::Value::Object(
|
|
entries
|
|
.iter()
|
|
.map(|named_entry| {
|
|
(
|
|
named_entry.name.clone(),
|
|
json_session_settings_to_settings(
|
|
&session_settings[&named_entry.name],
|
|
&named_entry.content,
|
|
),
|
|
)
|
|
})
|
|
.collect(),
|
|
),
|
|
|
|
SchemaNode::Choice { variants, .. } => {
|
|
let variant = session_settings["variant"].as_str().unwrap();
|
|
let maybe_content = variants
|
|
.iter()
|
|
.find(|named_entry| named_entry.name == variant)
|
|
.and_then(|named_entry| named_entry.content.as_ref())
|
|
.map(|data_schema| {
|
|
json_session_settings_to_settings(&session_settings[variant], data_schema)
|
|
});
|
|
if let Some(content) = maybe_content {
|
|
json::json!({ variant: content })
|
|
} else {
|
|
json::Value::String(variant.to_owned())
|
|
}
|
|
}
|
|
|
|
SchemaNode::Optional { content, .. } => {
|
|
if session_settings["set"].as_bool().unwrap() {
|
|
json_session_settings_to_settings(&session_settings["content"], content)
|
|
} else {
|
|
json::Value::Null
|
|
}
|
|
}
|
|
|
|
SchemaNode::Switch { content, .. } => {
|
|
if session_settings["enabled"].as_bool().unwrap() {
|
|
let content =
|
|
json_session_settings_to_settings(&session_settings["content"], content);
|
|
|
|
json::json!({ "Enabled": content })
|
|
} else {
|
|
json::Value::String("Disabled".into())
|
|
}
|
|
}
|
|
|
|
SchemaNode::Boolean { .. } | SchemaNode::Number { .. } | SchemaNode::Text { .. } => {
|
|
session_settings.clone()
|
|
}
|
|
|
|
SchemaNode::Array(array_schema) => json::Value::Array(
|
|
array_schema
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, element_schema)| {
|
|
json_session_settings_to_settings(&session_settings[idx], element_schema)
|
|
})
|
|
.collect(),
|
|
),
|
|
|
|
SchemaNode::Vector {
|
|
default_element, ..
|
|
} => json::to_value(
|
|
session_settings["content"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|element_json| {
|
|
json_session_settings_to_settings(element_json, default_element)
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
.unwrap(),
|
|
|
|
SchemaNode::Dictionary { default_value, .. } => json::to_value(
|
|
json::from_value::<Vec<(String, json::Value)>>(session_settings["content"].clone())
|
|
.unwrap()
|
|
.into_iter()
|
|
.map(|(key, value_json)| {
|
|
(
|
|
key,
|
|
json_session_settings_to_settings(&value_json, default_value),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
.unwrap(),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_manual_session_to_settings() {
|
|
let default = session_settings_default();
|
|
let settings_schema = json::to_value(&default).unwrap();
|
|
let schema = Settings::schema(default);
|
|
|
|
let _settings = json::from_value::<Settings>(json_session_settings_to_settings(
|
|
&settings_schema,
|
|
&schema,
|
|
))
|
|
.err();
|
|
}
|
|
|
|
#[test]
|
|
fn test_session_to_settings() {
|
|
let _settings = SessionConfig::default().to_settings();
|
|
}
|
|
|
|
#[test]
|
|
fn test_session_extrapolation_trivial() {
|
|
SessionConfig::default()
|
|
.merge_from_json(&json::to_value(SessionConfig::default()).unwrap())
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_session_extrapolation_diff() {
|
|
let input_json_string = r#"{
|
|
"session_settings": {
|
|
"gui_collapsed": false,
|
|
"fjdshfks": false,
|
|
"video": {
|
|
"preferred_fps": 60.0
|
|
},
|
|
"headset": {
|
|
"gui_collapsed": false,
|
|
"controllers": {
|
|
"enabled": false
|
|
}
|
|
}
|
|
}
|
|
}"#;
|
|
|
|
SessionConfig::default()
|
|
.merge_from_json(&json::from_str(input_json_string).unwrap())
|
|
.unwrap();
|
|
}
|
|
}
|