using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Threading; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Applet; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Common.Logging; using Ryujinx.Graphics.Gpu; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.Input.HLE; using Ryujinx.Input.SDL2; using Ryujinx.Modules; using Ryujinx.UI.App.Common; using Ryujinx.UI.Common; using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Helper; using System; using System.IO; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Windows { public partial class MainWindow : StyleableWindow { internal static MainWindowViewModel MainWindowViewModel { get; private set; } private bool _isLoading; private UserChannelPersistence _userChannelPersistence; private static bool _deferLoad; private static string _launchPath; private static bool _startFullscreen; internal readonly AvaHostUIHandler UiHandler; public VirtualFileSystem VirtualFileSystem { get; private set; } public ContentManager ContentManager { get; private set; } public AccountManager AccountManager { get; private set; } public LibHacHorizonManager LibHacHorizonManager { get; private set; } public InputManager InputManager { get; private set; } internal MainWindowViewModel ViewModel { get; private set; } public SettingsWindow SettingsWindow { get; set; } public static bool ShowKeyErrorOnLoad { get; set; } public ApplicationLibrary ApplicationLibrary { get; set; } public MainWindow() { ViewModel = new MainWindowViewModel(); MainWindowViewModel = ViewModel; DataContext = ViewModel; SetWindowSizePosition(); InitializeComponent(); Load(); UiHandler = new AvaHostUIHandler(this); ViewModel.Title = $"Ryujinx {Program.Version}"; // NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point. double barHeight = MenuBar.MinHeight + StatusBarView.StatusBar.MinHeight; Height = ((Height - barHeight) / Program.WindowScaleFactor) + barHeight; Width /= Program.WindowScaleFactor; if (Program.PreviewerDetached) { InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver()); this.GetObservable(IsActiveProperty).Subscribe(IsActiveChanged); this.ScalingChanged += OnScalingChanged; } } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); NotificationHelper.SetNotificationManager(this); } private void IsActiveChanged(bool obj) { ViewModel.IsActive = obj; } private void OnScalingChanged(object sender, EventArgs e) { Program.DesktopScaleFactor = this.RenderScaling; } private void ApplicationLibrary_ApplicationAdded(object sender, ApplicationAddedEventArgs e) { Dispatcher.UIThread.Post(() => { ViewModel.Applications.Add(e.AppData); }); } private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e) { LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, e.NumAppsLoaded, e.NumAppsFound); Dispatcher.UIThread.Post(() => { ViewModel.StatusBarProgressValue = e.NumAppsLoaded; ViewModel.StatusBarProgressMaximum = e.NumAppsFound; if (e.NumAppsFound == 0) { StatusBarView.LoadProgressBar.IsVisible = false; } if (e.NumAppsLoaded == e.NumAppsFound) { StatusBarView.LoadProgressBar.IsVisible = false; } }); } public void Application_Opened(object sender, ApplicationOpenedEventArgs args) { if (args.Application != null) { ViewModel.SelectedIcon = args.Application.Icon; string path = new FileInfo(args.Application.Path).FullName; ViewModel.LoadApplication(path).Wait(); } args.Handled = true; } internal static void DeferLoadApplication(string launchPathArg, bool startFullscreenArg) { _deferLoad = true; _launchPath = launchPathArg; _startFullscreen = startFullscreenArg; } public void SwitchToGameControl(bool startFullscreen = false) { ViewModel.ShowLoadProgress = false; ViewModel.ShowContent = true; ViewModel.IsLoadingIndeterminate = false; if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen) { ViewModel.ToggleFullscreen(); } } public void ShowLoading(bool startFullscreen = false) { ViewModel.ShowContent = false; ViewModel.ShowLoadProgress = true; ViewModel.IsLoadingIndeterminate = true; if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen) { ViewModel.ToggleFullscreen(); } } private void Initialize() { _userChannelPersistence = new UserChannelPersistence(); VirtualFileSystem = VirtualFileSystem.CreateInstance(); LibHacHorizonManager = new LibHacHorizonManager(); ContentManager = new ContentManager(VirtualFileSystem); LibHacHorizonManager.InitializeFsServer(VirtualFileSystem); LibHacHorizonManager.InitializeArpServer(); LibHacHorizonManager.InitializeBcatServer(); LibHacHorizonManager.InitializeSystemClients(); ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem); // Save data created before we supported extra data in directory save data will not work properly if // given empty extra data. Luckily some of that extra data can be created using the data from the // save data indexer, which should be enough to check access permissions for user saves. // Every single save data's extra data will be checked and fixed if needed each time the emulator is opened. // Consider removing this at some point in the future when we don't need to worry about old saves. VirtualFileSystem.FixExtraData(LibHacHorizonManager.RyujinxClient); AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient, CommandLineState.Profile); VirtualFileSystem.ReloadKeySet(); ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient); } [SupportedOSPlatform("linux")] private static async Task ShowVmMaxMapCountWarning() { LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary, LinuxHelper.VmMaxMapCount, LinuxHelper.RecommendedVmMaxMapCount); await ContentDialogHelper.CreateWarningDialog( LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountWarningTextPrimary], LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountWarningTextSecondary] ); } [SupportedOSPlatform("linux")] private static async Task ShowVmMaxMapCountDialog() { LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary, LinuxHelper.RecommendedVmMaxMapCount); UserResult response = await ContentDialogHelper.ShowTextDialog( $"Ryujinx - {LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTitle]}", LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextPrimary], LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogTextSecondary], LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonUntilRestart], LocaleManager.Instance[LocaleKeys.LinuxVmMaxMapCountDialogButtonPersistent], LocaleManager.Instance[LocaleKeys.InputDialogNo], (int)Symbol.Help ); int rc; switch (response) { case UserResult.Ok: rc = LinuxHelper.RunPkExec($"echo {LinuxHelper.RecommendedVmMaxMapCount} > {LinuxHelper.VmMaxMapCountPath}"); if (rc == 0) { Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount} until the next restart."); } else { Logger.Error?.Print(LogClass.Application, $"Unable to change vm.max_map_count. Process exited with code: {rc}"); } break; case UserResult.No: rc = LinuxHelper.RunPkExec($"echo \"vm.max_map_count = {LinuxHelper.RecommendedVmMaxMapCount}\" > {LinuxHelper.SysCtlConfigPath} && sysctl -p {LinuxHelper.SysCtlConfigPath}"); if (rc == 0) { Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount}. Written to config: {LinuxHelper.SysCtlConfigPath}"); } else { Logger.Error?.Print(LogClass.Application, $"Unable to write new value for vm.max_map_count to config. Process exited with code: {rc}"); } break; } } private async Task CheckLaunchState() { if (OperatingSystem.IsLinux() && LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount) { Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({LinuxHelper.VmMaxMapCount})"); if (LinuxHelper.PkExecPath is not null) { await Dispatcher.UIThread.InvokeAsync(ShowVmMaxMapCountDialog); } else { await Dispatcher.UIThread.InvokeAsync(ShowVmMaxMapCountWarning); } } if (!ShowKeyErrorOnLoad) { if (_deferLoad) { _deferLoad = false; ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait(); } } else { ShowKeyErrorOnLoad = false; await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys)); } if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false)) { await Updater.BeginParse(this, false).ContinueWith(task => { Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"); }, TaskContinuationOptions.OnlyOnFaulted); } } private void Load() { StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged; ApplicationGrid.ApplicationOpened += Application_Opened; ApplicationGrid.DataContext = ViewModel; ApplicationList.ApplicationOpened += Application_Opened; ApplicationList.DataContext = ViewModel; } private void SetWindowSizePosition() { PixelPoint savedPoint = new(ConfigurationState.Instance.UI.WindowStartup.WindowPositionX, ConfigurationState.Instance.UI.WindowStartup.WindowPositionY); ViewModel.WindowHeight = ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight * Program.WindowScaleFactor; ViewModel.WindowWidth = ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth * Program.WindowScaleFactor; ViewModel.WindowState = ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value ? WindowState.Maximized : WindowState.Normal; if (CheckScreenBounds(savedPoint)) { Position = savedPoint; } else { WindowStartupLocation = WindowStartupLocation.CenterScreen; } } private bool CheckScreenBounds(PixelPoint configPoint) { for (int i = 0; i < Screens.ScreenCount; i++) { if (Screens.All[i].Bounds.Contains(configPoint)) { return true; } } Logger.Warning?.Print(LogClass.Application, "Failed to find valid start-up coordinates. Defaulting to primary monitor center."); return false; } private void SaveWindowSizePosition() { ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)Height; ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth.Value = (int)Width; ConfigurationState.Instance.UI.WindowStartup.WindowPositionX.Value = Position.X; ConfigurationState.Instance.UI.WindowStartup.WindowPositionY.Value = Position.Y; ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value = WindowState == WindowState.Maximized; MainWindowViewModel.SaveConfig(); } protected override void OnOpened(EventArgs e) { base.OnOpened(e); Initialize(); ViewModel.Initialize( ContentManager, StorageProvider, ApplicationLibrary, VirtualFileSystem, AccountManager, InputManager, _userChannelPersistence, LibHacHorizonManager, UiHandler, ShowLoading, SwitchToGameControl, SetMainContent, this); ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated; ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded; ViewModel.RefreshFirmwareStatus(); LoadApplications(); #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed CheckLaunchState(); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } private void SetMainContent(Control content = null) { content ??= GameLibrary; if (MainContent.Content != content) { MainContent.Content = content; } } public static void UpdateGraphicsConfig() { #pragma warning disable IDE0055 // Disable formatting GraphicsConfig.ResScale = ConfigurationState.Instance.Graphics.ResScale == -1 ? ConfigurationState.Instance.Graphics.ResScaleCustom : ConfigurationState.Instance.Graphics.ResScale; GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy; GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath; GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache; GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression; GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE; #pragma warning restore IDE0055 } private void VolumeStatus_CheckedChanged(object sender, RoutedEventArgs e) { var volumeSplitButton = sender as ToggleSplitButton; if (ViewModel.IsGameRunning) { if (!volumeSplitButton.IsChecked) { ViewModel.AppHost.Device.SetVolume(ViewModel.VolumeBeforeMute); } else { ViewModel.VolumeBeforeMute = ViewModel.AppHost.Device.GetVolume(); ViewModel.AppHost.Device.SetVolume(0); } ViewModel.Volume = ViewModel.AppHost.Device.GetVolume(); } } protected override void OnClosing(WindowClosingEventArgs e) { if (!ViewModel.IsClosing && ViewModel.AppHost != null && ConfigurationState.Instance.ShowConfirmExit) { e.Cancel = true; ConfirmExit(); return; } ViewModel.IsClosing = true; if (ViewModel.AppHost != null) { ViewModel.AppHost.AppExit -= ViewModel.AppHost_AppExit; ViewModel.AppHost.AppExit += (sender, e) => { ViewModel.AppHost = null; Dispatcher.UIThread.Post(() => { MainContent = null; Close(); }); }; ViewModel.AppHost?.Stop(); e.Cancel = true; return; } SaveWindowSizePosition(); ApplicationLibrary.CancelLoading(); InputManager.Dispose(); Program.Exit(); base.OnClosing(e); } private void ConfirmExit() { Dispatcher.UIThread.InvokeAsync(async () => { ViewModel.IsClosing = await ContentDialogHelper.CreateExitDialog(); if (ViewModel.IsClosing) { Close(); } }); } public void LoadApplications() { ViewModel.Applications.Clear(); StatusBarView.LoadProgressBar.IsVisible = true; ViewModel.StatusBarProgressMaximum = 0; ViewModel.StatusBarProgressValue = 0; LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0); ReloadGameList(); } public void ToggleFileType(string fileType) { _ = fileType switch { #pragma warning disable IDE0055 // Disable formatting "NSP" => ConfigurationState.Instance.UI.ShownFileTypes.NSP.Value = !ConfigurationState.Instance.UI.ShownFileTypes.NSP, "PFS0" => ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Value = !ConfigurationState.Instance.UI.ShownFileTypes.PFS0, "XCI" => ConfigurationState.Instance.UI.ShownFileTypes.XCI.Value = !ConfigurationState.Instance.UI.ShownFileTypes.XCI, "NCA" => ConfigurationState.Instance.UI.ShownFileTypes.NCA.Value = !ConfigurationState.Instance.UI.ShownFileTypes.NCA, "NRO" => ConfigurationState.Instance.UI.ShownFileTypes.NRO.Value = !ConfigurationState.Instance.UI.ShownFileTypes.NRO, "NSO" => ConfigurationState.Instance.UI.ShownFileTypes.NSO.Value = !ConfigurationState.Instance.UI.ShownFileTypes.NSO, _ => throw new ArgumentOutOfRangeException(fileType), #pragma warning restore IDE0055 }; ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); LoadApplications(); } private void ReloadGameList() { if (_isLoading) { return; } _isLoading = true; Thread applicationLibraryThread = new(() => { ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs, ConfigurationState.Instance.System.Language); _isLoading = false; }) { Name = "GUI.ApplicationLibraryThread", IsBackground = true, }; applicationLibraryThread.Start(); } } }