SettingsViewModel.cs
1 using Avalonia.Collections; 2 using Avalonia.Controls; 3 using Avalonia.Threading; 4 using LibHac.Tools.FsSystem; 5 using Ryujinx.Audio.Backends.OpenAL; 6 using Ryujinx.Audio.Backends.SDL2; 7 using Ryujinx.Audio.Backends.SoundIo; 8 using Ryujinx.Ava.Common.Locale; 9 using Ryujinx.Ava.UI.Helpers; 10 using Ryujinx.Ava.UI.Models.Input; 11 using Ryujinx.Ava.UI.Windows; 12 using Ryujinx.Common.Configuration; 13 using Ryujinx.Common.Configuration.Multiplayer; 14 using Ryujinx.Common.GraphicsDriver; 15 using Ryujinx.Common.Logging; 16 using Ryujinx.Graphics.Vulkan; 17 using Ryujinx.HLE.FileSystem; 18 using Ryujinx.HLE.HOS.Services.Time.TimeZone; 19 using Ryujinx.UI.Common.Configuration; 20 using Ryujinx.UI.Common.Configuration.System; 21 using System; 22 using System.Collections.Generic; 23 using System.Collections.ObjectModel; 24 using System.Linq; 25 using System.Net.NetworkInformation; 26 using System.Runtime.InteropServices; 27 using System.Threading.Tasks; 28 using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; 29 30 namespace Ryujinx.Ava.UI.ViewModels 31 { 32 public class SettingsViewModel : BaseModel 33 { 34 private readonly VirtualFileSystem _virtualFileSystem; 35 private readonly ContentManager _contentManager; 36 private TimeZoneContentManager _timeZoneContentManager; 37 38 private readonly List<string> _validTzRegions; 39 40 private readonly Dictionary<string, string> _networkInterfaces; 41 42 private float _customResolutionScale; 43 private int _resolutionScale; 44 private int _graphicsBackendMultithreadingIndex; 45 private float _volume; 46 private bool _isVulkanAvailable = true; 47 private bool _directoryChanged; 48 private readonly List<string> _gpuIds = new(); 49 private int _graphicsBackendIndex; 50 private int _scalingFilter; 51 private int _scalingFilterLevel; 52 53 public event Action CloseWindow; 54 public event Action SaveSettingsEvent; 55 private int _networkInterfaceIndex; 56 private int _multiplayerModeIndex; 57 58 public int ResolutionScale 59 { 60 get => _resolutionScale; 61 set 62 { 63 _resolutionScale = value; 64 65 OnPropertyChanged(nameof(CustomResolutionScale)); 66 OnPropertyChanged(nameof(IsCustomResolutionScaleActive)); 67 } 68 } 69 70 public int GraphicsBackendMultithreadingIndex 71 { 72 get => _graphicsBackendMultithreadingIndex; 73 set 74 { 75 _graphicsBackendMultithreadingIndex = value; 76 77 if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value) 78 { 79 Dispatcher.UIThread.InvokeAsync(() => 80 ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage], 81 "", 82 "", 83 LocaleManager.Instance[LocaleKeys.InputDialogOk], 84 LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle]) 85 ); 86 } 87 88 OnPropertyChanged(); 89 } 90 } 91 92 public float CustomResolutionScale 93 { 94 get => _customResolutionScale; 95 set 96 { 97 _customResolutionScale = MathF.Round(value, 1); 98 99 OnPropertyChanged(); 100 } 101 } 102 103 public bool IsVulkanAvailable 104 { 105 get => _isVulkanAvailable; 106 set 107 { 108 _isVulkanAvailable = value; 109 110 OnPropertyChanged(); 111 } 112 } 113 114 public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS(); 115 116 public bool IsHypervisorAvailable => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; 117 118 public bool DirectoryChanged 119 { 120 get => _directoryChanged; 121 set 122 { 123 _directoryChanged = value; 124 125 OnPropertyChanged(); 126 } 127 } 128 129 public bool IsMacOS => OperatingSystem.IsMacOS(); 130 131 public bool EnableDiscordIntegration { get; set; } 132 public bool CheckUpdatesOnStart { get; set; } 133 public bool ShowConfirmExit { get; set; } 134 public bool RememberWindowState { get; set; } 135 public int HideCursor { get; set; } 136 public bool EnableDockedMode { get; set; } 137 public bool EnableKeyboard { get; set; } 138 public bool EnableMouse { get; set; } 139 public bool EnableVsync { get; set; } 140 public bool EnablePptc { get; set; } 141 public bool EnableInternetAccess { get; set; } 142 public bool EnableFsIntegrityChecks { get; set; } 143 public bool IgnoreMissingServices { get; set; } 144 public bool ExpandDramSize { get; set; } 145 public bool EnableShaderCache { get; set; } 146 public bool EnableTextureRecompression { get; set; } 147 public bool EnableMacroHLE { get; set; } 148 public bool EnableColorSpacePassthrough { get; set; } 149 public bool ColorSpacePassthroughAvailable => IsMacOS; 150 public bool EnableFileLog { get; set; } 151 public bool EnableStub { get; set; } 152 public bool EnableInfo { get; set; } 153 public bool EnableWarn { get; set; } 154 public bool EnableError { get; set; } 155 public bool EnableTrace { get; set; } 156 public bool EnableGuest { get; set; } 157 public bool EnableFsAccessLog { get; set; } 158 public bool EnableDebug { get; set; } 159 public bool IsOpenAlEnabled { get; set; } 160 public bool IsSoundIoEnabled { get; set; } 161 public bool IsSDL2Enabled { get; set; } 162 public bool IsCustomResolutionScaleActive => _resolutionScale == 4; 163 public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr; 164 165 public bool IsVulkanSelected => GraphicsBackendIndex == 0; 166 public bool UseHypervisor { get; set; } 167 168 public string TimeZone { get; set; } 169 public string ShaderDumpPath { get; set; } 170 171 public int Language { get; set; } 172 public int Region { get; set; } 173 public int FsGlobalAccessLogMode { get; set; } 174 public int AudioBackend { get; set; } 175 public int MaxAnisotropy { get; set; } 176 public int AspectRatio { get; set; } 177 public int AntiAliasingEffect { get; set; } 178 public string ScalingFilterLevelText => ScalingFilterLevel.ToString("0"); 179 public int ScalingFilterLevel 180 { 181 get => _scalingFilterLevel; 182 set 183 { 184 _scalingFilterLevel = value; 185 OnPropertyChanged(); 186 OnPropertyChanged(nameof(ScalingFilterLevelText)); 187 } 188 } 189 public int OpenglDebugLevel { get; set; } 190 public int MemoryMode { get; set; } 191 public int BaseStyleIndex { get; set; } 192 public int GraphicsBackendIndex 193 { 194 get => _graphicsBackendIndex; 195 set 196 { 197 _graphicsBackendIndex = value; 198 OnPropertyChanged(); 199 OnPropertyChanged(nameof(IsVulkanSelected)); 200 } 201 } 202 public int ScalingFilter 203 { 204 get => _scalingFilter; 205 set 206 { 207 _scalingFilter = value; 208 OnPropertyChanged(); 209 OnPropertyChanged(nameof(IsScalingFilterActive)); 210 } 211 } 212 213 public int PreferredGpuIndex { get; set; } 214 215 public float Volume 216 { 217 get => _volume; 218 set 219 { 220 _volume = value; 221 222 ConfigurationState.Instance.System.AudioVolume.Value = _volume / 100; 223 224 OnPropertyChanged(); 225 } 226 } 227 228 public DateTimeOffset CurrentDate { get; set; } 229 public TimeSpan CurrentTime { get; set; } 230 231 internal AvaloniaList<TimeZone> TimeZones { get; set; } 232 public AvaloniaList<string> GameDirectories { get; set; } 233 public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; } 234 235 public AvaloniaList<string> NetworkInterfaceList 236 { 237 get => new(_networkInterfaces.Keys); 238 } 239 240 public HotkeyConfig KeyboardHotkey { get; set; } 241 242 public int NetworkInterfaceIndex 243 { 244 get => _networkInterfaceIndex; 245 set 246 { 247 _networkInterfaceIndex = value != -1 ? value : 0; 248 ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[_networkInterfaceIndex]]; 249 } 250 } 251 252 public int MultiplayerModeIndex 253 { 254 get => _multiplayerModeIndex; 255 set 256 { 257 _multiplayerModeIndex = value; 258 ConfigurationState.Instance.Multiplayer.Mode.Value = (MultiplayerMode)_multiplayerModeIndex; 259 } 260 } 261 262 public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() 263 { 264 _virtualFileSystem = virtualFileSystem; 265 _contentManager = contentManager; 266 if (Program.PreviewerDetached) 267 { 268 Task.Run(LoadTimeZones); 269 } 270 } 271 272 public SettingsViewModel() 273 { 274 GameDirectories = new AvaloniaList<string>(); 275 TimeZones = new AvaloniaList<TimeZone>(); 276 AvailableGpus = new ObservableCollection<ComboBoxItem>(); 277 _validTzRegions = new List<string>(); 278 _networkInterfaces = new Dictionary<string, string>(); 279 280 Task.Run(CheckSoundBackends); 281 Task.Run(PopulateNetworkInterfaces); 282 283 if (Program.PreviewerDetached) 284 { 285 Task.Run(LoadAvailableGpus); 286 LoadCurrentConfiguration(); 287 } 288 } 289 290 public async Task CheckSoundBackends() 291 { 292 IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported; 293 IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported; 294 IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported; 295 296 await Dispatcher.UIThread.InvokeAsync(() => 297 { 298 OnPropertyChanged(nameof(IsOpenAlEnabled)); 299 OnPropertyChanged(nameof(IsSoundIoEnabled)); 300 OnPropertyChanged(nameof(IsSDL2Enabled)); 301 }); 302 } 303 304 private async Task LoadAvailableGpus() 305 { 306 AvailableGpus.Clear(); 307 308 var devices = VulkanRenderer.GetPhysicalDevices(); 309 310 if (devices.Length == 0) 311 { 312 IsVulkanAvailable = false; 313 GraphicsBackendIndex = 1; 314 } 315 else 316 { 317 foreach (var device in devices) 318 { 319 await Dispatcher.UIThread.InvokeAsync(() => 320 { 321 _gpuIds.Add(device.Id); 322 323 AvailableGpus.Add(new ComboBoxItem { Content = $"{device.Name} {(device.IsDiscrete ? "(dGPU)" : "")}" }); 324 }); 325 } 326 } 327 328 // GPU configuration needs to be loaded during the async method or it will always return 0. 329 PreferredGpuIndex = _gpuIds.Contains(ConfigurationState.Instance.Graphics.PreferredGpu) ? 330 _gpuIds.IndexOf(ConfigurationState.Instance.Graphics.PreferredGpu) : 0; 331 332 Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(PreferredGpuIndex))); 333 } 334 335 public async Task LoadTimeZones() 336 { 337 _timeZoneContentManager = new TimeZoneContentManager(); 338 339 _timeZoneContentManager.InitializeInstance(_virtualFileSystem, _contentManager, IntegrityCheckLevel.None); 340 341 foreach ((int offset, string location, string abbr) in _timeZoneContentManager.ParseTzOffsets()) 342 { 343 int hours = Math.DivRem(offset, 3600, out int seconds); 344 int minutes = Math.Abs(seconds) / 60; 345 346 string abbr2 = abbr.StartsWith('+') || abbr.StartsWith('-') ? string.Empty : abbr; 347 348 await Dispatcher.UIThread.InvokeAsync(() => 349 { 350 TimeZones.Add(new TimeZone($"UTC{hours:+0#;-0#;+00}:{minutes:D2}", location, abbr2)); 351 352 _validTzRegions.Add(location); 353 }); 354 } 355 356 Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(TimeZone))); 357 } 358 359 private async Task PopulateNetworkInterfaces() 360 { 361 _networkInterfaces.Clear(); 362 _networkInterfaces.Add(LocaleManager.Instance[LocaleKeys.NetworkInterfaceDefault], "0"); 363 364 foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) 365 { 366 await Dispatcher.UIThread.InvokeAsync(() => 367 { 368 _networkInterfaces.Add(networkInterface.Name, networkInterface.Id); 369 }); 370 } 371 372 // Network interface index needs to be loaded during the async method or it will always return 0. 373 NetworkInterfaceIndex = _networkInterfaces.Values.ToList().IndexOf(ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value); 374 375 Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(NetworkInterfaceIndex))); 376 } 377 378 public void ValidateAndSetTimeZone(string location) 379 { 380 if (_validTzRegions.Contains(location)) 381 { 382 TimeZone = location; 383 } 384 } 385 386 public void LoadCurrentConfiguration() 387 { 388 ConfigurationState config = ConfigurationState.Instance; 389 390 // User Interface 391 EnableDiscordIntegration = config.EnableDiscordIntegration; 392 CheckUpdatesOnStart = config.CheckUpdatesOnStart; 393 ShowConfirmExit = config.ShowConfirmExit; 394 RememberWindowState = config.RememberWindowState; 395 HideCursor = (int)config.HideCursor.Value; 396 397 GameDirectories.Clear(); 398 GameDirectories.AddRange(config.UI.GameDirs.Value); 399 400 BaseStyleIndex = config.UI.BaseStyle.Value switch 401 { 402 "Auto" => 0, 403 "Light" => 1, 404 "Dark" => 2, 405 _ => 0 406 }; 407 408 // Input 409 EnableDockedMode = config.System.EnableDockedMode; 410 EnableKeyboard = config.Hid.EnableKeyboard; 411 EnableMouse = config.Hid.EnableMouse; 412 413 // Keyboard Hotkeys 414 KeyboardHotkey = new HotkeyConfig(config.Hid.Hotkeys.Value); 415 416 // System 417 Region = (int)config.System.Region.Value; 418 Language = (int)config.System.Language.Value; 419 TimeZone = config.System.TimeZone; 420 421 DateTime currentHostDateTime = DateTime.Now; 422 TimeSpan systemDateTimeOffset = TimeSpan.FromSeconds(config.System.SystemTimeOffset); 423 DateTime currentDateTime = currentHostDateTime.Add(systemDateTimeOffset); 424 CurrentDate = currentDateTime.Date; 425 CurrentTime = currentDateTime.TimeOfDay; 426 427 EnableVsync = config.Graphics.EnableVsync; 428 EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; 429 ExpandDramSize = config.System.ExpandRam; 430 IgnoreMissingServices = config.System.IgnoreMissingServices; 431 432 // CPU 433 EnablePptc = config.System.EnablePtc; 434 MemoryMode = (int)config.System.MemoryManagerMode.Value; 435 UseHypervisor = config.System.UseHypervisor; 436 437 // Graphics 438 GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value; 439 // Physical devices are queried asynchronously hence the prefered index config value is loaded in LoadAvailableGpus(). 440 EnableShaderCache = config.Graphics.EnableShaderCache; 441 EnableTextureRecompression = config.Graphics.EnableTextureRecompression; 442 EnableMacroHLE = config.Graphics.EnableMacroHLE; 443 EnableColorSpacePassthrough = config.Graphics.EnableColorSpacePassthrough; 444 ResolutionScale = config.Graphics.ResScale == -1 ? 4 : config.Graphics.ResScale - 1; 445 CustomResolutionScale = config.Graphics.ResScaleCustom; 446 MaxAnisotropy = config.Graphics.MaxAnisotropy == -1 ? 0 : (int)(MathF.Log2(config.Graphics.MaxAnisotropy)); 447 AspectRatio = (int)config.Graphics.AspectRatio.Value; 448 GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value; 449 ShaderDumpPath = config.Graphics.ShadersDumpPath; 450 AntiAliasingEffect = (int)config.Graphics.AntiAliasing.Value; 451 ScalingFilter = (int)config.Graphics.ScalingFilter.Value; 452 ScalingFilterLevel = config.Graphics.ScalingFilterLevel.Value; 453 454 // Audio 455 AudioBackend = (int)config.System.AudioBackend.Value; 456 Volume = config.System.AudioVolume * 100; 457 458 // Network 459 EnableInternetAccess = config.System.EnableInternetAccess; 460 // LAN interface index is loaded asynchronously in PopulateNetworkInterfaces() 461 462 // Logging 463 EnableFileLog = config.Logger.EnableFileLog; 464 EnableStub = config.Logger.EnableStub; 465 EnableInfo = config.Logger.EnableInfo; 466 EnableWarn = config.Logger.EnableWarn; 467 EnableError = config.Logger.EnableError; 468 EnableTrace = config.Logger.EnableTrace; 469 EnableGuest = config.Logger.EnableGuest; 470 EnableDebug = config.Logger.EnableDebug; 471 EnableFsAccessLog = config.Logger.EnableFsAccessLog; 472 FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; 473 OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; 474 475 MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value; 476 } 477 478 public void SaveSettings() 479 { 480 ConfigurationState config = ConfigurationState.Instance; 481 482 // User Interface 483 config.EnableDiscordIntegration.Value = EnableDiscordIntegration; 484 config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart; 485 config.ShowConfirmExit.Value = ShowConfirmExit; 486 config.RememberWindowState.Value = RememberWindowState; 487 config.HideCursor.Value = (HideCursorMode)HideCursor; 488 489 if (_directoryChanged) 490 { 491 List<string> gameDirs = new(GameDirectories); 492 config.UI.GameDirs.Value = gameDirs; 493 } 494 495 config.UI.BaseStyle.Value = BaseStyleIndex switch 496 { 497 0 => "Auto", 498 1 => "Light", 499 2 => "Dark", 500 _ => "Auto" 501 }; 502 503 // Input 504 config.System.EnableDockedMode.Value = EnableDockedMode; 505 config.Hid.EnableKeyboard.Value = EnableKeyboard; 506 config.Hid.EnableMouse.Value = EnableMouse; 507 508 // Keyboard Hotkeys 509 config.Hid.Hotkeys.Value = KeyboardHotkey.GetConfig(); 510 511 // System 512 config.System.Region.Value = (Region)Region; 513 config.System.Language.Value = (Language)Language; 514 515 if (_validTzRegions.Contains(TimeZone)) 516 { 517 config.System.TimeZone.Value = TimeZone; 518 } 519 520 config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); 521 config.Graphics.EnableVsync.Value = EnableVsync; 522 config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; 523 config.System.ExpandRam.Value = ExpandDramSize; 524 config.System.IgnoreMissingServices.Value = IgnoreMissingServices; 525 526 // CPU 527 config.System.EnablePtc.Value = EnablePptc; 528 config.System.MemoryManagerMode.Value = (MemoryManagerMode)MemoryMode; 529 config.System.UseHypervisor.Value = UseHypervisor; 530 531 // Graphics 532 config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex; 533 config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex); 534 config.Graphics.EnableShaderCache.Value = EnableShaderCache; 535 config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression; 536 config.Graphics.EnableMacroHLE.Value = EnableMacroHLE; 537 config.Graphics.EnableColorSpacePassthrough.Value = EnableColorSpacePassthrough; 538 config.Graphics.ResScale.Value = ResolutionScale == 4 ? -1 : ResolutionScale + 1; 539 config.Graphics.ResScaleCustom.Value = CustomResolutionScale; 540 config.Graphics.MaxAnisotropy.Value = MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy); 541 config.Graphics.AspectRatio.Value = (AspectRatio)AspectRatio; 542 config.Graphics.AntiAliasing.Value = (AntiAliasing)AntiAliasingEffect; 543 config.Graphics.ScalingFilter.Value = (ScalingFilter)ScalingFilter; 544 config.Graphics.ScalingFilterLevel.Value = ScalingFilterLevel; 545 546 if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex) 547 { 548 DriverUtilities.ToggleOGLThreading(GraphicsBackendMultithreadingIndex == (int)BackendThreading.Off); 549 } 550 551 config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex; 552 config.Graphics.ShadersDumpPath.Value = ShaderDumpPath; 553 554 // Audio 555 AudioBackend audioBackend = (AudioBackend)AudioBackend; 556 if (audioBackend != config.System.AudioBackend.Value) 557 { 558 config.System.AudioBackend.Value = audioBackend; 559 560 Logger.Info?.Print(LogClass.Application, $"AudioBackend toggled to: {audioBackend}"); 561 } 562 563 config.System.AudioVolume.Value = Volume / 100; 564 565 // Network 566 config.System.EnableInternetAccess.Value = EnableInternetAccess; 567 568 // Logging 569 config.Logger.EnableFileLog.Value = EnableFileLog; 570 config.Logger.EnableStub.Value = EnableStub; 571 config.Logger.EnableInfo.Value = EnableInfo; 572 config.Logger.EnableWarn.Value = EnableWarn; 573 config.Logger.EnableError.Value = EnableError; 574 config.Logger.EnableTrace.Value = EnableTrace; 575 config.Logger.EnableGuest.Value = EnableGuest; 576 config.Logger.EnableDebug.Value = EnableDebug; 577 config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog; 578 config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; 579 config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; 580 581 config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]]; 582 config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex; 583 584 config.ToFileFormat().SaveConfig(Program.ConfigurationPath); 585 586 MainWindow.UpdateGraphicsConfig(); 587 588 SaveSettingsEvent?.Invoke(); 589 590 _directoryChanged = false; 591 } 592 593 private static void RevertIfNotSaved() 594 { 595 Program.ReloadConfig(); 596 } 597 598 public void ApplyButton() 599 { 600 SaveSettings(); 601 } 602 603 public void OkButton() 604 { 605 SaveSettings(); 606 CloseWindow?.Invoke(); 607 } 608 609 public void CancelButton() 610 { 611 RevertIfNotSaved(); 612 CloseWindow?.Invoke(); 613 } 614 } 615 }