using Avalonia; using Avalonia.Threading; using DiscordRPC; using Gommon; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; using Projektanker.Icons.Avalonia.MaterialDesign; using Ryujinx.Ava.Systems; using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.Systems.Configuration.System; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities.SystemInfo; using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; using Ryujinx.Common.SystemInterop; using Ryujinx.Common.Utilities; using Ryujinx.Graphics.Vulkan.MoltenVK; using Ryujinx.Headless; using Ryujinx.SDL3.Common; using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Security.Principal; using System.Threading.Tasks; namespace Ryujinx.Ava { internal static class Program { public static double WindowScaleFactor { get; set; } public static double DesktopScaleFactor { get; set; } = 1.0; public static string Version { get; private set; } public static string ConfigurationPath { get; private set; } public static string GlobalConfigurationPath { get; private set; } public static bool UseExtraConfig { get; set; } public static bool PreviewerDetached { get; private set; } public static bool UseHardwareAcceleration { get; private set; } public static string BackendThreadingArg { get; private set; } private const uint MbIconwarning = 0x30; public static int Main(string[] args) { Version = ReleaseInformation.Version; if (OperatingSystem.IsWindows()) { if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041)) { _ = Win32NativeInterop.MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", $"Ryujinx {Version}", MbIconwarning); return 0; } string onedriveFiles = Environment.GetEnvironmentVariable("Onedrive"); string onedriveConsumerFiles = Environment.GetEnvironmentVariable("OnedriveConsumer"); string onedriveCommercialFiles = Environment.GetEnvironmentVariable("OnedriveCommercial"); if (Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveFiles) || Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveConsumerFiles) || Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveCommercialFiles)) { _ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run from a OneDrive folder. Please move it out and relaunch.", $"Ryujinx {Version}", MbIconwarning); return 0; } string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); if (Environment.CurrentDirectory.StartsWithIgnoreCase(programFiles) || Environment.CurrentDirectory.StartsWithIgnoreCase(programFilesX86)) { _ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", $"Ryujinx {Version}", MbIconwarning); return 0; } // The names of everything here makes no sense for what this actually checks for. Thanks, Microsoft. // If you can't tell by the error string, // this actually checks if the current process was run with "Run as Administrator" // ...but this reads like it checks if the current is in/has the Windows admin role? lol if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) { _ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run as administrator.", $"Ryujinx {Version}", MbIconwarning); return 0; } } else { // Unix [DllImport("libc")] static extern uint geteuid(); bool root = geteuid().Equals(0); if (OperatingSystem.IsMacOS()) { // If you add a check here, please, for the love of god, check it BEFORE loading the dialog. if (root) { Console.WriteLine($"Ryujinx {Version}: Ryujinx is not intended to be run as administrator. Exiting..."); // Grab what we need to make the message box. const string ObjCRuntime = "/usr/lib/libobjc.A.dylib"; const string FoundationFramework = "/System/Library/Frameworks/Foundation.framework/Foundation"; const string AppKitFramework = "/System/Library/Frameworks/AppKit.framework/AppKit"; [DllImport(ObjCRuntime, EntryPoint = "sel_registerName")] static extern IntPtr GetSelector(string name); [DllImport(ObjCRuntime, EntryPoint = "objc_getClass")] static extern IntPtr GetClass(string name); [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] static extern IntPtr SendMessage(IntPtr target, IntPtr selector); [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] static extern IntPtr SendMessageWithParameter(IntPtr target, IntPtr selector, IntPtr param); [DllImport(ObjCRuntime)] static extern IntPtr dlopen(string path, int mode); dlopen(AppKitFramework, 0x1); // have to invoke AppKit so that NSAlert doesn't return a null pointer IntPtr NSStringClass = GetClass("NSString"); IntPtr Selector = GetSelector("stringWithUTF8String:"); IntPtr SharedApp = SendMessage(GetClass("NSApplication"), GetSelector("sharedApplication")); IntPtr NSAlert = SendMessage(GetClass("NSAlert"), GetSelector("alloc")); IntPtr AlertInstance = SendMessage(NSAlert, GetSelector("init")); // Create caption, text, and button text. // If you add a check, you can switch text with the check reason. IntPtr caption = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi($"Ryujinx {Version}")); IntPtr text = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi("Ryujinx is not intended to be run as administrator.")); IntPtr button = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi("OK")); // Set up the window. SendMessageWithParameter(SharedApp, GetSelector("setActivationPolicy:"), (IntPtr) 0); // Give it a window. SendMessageWithParameter(SharedApp, GetSelector("activateIgnoringOtherApps:"), (IntPtr) 1); // Force it to the front. // Set up the message box. SendMessageWithParameter(AlertInstance, GetSelector("setAlertStyle:"), (IntPtr)0); // Set style to warning. SendMessageWithParameter(AlertInstance, GetSelector("setMessageText:"), caption); SendMessageWithParameter(AlertInstance, GetSelector("setInformativeText:"), text); SendMessageWithParameter(AlertInstance, GetSelector("addButtonWithTitle:"), button); // Send prompt to user, then clean up. SendMessage(AlertInstance, GetSelector("runModal")); SendMessage(AlertInstance, GetSelector("release")); return 0; } } if (OperatingSystem.IsLinux()) { if (!root) { Console.WriteLine( $"Ryujinx {Version}: Ryujinx is not intended to be run as administrator. Exiting..."); const string sdl = "SDL2"; [DllImport(sdl)] static extern int SDL_Init(uint flags); [DllImport(sdl, CallingConvention = CallingConvention.Cdecl)] static extern int SDL_ShowSimpleMessageBox(uint flags, string title, string message, IntPtr window); [DllImport(sdl)] static extern void SDL_Quit(); SDL_Init(0); SDL_ShowSimpleMessageBox(32 /* 32 = warning style */, $"Ryujinx {Version}", "Ryujinx is not intended to be run as administrator.", IntPtr.Zero); SDL_Quit(); return 0; } if (Environment.GetEnvironmentVariable("container").EqualsIgnoreCase("flatpak")) { Logger.Warning?.PrintMsg(LogClass.Application, "This is very likely an unofficial build, Ryujinx does NOT have a flatpak!"); Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-="); Logger.Info?.PrintMsg(LogClass.Application, "Please visit https://ryujinx.app/ for our official builds."); Logger.Info?.PrintMsg(LogClass.Application, " AppImage >> https://update.ryujinx.app/download/query?os=linuxappimage&arch=x64&rc=stable"); Logger.Info?.PrintMsg(LogClass.Application, " Tarball >> https://update.ryujinx.app/download/query?os=linux&arch=x64&rc=stable"); Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-="); } } } bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui"); bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps"); // TODO: Ryujinx causes core dumps on Linux when it exits "uncleanly", eg. through an unhandled exception. // This is undesirable and causes very odd behavior during development (the process stops responding, // the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user. // This needs to be investigated, but calling prctl() is better than modifying system-wide settings or leaving this be. if (!coreDumpArg) { OsUtils.SetCoreDumpable(false); } PreviewerDetached = true; if (noGuiArg) { HeadlessRyujinx.Entrypoint(args); return 0; } Initialize(args); LoggerAdapter.Register(); IconProvider.Current .Register() .Register(); return BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() .With(new X11PlatformOptions { UseDBusMenu = false, UseDBusFilePicker = false, EnableMultiTouch = true, EnableIme = true, EnableInputFocusProxy = Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP") == "gamescope", RenderingMode = UseHardwareAcceleration ? [X11RenderingMode.Glx, X11RenderingMode.Software] : [X11RenderingMode.Software] }) .With(new Win32PlatformOptions { WinUICompositionBackdropCornerRadius = 8.0f, RenderingMode = UseHardwareAcceleration ? [Win32RenderingMode.AngleEgl, Win32RenderingMode.Software] : [Win32RenderingMode.Software] }); private static bool ConsumeCommandLineArgument(ref string[] args, string targetArgument) { List argList = [.. args]; bool found = argList.Remove(targetArgument); args = argList.ToArray(); return found; } private static void Initialize(string[] args) { // Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched DiscordIntegrationModule.EmulatorStartedAt = Timestamps.Now; // Parse arguments CommandLineState.ParseArguments(args); if (OperatingSystem.IsMacOS()) { MVKInitialization.InitializeResolver(); } // Delete backup files after updating. Task.Run(Updater.CleanupUpdate); Console.Title = $"{RyujinxApp.FullAppName} Console {Version}"; // Hook unhandled exception and process exit events. AppDomain.CurrentDomain.UnhandledException += (sender, e) => ProcessUnhandledException(sender, e.ExceptionObject as Exception, e.IsTerminating); TaskScheduler.UnobservedTaskException += (sender, e) => ProcessUnhandledException(sender, e.Exception, false); AppDomain.CurrentDomain.ProcessExit += (_, _) => Exit(); // 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 SDL3 driver SDL3Driver.MainThreadDispatcher = action => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Input); ReloadConfig(); WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor(); // Logging system information. PrintSystemInfo(); // Enable OGL multithreading on the driver, and some other flags. DriverUtilities.InitDriverConfig(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off); // Check if keys exists. if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys"))) { if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys")))) { MainWindow.ShowKeyErrorOnLoad = true; } } if (CommandLineState.LaunchPathArg != null) { MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.LaunchApplicationId, CommandLineState.StartFullscreenArg); } } public static string GetDirGameUserConfig(string gameId, bool changeFolderForGame = false) { if (string.IsNullOrEmpty(gameId)) { return ""; } string gameDir = Path.Combine(AppDataManager.GamesDirPath, gameId, ReleaseInformation.ConfigName); if (changeFolderForGame) { ConfigurationPath = gameDir; UseExtraConfig = true; } return gameDir; } public static void ReloadConfig(bool isRunGameWithCustomConfig = false) { string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName); string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName); if (!isRunGameWithCustomConfig) // To return settings from the game folder if the user configuration exists { // Now load the configuration as the other subsystems are now registered if (File.Exists(localConfigurationPath)) { ConfigurationPath = localConfigurationPath; } else if (File.Exists(appDataConfigurationPath)) { ConfigurationPath = appDataConfigurationPath; } } if (ConfigurationPath == null) { // No configuration, we load the default values and save it to disk ConfigurationPath = appDataConfigurationPath; Logger.Notice.Print(LogClass.Application, $"No configuration file found. Saving default configuration to: {ConfigurationPath}"); ConfigurationState.Instance.LoadDefault(); ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath); } else { Logger.Notice.Print(LogClass.Application, $"Loading configuration from: {ConfigurationPath}"); if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat)) { ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath); } else { Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location: {ConfigurationPath}"); ConfigurationFileFormat.RenameInvalidConfigFile(ConfigurationPath); ConfigurationState.Instance.LoadDefault(); } } // When you first load the program, copy to remember the path for the global configuration GlobalConfigurationPath ??= ConfigurationPath; UseHardwareAcceleration = ConfigurationState.Instance.EnableHardwareAcceleration; // Check if graphics backend was overridden if (CommandLineState.OverrideGraphicsBackend is not null) ConfigurationState.Instance.Graphics.GraphicsBackend.Value = CommandLineState.OverrideGraphicsBackend.ToLower() switch { "opengl" => GraphicsBackend.OpenGl, "vulkan" => GraphicsBackend.Vulkan, _ => ConfigurationState.Instance.Graphics.GraphicsBackend }; // Check if backend threading was overridden if (CommandLineState.OverrideBackendThreading is not null) ConfigurationState.Instance.Graphics.BackendThreading.Value = CommandLineState.OverrideBackendThreading.ToLower() switch { "auto" => BackendThreading.Auto, "off" => BackendThreading.Off, "on" => BackendThreading.On, _ => ConfigurationState.Instance.Graphics.BackendThreading }; if (CommandLineState.OverrideBackendThreadingAfterReboot is not null) { BackendThreadingArg = CommandLineState.OverrideBackendThreadingAfterReboot; } // Check if docked mode was overriden. if (CommandLineState.OverrideDockedMode.HasValue) ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value; // 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 }; // Check if memoryManagerMode was overridden. if (CommandLineState.OverrideMemoryManagerMode is not null) if (Enum.TryParse(CommandLineState.OverrideMemoryManagerMode, true, out MemoryManagerMode result)) { ConfigurationState.Instance.System.MemoryManagerMode.Value = result; } // Check if PPTC was overridden. if (CommandLineState.OverridePPTC is not null) if (Enum.TryParse(CommandLineState.OverridePPTC, true, out bool result)) { ConfigurationState.Instance.System.EnablePtc.Value = result; } // Check if region was overridden. if (CommandLineState.OverrideSystemRegion is not null) if (Enum.TryParse(CommandLineState.OverrideSystemRegion, true, out Region result)) { ConfigurationState.Instance.System.Region.Value = result; } //Check if language was overridden. if (CommandLineState.OverrideSystemLanguage is not null) if (Enum.TryParse(CommandLineState.OverrideSystemLanguage, true, out Language result)) { ConfigurationState.Instance.System.Language.Value = result; } // Check if hardware-acceleration was overridden. if (CommandLineState.OverrideHardwareAcceleration != null) UseHardwareAcceleration = CommandLineState.OverrideHardwareAcceleration.Value; } internal static void PrintSystemInfo() { Logger.Notice.Print(LogClass.Application, " ___ __ _ "); Logger.Notice.Print(LogClass.Application, @" / _ \ __ __ __ __ / / (_) ___ ___ _"); Logger.Notice.Print(LogClass.Application, @" / , _/ / // // // / / _ \ / / / _ \ / _ `/"); Logger.Notice.Print(LogClass.Application, @"/_/|_| \_, / \_,_/ /_.__//_/ /_//_/ \_, / "); Logger.Notice.Print(LogClass.Application, " /___/ /___/ "); Logger.Notice.Print(LogClass.Application, $"{RyujinxApp.FullAppName} Version: {Version}"); Logger.Notice.Print(LogClass.Application, $".NET Runtime: {RuntimeInformation.FrameworkDescription}"); SystemInfo.Gather().Print(); Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {Logger.GetEnabledLevels() .FormatCollection( x => x.ToString(), separator: ", ", emptyCollectionFallback: "")}"); Logger.Notice.Print(LogClass.Application, AppDataManager.Mode == AppDataManager.LaunchMode.Custom ? $"Launch Mode: Custom Path {AppDataManager.BaseDirPath}" : $"Launch Mode: {AppDataManager.Mode}"); } internal static void ProcessUnhandledException(object sender, Exception initialException, bool isTerminating) { Logger.Log log = Logger.Error ?? Logger.Notice; List exceptions = []; if (initialException is AggregateException ae) { exceptions.AddRange(ae.InnerExceptions); } else { exceptions.Add(initialException); } foreach (Exception e in exceptions) { string message = $"Unhandled exception caught: {e}"; // ReSharper disable once ConstantConditionalAccessQualifier if (sender?.GetType()?.AsPrettyString() is { } senderName) log.Print(LogClass.Application, message, senderName); else log.PrintMsg(LogClass.Application, message); } if (isTerminating) { Logger.Flush(); Exit(); } } internal static void Exit() { DiscordIntegrationModule.Exit(); Logger.Shutdown(); } } }