modde_games/
optiscaler.rs1pub(crate) mod loader;
8pub(crate) mod spec;
9
10use std::sync::OnceLock;
11
12use dashmap::DashMap;
13use tracing::warn;
14
15use crate::generic::leak::slice as leak_slice;
16
17pub use spec::{
18 OptiScalerImportToml, OptiScalerProfilesSpec, serialize as serialize_optiscaler_profiles_toml,
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct OptiScalerIniOverride {
24 pub key: &'static str,
25 pub value: &'static str,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[allow(clippy::struct_excessive_bools)]
31pub struct OptiScalerProfile {
32 pub id: &'static str,
33 pub name: &'static str,
34 pub source_url: &'static str,
35 pub tested_optiscaler_version: &'static str,
36 pub source_mode: Option<&'static str>,
37 pub goverlay_channel: Option<&'static str>,
38 pub proxy_dll: &'static str,
39 pub release_tag: Option<&'static str>,
40 pub release_asset: Option<&'static str>,
41 pub wine_dll_overrides: &'static [&'static str],
42 pub copy_companion_files: bool,
43 pub enable_optipatcher: bool,
44 pub fsr4_variant: Option<&'static str>,
45 pub emulate_fp8: bool,
46 pub spoof_dlss: bool,
47 pub ini_overrides: &'static [OptiScalerIniOverride],
48 pub notes: &'static str,
49}
50
51static MERGED_PROFILE_CACHE: OnceLock<DashMap<String, &'static [OptiScalerProfile]>> =
52 OnceLock::new();
53
54fn merged_profile_cache() -> &'static DashMap<String, &'static [OptiScalerProfile]> {
55 MERGED_PROFILE_CACHE.get_or_init(DashMap::new)
56}
57
58#[must_use]
60pub fn resolve_optiscaler_profiles(game_id: &str) -> &'static [OptiScalerProfile] {
61 let built_in = crate::registry::resolve_game(game_id)
62 .map_or(&[] as &'static [OptiScalerProfile], |game| {
63 game.optiscaler_profiles
64 });
65 let user = loader::load_user_profiles(game_id);
66
67 if user.is_empty() {
68 return built_in;
69 }
70
71 if built_in.is_empty() {
72 return user;
73 }
74
75 if let Some(profiles) = merged_profile_cache().get(game_id) {
76 return *profiles;
77 }
78
79 let mut merged = built_in.to_vec();
80 for profile in user {
81 if let Some(existing_index) = merged.iter().position(|existing| existing.id == profile.id) {
82 warn!(
83 game_id,
84 profile_id = profile.id,
85 "user OptiScaler profile overrides built-in profile"
86 );
87 merged[existing_index] = *profile;
88 } else {
89 merged.push(*profile);
90 }
91 }
92
93 *merged_profile_cache()
94 .entry(game_id.to_string())
95 .or_insert_with(move || leak_slice(merged))
96}
97
98#[must_use]
100pub fn default_optiscaler_profile(game_id: &str) -> Option<&'static OptiScalerProfile> {
101 resolve_optiscaler_profiles(game_id).first()
102}
103
104#[cfg(test)]
105mod tests {
106 use super::{default_optiscaler_profile, resolve_optiscaler_profiles};
107 use std::sync::OnceLock;
108
109 fn shared_data_dir() -> &'static std::path::PathBuf {
110 static DATA_DIR: OnceLock<std::path::PathBuf> = OnceLock::new();
111 DATA_DIR.get_or_init(|| {
112 let tempdir = tempfile::TempDir::new().expect("create tempdir");
113 let data_dir = tempdir.path().join("data");
114 std::fs::create_dir_all(data_dir.join("games")).expect("create games dir");
115 modde_core::paths::set_data_dir(data_dir.clone());
116 std::mem::forget(tempdir);
117 data_dir
118 })
119 }
120
121 #[test]
122 fn stellar_blade_resolves_community_optiscaler_profile() {
123 let _ = shared_data_dir();
124 let profiles = resolve_optiscaler_profiles("stellar-blade");
125
126 assert_eq!(profiles.len(), 1);
127 assert_eq!(profiles[0].id, "community-dxgi");
128 assert_eq!(profiles[0].proxy_dll, "dxgi.dll");
129 assert_eq!(profiles[0].tested_optiscaler_version, "0.9");
130 assert_eq!(profiles[0].source_mode, Some("github_release"));
131 assert_eq!(profiles[0].goverlay_channel, None);
132 assert!(profiles[0].enable_optipatcher);
133 assert_eq!(profiles[0].fsr4_variant, Some("latest_fp8"));
134 assert!(profiles[0].emulate_fp8);
135 assert!(!profiles[0].spoof_dlss);
136 assert_eq!(
137 default_optiscaler_profile("stellar-blade").map(|profile| profile.id),
138 Some("community-dxgi")
139 );
140 }
141
142 #[test]
143 fn unsupported_optiscaler_profiles_resolve_empty() {
144 let _ = shared_data_dir();
145 assert!(resolve_optiscaler_profiles("skyrim-se").is_empty());
146 assert!(default_optiscaler_profile("skyrim-se").is_none());
147 }
148
149 #[test]
150 fn missing_sidecar_returns_built_in_unchanged() {
151 let _ = shared_data_dir();
152 let first = resolve_optiscaler_profiles("stellar-blade");
153 let second = resolve_optiscaler_profiles("stellar-blade");
154
155 assert_eq!(first.len(), 1);
156 assert!(std::ptr::eq(first, second));
157 }
158}