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 }