/ src / Ryujinx.Gtk3 / Program.cs
Program.cs
  1  using Gtk;
  2  using Ryujinx.Common;
  3  using Ryujinx.Common.Configuration;
  4  using Ryujinx.Common.GraphicsDriver;
  5  using Ryujinx.Common.Logging;
  6  using Ryujinx.Common.SystemInterop;
  7  using Ryujinx.Common.Utilities;
  8  using Ryujinx.Graphics.Vulkan.MoltenVK;
  9  using Ryujinx.Modules;
 10  using Ryujinx.SDL2.Common;
 11  using Ryujinx.UI;
 12  using Ryujinx.UI.App.Common;
 13  using Ryujinx.UI.Common;
 14  using Ryujinx.UI.Common.Configuration;
 15  using Ryujinx.UI.Common.Helper;
 16  using Ryujinx.UI.Common.SystemInfo;
 17  using Ryujinx.UI.Widgets;
 18  using System;
 19  using System.Collections.Generic;
 20  using System.Diagnostics;
 21  using System.IO;
 22  using System.Runtime.InteropServices;
 23  using System.Threading.Tasks;
 24  
 25  namespace Ryujinx
 26  {
 27      partial class Program
 28      {
 29          public static double WindowScaleFactor { get; private set; }
 30  
 31          public static string Version { get; private set; }
 32  
 33          public static string ConfigurationPath { get; set; }
 34  
 35          public static string CommandLineProfile { get; set; }
 36  
 37          private const string X11LibraryName = "libX11";
 38  
 39          [LibraryImport(X11LibraryName)]
 40          private static partial int XInitThreads();
 41  
 42          [LibraryImport("user32.dll", SetLastError = true)]
 43          public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);
 44  
 45          private const uint MbIconWarning = 0x30;
 46  
 47          static Program()
 48          {
 49              if (OperatingSystem.IsLinux())
 50              {
 51                  NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, assembly, path) =>
 52                  {
 53                      if (name != X11LibraryName)
 54                      {
 55                          return IntPtr.Zero;
 56                      }
 57  
 58                      if (!NativeLibrary.TryLoad("libX11.so.6", assembly, path, out IntPtr result))
 59                      {
 60                          if (!NativeLibrary.TryLoad("libX11.so", assembly, path, out result))
 61                          {
 62                              return IntPtr.Zero;
 63                          }
 64                      }
 65  
 66                      return result;
 67                  });
 68              }
 69          }
 70  
 71          static void Main(string[] args)
 72          {
 73              Version = ReleaseInformation.Version;
 74  
 75              if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
 76              {
 77                  MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 1803 and newer.\n", $"Ryujinx {Version}", MbIconWarning);
 78              }
 79  
 80              // Parse arguments
 81              CommandLineState.ParseArguments(args);
 82  
 83              // Hook unhandled exception and process exit events.
 84              GLib.ExceptionManager.UnhandledException += (GLib.UnhandledExceptionArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating);
 85              AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating);
 86              AppDomain.CurrentDomain.ProcessExit += (object sender, EventArgs e) => Exit();
 87  
 88              // Make process DPI aware for proper window sizing on high-res screens.
 89              ForceDpiAware.Windows();
 90              WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
 91  
 92              // Delete backup files after updating.
 93              Task.Run(Updater.CleanupUpdate);
 94  
 95              Console.Title = $"Ryujinx Console {Version}";
 96  
 97              // NOTE: GTK3 doesn't init X11 in a multi threaded way.
 98              // This ends up causing race condition and abort of XCB when a context is created by SPB (even if SPB do call XInitThreads).
 99              if (OperatingSystem.IsLinux())
100              {
101                  if (XInitThreads() == 0)
102                  {
103                      throw new NotSupportedException("Failed to initialize multi-threading support.");
104                  }
105  
106                  OsUtils.SetEnvironmentVariableNoCaching("GDK_BACKEND", "x11");
107              }
108  
109              if (OperatingSystem.IsMacOS())
110              {
111                  MVKInitialization.InitializeResolver();
112  
113                  string baseDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
114                  string resourcesDataDir;
115  
116                  if (Path.GetFileName(baseDirectory) == "MacOS")
117                  {
118                      resourcesDataDir = Path.Combine(Directory.GetParent(baseDirectory).FullName, "Resources");
119                  }
120                  else
121                  {
122                      resourcesDataDir = baseDirectory;
123                  }
124  
125                  // On macOS, GTK3 needs XDG_DATA_DIRS to be set, otherwise it will try searching for "gschemas.compiled" in system directories.
126                  OsUtils.SetEnvironmentVariableNoCaching("XDG_DATA_DIRS", Path.Combine(resourcesDataDir, "share"));
127  
128                  // On macOS, GTK3 needs GDK_PIXBUF_MODULE_FILE to be set, otherwise it will try searching for "loaders.cache" in system directories.
129                  OsUtils.SetEnvironmentVariableNoCaching("GDK_PIXBUF_MODULE_FILE", Path.Combine(resourcesDataDir, "lib", "gdk-pixbuf-2.0", "2.10.0", "loaders.cache"));
130  
131                  OsUtils.SetEnvironmentVariableNoCaching("GTK_IM_MODULE_FILE", Path.Combine(resourcesDataDir, "lib", "gtk-3.0", "3.0.0", "immodules.cache"));
132              }
133  
134              string systemPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine);
135              Environment.SetEnvironmentVariable("Path", $"{Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")};{systemPath}");
136  
137              // Setup base data directory.
138              AppDataManager.Initialize(CommandLineState.BaseDirPathArg);
139  
140              // Initialize the configuration.
141              ConfigurationState.Initialize();
142  
143              // Initialize the logger system.
144              LoggerModule.Initialize();
145  
146              // Initialize Discord integration.
147              DiscordIntegrationModule.Initialize();
148  
149              // Initialize SDL2 driver
150              SDL2Driver.MainThreadDispatcher = action =>
151              {
152                  Application.Invoke(delegate
153                  {
154                      action();
155                  });
156              };
157  
158              string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
159              string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
160  
161              // Now load the configuration as the other subsystems are now registered
162              ConfigurationPath = File.Exists(localConfigurationPath)
163                  ? localConfigurationPath
164                  : File.Exists(appDataConfigurationPath)
165                      ? appDataConfigurationPath
166                      : null;
167  
168              if (ConfigurationPath == null)
169              {
170                  // No configuration, we load the default values and save it to disk
171                  ConfigurationPath = appDataConfigurationPath;
172                  Logger.Notice.Print(LogClass.Application, $"No configuration file found. Saving default configuration to: {ConfigurationPath}");
173  
174                  ConfigurationState.Instance.LoadDefault();
175                  ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
176              }
177              else
178              {
179                  Logger.Notice.Print(LogClass.Application, $"Loading configuration from: {ConfigurationPath}");
180  
181                  if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat))
182                  {
183                      ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
184                  }
185                  else
186                  {
187                      Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location: {ConfigurationPath}");
188  
189                      ConfigurationState.Instance.LoadDefault();
190                  }
191              }
192  
193              // Check if graphics backend was overridden.
194              if (CommandLineState.OverrideGraphicsBackend != null)
195              {
196                  if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
197                  {
198                      ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl;
199                  }
200                  else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan")
201                  {
202                      ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan;
203                  }
204              }
205  
206              // Check if HideCursor was overridden.
207              if (CommandLineState.OverrideHideCursor is not null)
208              {
209                  ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch
210                  {
211                      "never" => HideCursorMode.Never,
212                      "onidle" => HideCursorMode.OnIdle,
213                      "always" => HideCursorMode.Always,
214                      _ => ConfigurationState.Instance.HideCursor.Value,
215                  };
216              }
217  
218              // Check if docked mode was overridden.
219              if (CommandLineState.OverrideDockedMode.HasValue)
220              {
221                  ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;
222              }
223  
224              // Logging system information.
225              PrintSystemInfo();
226  
227              // Enable OGL multithreading on the driver, and some other flags.
228              BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading;
229              DriverUtilities.InitDriverConfig(threadingMode == BackendThreading.Off);
230  
231              // Initialize Gtk.
232              Application.Init();
233  
234              // Check if keys exists.
235              bool hasSystemProdKeys = File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys"));
236              bool hasCommonProdKeys = AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"));
237              if (!hasSystemProdKeys && !hasCommonProdKeys)
238              {
239                  UserErrorDialog.CreateUserErrorDialog(UserError.NoKeys);
240              }
241  
242              // Show the main window UI.
243              MainWindow mainWindow = new();
244              mainWindow.Show();
245  
246              // Load the game table if no application was requested by the command line
247              if (CommandLineState.LaunchPathArg == null)
248              {
249                  mainWindow.UpdateGameTable();
250              }
251  
252              if (OperatingSystem.IsLinux())
253              {
254                  int currentVmMaxMapCount = LinuxHelper.VmMaxMapCount;
255  
256                  if (LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount)
257                  {
258                      Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({currentVmMaxMapCount})");
259  
260                      if (LinuxHelper.PkExecPath is not null)
261                      {
262                          var buttonTexts = new Dictionary<int, string>()
263                          {
264                              { 0, "Yes, until the next restart" },
265                              { 1, "Yes, permanently" },
266                              { 2, "No" },
267                          };
268  
269                          ResponseType response = GtkDialog.CreateCustomDialog(
270                              "Ryujinx - Low limit for memory mappings detected",
271                              $"Would you like to increase the value of vm.max_map_count to {LinuxHelper.RecommendedVmMaxMapCount}?",
272                              "Some games might try to create more memory mappings than currently allowed. " +
273                              "Ryujinx will crash as soon as this limit gets exceeded.",
274                              buttonTexts,
275                              MessageType.Question);
276  
277                          int rc;
278  
279                          switch ((int)response)
280                          {
281                              case 0:
282                                  rc = LinuxHelper.RunPkExec($"echo {LinuxHelper.RecommendedVmMaxMapCount} > {LinuxHelper.VmMaxMapCountPath}");
283                                  if (rc == 0)
284                                  {
285                                      Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount} until the next restart.");
286                                  }
287                                  else
288                                  {
289                                      Logger.Error?.Print(LogClass.Application, $"Unable to change vm.max_map_count. Process exited with code: {rc}");
290                                  }
291                                  break;
292                              case 1:
293                                  rc = LinuxHelper.RunPkExec($"echo \"vm.max_map_count = {LinuxHelper.RecommendedVmMaxMapCount}\" > {LinuxHelper.SysCtlConfigPath} && sysctl -p {LinuxHelper.SysCtlConfigPath}");
294                                  if (rc == 0)
295                                  {
296                                      Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount}. Written to config: {LinuxHelper.SysCtlConfigPath}");
297                                  }
298                                  else
299                                  {
300                                      Logger.Error?.Print(LogClass.Application, $"Unable to write new value for vm.max_map_count to config. Process exited with code: {rc}");
301                                  }
302                                  break;
303                          }
304                      }
305                      else
306                      {
307                          GtkDialog.CreateWarningDialog(
308                              "Max amount of memory mappings is lower than recommended.",
309                              $"The current value of vm.max_map_count ({currentVmMaxMapCount}) is lower than {LinuxHelper.RecommendedVmMaxMapCount}." +
310                              "Some games might try to create more memory mappings than currently allowed. " +
311                              "Ryujinx will crash as soon as this limit gets exceeded.\n\n" +
312                              "You might want to either manually increase the limit or install pkexec, which allows Ryujinx to assist with that.");
313                      }
314                  }
315              }
316  
317              if (CommandLineState.LaunchPathArg != null)
318              {
319                  if (mainWindow.ApplicationLibrary.TryGetApplicationsFromFile(CommandLineState.LaunchPathArg, out List<ApplicationData> applications))
320                  {
321                      ApplicationData applicationData;
322  
323                      if (CommandLineState.LaunchApplicationId != null)
324                      {
325                          applicationData = applications.Find(application => application.IdString == CommandLineState.LaunchApplicationId);
326  
327                          if (applicationData != null)
328                          {
329                              mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg);
330                          }
331                          else
332                          {
333                              Logger.Error?.Print(LogClass.Application, $"Couldn't find requested application id '{CommandLineState.LaunchApplicationId}' in '{CommandLineState.LaunchPathArg}'.");
334                              UserErrorDialog.CreateUserErrorDialog(UserError.ApplicationNotFound);
335                          }
336                      }
337                      else
338                      {
339                          applicationData = applications[0];
340                          mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg);
341                      }
342                  }
343                  else
344                  {
345                      Logger.Error?.Print(LogClass.Application, $"Couldn't find any application in '{CommandLineState.LaunchPathArg}'.");
346                      UserErrorDialog.CreateUserErrorDialog(UserError.ApplicationNotFound);
347                  }
348              }
349  
350              if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))
351              {
352                  Updater.BeginParse(mainWindow, false).ContinueWith(task =>
353                  {
354                      Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}");
355                  }, TaskContinuationOptions.OnlyOnFaulted);
356              }
357  
358              Application.Run();
359          }
360  
361          private static void PrintSystemInfo()
362          {
363              Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}");
364              SystemInfo.Gather().Print();
365  
366              var enabledLogs = Logger.GetEnabledLevels();
367              Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogs.Count == 0 ? "<None>" : string.Join(", ", enabledLogs))}");
368  
369              if (AppDataManager.Mode == AppDataManager.LaunchMode.Custom)
370              {
371                  Logger.Notice.Print(LogClass.Application, $"Launch Mode: Custom Path {AppDataManager.BaseDirPath}");
372              }
373              else
374              {
375                  Logger.Notice.Print(LogClass.Application, $"Launch Mode: {AppDataManager.Mode}");
376              }
377          }
378  
379          private static void ProcessUnhandledException(Exception ex, bool isTerminating)
380          {
381              string message = $"Unhandled exception caught: {ex}";
382  
383              Logger.Error?.PrintMsg(LogClass.Application, message);
384  
385              if (Logger.Error == null)
386              {
387                  Logger.Notice.PrintMsg(LogClass.Application, message);
388              }
389  
390              if (isTerminating)
391              {
392                  Exit();
393              }
394          }
395  
396          public static void Exit()
397          {
398              DiscordIntegrationModule.Exit();
399  
400              Logger.Shutdown();
401          }
402      }
403  }