diff --git a/Cargo.lock b/Cargo.lock index 4b028d32..27b5d525 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4557,6 +4557,7 @@ dependencies = [ "interfrost", "serde", "strum", + "sysinfo", "thiserror 2.0.18", "url", ] diff --git a/packages/oneclient_app/src/components/memory_field.rs b/packages/oneclient_app/src/components/memory_field.rs index 76176220..5827501f 100644 --- a/packages/oneclient_app/src/components/memory_field.rs +++ b/packages/oneclient_app/src/components/memory_field.rs @@ -39,7 +39,7 @@ pub fn memory_field(mut memory: State) -> impl IntoElement { .child( TextInput::new(memory) .width(Size::px(90.)) - .placeholder("4096") + .placeholder(oneclient_common::default_mem_max().to_string()) .on_validate(validate_memory) .trailing( label() diff --git a/packages/oneclient_app/src/utils.rs b/packages/oneclient_app/src/utils.rs index e8cf54d2..ada19be8 100644 --- a/packages/oneclient_app/src/utils.rs +++ b/packages/oneclient_app/src/utils.rs @@ -5,7 +5,9 @@ use chrono::{Datelike, NaiveDate}; use oneclient_core::clusters::Cluster; use oneclient_common::domain::GameLoader; use oneclient_common::{ParsedMcVersion, VersionKey, format_mc_version, parse_mc_version}; -use sysinfo::{MemoryRefreshKind, RefreshKind, System}; + +use oneclient_common::MEMORY_HEADROOM_GB; +pub use oneclient_common::total_ram_mb; pub type ClusterGroups = BTreeMap>; @@ -207,19 +209,7 @@ pub fn format_res((w, h): (u32, u32)) -> String { /// Prevents user from choosing his max amount of ram preset (e.g Someone has 16GB of RAM, /// so the max preset is 16GB - 2GB = 14GB) -const MEMORY_HEADROOM_GB: u32 = 2; -const MEMORY_PRESETS_GB: [u32; 10] = [2, 4, 6, 8, 12, 16, 24, 32, 48, 64]; - -pub fn total_ram_mb() -> u32 { - static TOTAL_RAM_MB: std::sync::OnceLock = std::sync::OnceLock::new(); - - *TOTAL_RAM_MB.get_or_init(|| { - let system = System::new_with_specifics( - RefreshKind::nothing().with_memory(MemoryRefreshKind::nothing().with_ram()), - ); - (system.total_memory() / 1024 / 1024).min(u32::MAX as u64) as u32 - }) -} +const MEMORY_PRESETS_GB: [u32; 11] = [2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64]; pub fn memory_presets_mb() -> Vec { presets_for_total_gb((total_ram_mb() as f32 / 1024.).round() as u32) @@ -451,17 +441,17 @@ mod tests { fn presets_stop_two_gigabytes_short_of_the_machine() { assert_eq!( presets_for_total_gb(16), - vec![2048, 4096, 6144, 8192, 12288, 14336] + vec![2048, 3072, 4096, 6144, 8192, 12288, 14336] ); assert_eq!( presets_for_total_gb(32), - vec![2048, 4096, 6144, 8192, 12288, 16384, 24576, 30720] + vec![2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576, 30720] ); } #[test] fn a_ceiling_landing_on_a_round_step_is_not_repeated() { - assert_eq!(presets_for_total_gb(8), vec![2048, 4096, 6144]); + assert_eq!(presets_for_total_gb(8), vec![2048, 3072, 4096, 6144]); } #[test] diff --git a/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs b/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs index 17381e31..7242eb53 100644 --- a/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs +++ b/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs @@ -112,7 +112,9 @@ impl Component for ClusterSettings { MemoryRow { cluster_id, value: profile.mem_max, - global: global.mem_max.unwrap_or(4096), + global: global + .mem_max + .unwrap_or_else(oneclient_common::default_mem_max), } .into_element(), ) diff --git a/packages/oneclient_cluster/src/profile.rs b/packages/oneclient_cluster/src/profile.rs index 0177544c..07d7c86c 100644 --- a/packages/oneclient_cluster/src/profile.rs +++ b/packages/oneclient_cluster/src/profile.rs @@ -68,7 +68,7 @@ impl GameSettingsProfile { java_path: None, resolution: None, force_fullscreen: Some(false), - mem_max: Some(4096), + mem_max: Some(oneclient_common::default_mem_max()), launch_args: None, launch_env: None, hook_pre: None, diff --git a/packages/oneclient_common/Cargo.toml b/packages/oneclient_common/Cargo.toml index cf1ad39b..12873d14 100644 --- a/packages/oneclient_common/Cargo.toml +++ b/packages/oneclient_common/Cargo.toml @@ -20,6 +20,7 @@ interfrost.workspace = true directories.workspace = true serde.workspace = true strum.workspace = true +sysinfo.workspace = true thiserror.workspace = true url.workspace = true diff --git a/packages/oneclient_common/src/lib.rs b/packages/oneclient_common/src/lib.rs index bf620eb9..27376f67 100644 --- a/packages/oneclient_common/src/lib.rs +++ b/packages/oneclient_common/src/lib.rs @@ -4,6 +4,7 @@ pub mod consent; pub mod constants; pub mod domain; +pub mod memory; pub mod os_ext; pub mod paths; pub mod patch; @@ -17,6 +18,7 @@ pub use domain::{ ContentType, GameLoader, HashAlgorithm, PackageUpdateMode, ProviderId, Resolution, }; pub use error::{PathsError, PathsResult}; +pub use memory::{MEMORY_HEADROOM_GB, default_mem_max, default_mem_max_for_total, total_ram_mb}; pub use os_ext::OsExt; pub use patch::Patch; pub use search::{MatchScore, SearchQuery, normalize_query}; diff --git a/packages/oneclient_common/src/memory.rs b/packages/oneclient_common/src/memory.rs new file mode 100644 index 00000000..f834c2d6 --- /dev/null +++ b/packages/oneclient_common/src/memory.rs @@ -0,0 +1,55 @@ +use sysinfo::{MemoryRefreshKind, RefreshKind, System}; + +pub const MEMORY_HEADROOM_GB: u32 = 2; + +const EIGHT_GB_MB: u32 = 7 * 1024; +const TWELVE_GB_MB: u32 = 11 * 1024; + +pub fn total_ram_mb() -> u32 { + static TOTAL_RAM_MB: std::sync::OnceLock = std::sync::OnceLock::new(); + + *TOTAL_RAM_MB.get_or_init(|| { + let system = System::new_with_specifics( + RefreshKind::nothing().with_memory(MemoryRefreshKind::nothing().with_ram()), + ); + (system.total_memory() / 1024 / 1024).min(u32::MAX as u64) as u32 + }) +} + +#[must_use] +pub fn default_mem_max() -> u32 { + default_mem_max_for_total(total_ram_mb()) +} + +#[must_use] +pub fn default_mem_max_for_total(total_mb: u32) -> u32 { + match total_mb { + 0..EIGHT_GB_MB => 2048, + EIGHT_GB_MB..TWELVE_GB_MB => 3072, + _ => 4096, + } +} + +#[cfg(test)] +mod tests { + use super::default_mem_max_for_total; + + #[test] + fn the_default_heap_ramps_with_total_ram() { + assert_eq!(default_mem_max_for_total(3987), 2048); // 4GB + assert_eq!(default_mem_max_for_total(6060), 2048); // 6GB + assert_eq!(default_mem_max_for_total(7167), 2048); + assert_eq!(default_mem_max_for_total(7168), 3072); + assert_eq!(default_mem_max_for_total(7900), 3072); // 8GB + assert_eq!(default_mem_max_for_total(11263), 3072); + assert_eq!(default_mem_max_for_total(11264), 4096); + assert_eq!(default_mem_max_for_total(11800), 4096); // 12GB + assert_eq!(default_mem_max_for_total(16290), 4096); // 16GB + assert_eq!(default_mem_max_for_total(65229), 4096); // 64GB + } + + #[test] + fn a_machine_that_reports_nothing_still_gets_a_heap() { + assert_eq!(default_mem_max_for_total(0), 2048); + } +} diff --git a/packages/oneclient_core/src/game/launch.rs b/packages/oneclient_core/src/game/launch.rs index d43804f4..60e3ba08 100644 --- a/packages/oneclient_core/src/game/launch.rs +++ b/packages/oneclient_core/src/game/launch.rs @@ -259,7 +259,7 @@ pub async fn launch_cluster( &libraries, &classpaths, &version_name, - profile.mem_max.unwrap_or(2048), + profile.mem_max.unwrap_or_else(oneclient_common::default_mem_max), profile.launch_args.clone().unwrap_or_default(), &java.os_arch, java.major, @@ -349,8 +349,13 @@ pub async fn launch_cluster( mc_version: cluster.mc_version.clone(), }); - let recorder = - SessionRecorder::start(state, cluster_id, profile.mem_max.unwrap_or(2048), &java).await; + let recorder = SessionRecorder::start( + state, + cluster_id, + profile.mem_max.unwrap_or_else(oneclient_common::default_mem_max), + &java, + ) + .await; // Pinned to the session row so that if the launcher exits first the next // start can tell whether the game is still playing diff --git a/packages/oneclient_core/tests/clusters.rs b/packages/oneclient_core/tests/clusters.rs index 3ab6afd1..13f6946a 100644 --- a/packages/oneclient_core/tests/clusters.rs +++ b/packages/oneclient_core/tests/clusters.rs @@ -13,11 +13,12 @@ async fn cluster_lifecycle_with_settings_profile() { let global = state.settings.read().global_game_settings.clone(); - // Deliberately not the global default of 4096 so the assertions distinguish - // "the cluster's own profile was used" from "fell back to global and matched" + // Deliberately not a value `default_mem_max()` can return so the assertions + // distinguish "the cluster's own profile was used" from "fell back to global + // and matched" let cluster = state.clusters.create( &global, - CreateClusterOptions::new("Test Cluster", "1.21.1", GameLoader::Fabric).mem_max(2048), + CreateClusterOptions::new("Test Cluster", "1.21.1", GameLoader::Fabric).mem_max(5120), ) .await .unwrap(); @@ -31,7 +32,7 @@ async fn cluster_lifecycle_with_settings_profile() { let resolved = state.clusters.resolve_settings(&global, &cluster) .await .unwrap(); - assert_eq!(resolved.mem_max, Some(2048)); + assert_eq!(resolved.mem_max, Some(5120)); state.clusters.update_profile(cluster.id, ProfileUpdate {