diff options
Diffstat (limited to 'src/Ryujinx.Gtk3/Program.cs')
-rw-r--r-- | src/Ryujinx.Gtk3/Program.cs | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/src/Ryujinx.Gtk3/Program.cs b/src/Ryujinx.Gtk3/Program.cs new file mode 100644 index 00000000..1845c512 --- /dev/null +++ b/src/Ryujinx.Gtk3/Program.cs @@ -0,0 +1,378 @@ +using Gtk; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.GraphicsDriver; +using Ryujinx.Common.Logging; +using Ryujinx.Common.SystemInterop; +using Ryujinx.Modules; +using Ryujinx.SDL2.Common; +using Ryujinx.UI; +using Ryujinx.UI.Common; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Helper; +using Ryujinx.UI.Common.SystemInfo; +using Ryujinx.UI.Widgets; +using SixLabors.ImageSharp.Formats.Jpeg; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Ryujinx +{ + partial class Program + { + public static double WindowScaleFactor { get; private set; } + + public static string Version { get; private set; } + + public static string ConfigurationPath { get; set; } + + public static string CommandLineProfile { get; set; } + + private const string X11LibraryName = "libX11"; + + [LibraryImport(X11LibraryName)] + private static partial int XInitThreads(); + + [LibraryImport("user32.dll", SetLastError = true)] + public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type); + + [LibraryImport("libc", SetLastError = true)] + private static partial int setenv([MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string value, int overwrite); + + private const uint MbIconWarning = 0x30; + + static Program() + { + if (OperatingSystem.IsLinux()) + { + NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, assembly, path) => + { + if (name != X11LibraryName) + { + return IntPtr.Zero; + } + + if (!NativeLibrary.TryLoad("libX11.so.6", assembly, path, out IntPtr result)) + { + if (!NativeLibrary.TryLoad("libX11.so", assembly, path, out result)) + { + return IntPtr.Zero; + } + } + + return result; + }); + } + } + + static void Main(string[] args) + { + Version = ReleaseInformation.Version; + + if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134)) + { + MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MbIconWarning); + } + + // Parse arguments + CommandLineState.ParseArguments(args); + + // Hook unhandled exception and process exit events. + GLib.ExceptionManager.UnhandledException += (GLib.UnhandledExceptionArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating); + AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating); + AppDomain.CurrentDomain.ProcessExit += (object sender, EventArgs e) => Exit(); + + // Make process DPI aware for proper window sizing on high-res screens. + ForceDpiAware.Windows(); + WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor(); + + // Delete backup files after updating. + Task.Run(Updater.CleanupUpdate); + + Console.Title = $"Ryujinx Console {Version}"; + + // NOTE: GTK3 doesn't init X11 in a multi threaded way. + // This ends up causing race condition and abort of XCB when a context is created by SPB (even if SPB do call XInitThreads). + if (OperatingSystem.IsLinux()) + { + if (XInitThreads() == 0) + { + throw new NotSupportedException("Failed to initialize multi-threading support."); + } + + Environment.SetEnvironmentVariable("GDK_BACKEND", "x11"); + setenv("GDK_BACKEND", "x11", 1); + } + + if (OperatingSystem.IsMacOS()) + { + string baseDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory); + string resourcesDataDir; + + if (Path.GetFileName(baseDirectory) == "MacOS") + { + resourcesDataDir = Path.Combine(Directory.GetParent(baseDirectory).FullName, "Resources"); + } + else + { + resourcesDataDir = baseDirectory; + } + + static void SetEnvironmentVariableNoCaching(string key, string value) + { + int res = setenv(key, value, 1); + Debug.Assert(res != -1); + } + + // On macOS, GTK3 needs XDG_DATA_DIRS to be set, otherwise it will try searching for "gschemas.compiled" in system directories. + SetEnvironmentVariableNoCaching("XDG_DATA_DIRS", Path.Combine(resourcesDataDir, "share")); + + // On macOS, GTK3 needs GDK_PIXBUF_MODULE_FILE to be set, otherwise it will try searching for "loaders.cache" in system directories. + SetEnvironmentVariableNoCaching("GDK_PIXBUF_MODULE_FILE", Path.Combine(resourcesDataDir, "lib", "gdk-pixbuf-2.0", "2.10.0", "loaders.cache")); + + SetEnvironmentVariableNoCaching("GTK_IM_MODULE_FILE", Path.Combine(resourcesDataDir, "lib", "gtk-3.0", "3.0.0", "immodules.cache")); + } + + string systemPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine); + Environment.SetEnvironmentVariable("Path", $"{Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")};{systemPath}"); + + // Setup base data directory. + AppDataManager.Initialize(CommandLineState.BaseDirPathArg); + + // Initialize the configuration. + ConfigurationState.Initialize(); + + // Initialize the logger system. + LoggerModule.Initialize(); + + // Initialize Discord integration. + DiscordIntegrationModule.Initialize(); + + // Initialize SDL2 driver + SDL2Driver.MainThreadDispatcher = action => + { + Application.Invoke(delegate + { + action(); + }); + }; + + // Sets ImageSharp Jpeg Encoder Quality. + SixLabors.ImageSharp.Configuration.Default.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder() + { + Quality = 100, + }); + + string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName); + string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName); + + // Now load the configuration as the other subsystems are now registered + ConfigurationPath = File.Exists(localConfigurationPath) + ? localConfigurationPath + : File.Exists(appDataConfigurationPath) + ? appDataConfigurationPath + : null; + + if (ConfigurationPath == null) + { + // No configuration, we load the default values and save it to disk + ConfigurationPath = appDataConfigurationPath; + + ConfigurationState.Instance.LoadDefault(); + ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath); + } + else + { + if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat)) + { + ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath); + } + else + { + ConfigurationState.Instance.LoadDefault(); + + Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}"); + } + } + + // Check if graphics backend was overridden. + if (CommandLineState.OverrideGraphicsBackend != null) + { + if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl") + { + ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl; + } + else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan") + { + ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan; + } + } + + // Check if HideCursor was overridden. + if (CommandLineState.OverrideHideCursor is not null) + { + ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch + { + "never" => HideCursorMode.Never, + "onidle" => HideCursorMode.OnIdle, + "always" => HideCursorMode.Always, + _ => ConfigurationState.Instance.HideCursor.Value, + }; + } + + // Check if docked mode was overridden. + if (CommandLineState.OverrideDockedMode.HasValue) + { + ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value; + } + + // Logging system information. + PrintSystemInfo(); + + // Enable OGL multithreading on the driver, when available. + BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading; + DriverUtilities.ToggleOGLThreading(threadingMode == BackendThreading.Off); + + // Initialize Gtk. + Application.Init(); + + // Check if keys exists. + bool hasSystemProdKeys = File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")); + bool hasCommonProdKeys = AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys")); + if (!hasSystemProdKeys && !hasCommonProdKeys) + { + UserErrorDialog.CreateUserErrorDialog(UserError.NoKeys); + } + + // Show the main window UI. + MainWindow mainWindow = new(); + mainWindow.Show(); + + if (OperatingSystem.IsLinux()) + { + int currentVmMaxMapCount = LinuxHelper.VmMaxMapCount; + + if (LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount) + { + Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({currentVmMaxMapCount})"); + + if (LinuxHelper.PkExecPath is not null) + { + var buttonTexts = new Dictionary<int, string>() + { + { 0, "Yes, until the next restart" }, + { 1, "Yes, permanently" }, + { 2, "No" }, + }; + + ResponseType response = GtkDialog.CreateCustomDialog( + "Ryujinx - Low limit for memory mappings detected", + $"Would you like to increase the value of vm.max_map_count to {LinuxHelper.RecommendedVmMaxMapCount}?", + "Some games might try to create more memory mappings than currently allowed. " + + "Ryujinx will crash as soon as this limit gets exceeded.", + buttonTexts, + MessageType.Question); + + int rc; + + switch ((int)response) + { + case 0: + 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 1: + 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; + } + } + else + { + GtkDialog.CreateWarningDialog( + "Max amount of memory mappings is lower than recommended.", + $"The current value of vm.max_map_count ({currentVmMaxMapCount}) is lower than {LinuxHelper.RecommendedVmMaxMapCount}." + + "Some games might try to create more memory mappings than currently allowed. " + + "Ryujinx will crash as soon as this limit gets exceeded.\n\n" + + "You might want to either manually increase the limit or install pkexec, which allows Ryujinx to assist with that."); + } + } + } + + if (CommandLineState.LaunchPathArg != null) + { + mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg); + } + + if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false)) + { + Updater.BeginParse(mainWindow, false).ContinueWith(task => + { + Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"); + }, TaskContinuationOptions.OnlyOnFaulted); + } + + Application.Run(); + } + + private static void PrintSystemInfo() + { + Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}"); + SystemInfo.Gather().Print(); + + var enabledLogs = Logger.GetEnabledLevels(); + Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogs.Count == 0 ? "<None>" : string.Join(", ", enabledLogs))}"); + + if (AppDataManager.Mode == AppDataManager.LaunchMode.Custom) + { + Logger.Notice.Print(LogClass.Application, $"Launch Mode: Custom Path {AppDataManager.BaseDirPath}"); + } + else + { + Logger.Notice.Print(LogClass.Application, $"Launch Mode: {AppDataManager.Mode}"); + } + } + + private static void ProcessUnhandledException(Exception ex, bool isTerminating) + { + string message = $"Unhandled exception caught: {ex}"; + + Logger.Error?.PrintMsg(LogClass.Application, message); + + if (Logger.Error == null) + { + Logger.Notice.PrintMsg(LogClass.Application, message); + } + + if (isTerminating) + { + Exit(); + } + } + + public static void Exit() + { + DiscordIntegrationModule.Exit(); + + Logger.Shutdown(); + } + } +} |