From a477658b85697d42bc709a5a8aef086b24739399 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Mon, 9 Dec 2024 03:21:07 -0600 Subject: [PATCH] Headless in Avalonia v2 --- .../Logging/Targets/AsyncLogTargetWrapper.cs | 6 +- .../Account/Acc/AccountSaveDataManager.cs | 16 +- .../Configuration/System/Language.cs | 1 + src/Ryujinx/Headless/HeadlessRyujinx.cs | 102 ++++++++---- src/Ryujinx/Headless/Options.cs | 154 +++++++++++++++++- src/Ryujinx/Program.cs | 17 +- 6 files changed, 247 insertions(+), 49 deletions(-) diff --git a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs index a9dbe646a..1fcfea4da 100644 --- a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs +++ b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs @@ -27,11 +27,7 @@ namespace Ryujinx.Common.Logging.Targets private readonly int _overflowTimeout; - string ILogTarget.Name { get => _target.Name; } - - public AsyncLogTargetWrapper(ILogTarget target) - : this(target, -1) - { } + string ILogTarget.Name => _target.Name; public AsyncLogTargetWrapper(ILogTarget target, int queueLimit = -1, AsyncLogTargetOverflowAction overflowAction = AsyncLogTargetOverflowAction.Block) { diff --git a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs index b1ef0761c..aa57a0310 100644 --- a/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Account/Acc/AccountSaveDataManager.cs @@ -1,3 +1,4 @@ +using Gommon; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; @@ -6,12 +7,13 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; namespace Ryujinx.HLE.HOS.Services.Account.Acc { - class AccountSaveDataManager + public class AccountSaveDataManager { - private readonly string _profilesJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "Profiles.json"); + private static readonly string _profilesJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "Profiles.json"); private static readonly ProfilesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); @@ -49,6 +51,16 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc } } + public static Optional GetLastUsedUser() + { + ProfilesJson profilesJson = JsonHelper.DeserializeFromFile(_profilesJsonPath, _serializerContext.ProfilesJson); + + return profilesJson.Profiles + .FindFirst(profile => profile.AccountState == AccountState.Open) + .Convert(profileJson => new UserProfile(new UserId(profileJson.UserId), profileJson.Name, + profileJson.Image, profileJson.LastModifiedTimestamp)); + } + public void Save(ConcurrentDictionary profiles) { ProfilesJson profilesJson = new() diff --git a/src/Ryujinx.UI.Common/Configuration/System/Language.cs b/src/Ryujinx.UI.Common/Configuration/System/Language.cs index d1d395b00..8ca4e542b 100644 --- a/src/Ryujinx.UI.Common/Configuration/System/Language.cs +++ b/src/Ryujinx.UI.Common/Configuration/System/Language.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.SystemState; using System.Text.Json.Serialization; namespace Ryujinx.UI.Common.Configuration.System diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.cs b/src/Ryujinx/Headless/HeadlessRyujinx.cs index 2918e130b..74cf69e57 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.cs @@ -68,7 +68,7 @@ namespace Ryujinx.Headless private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public static void Initialize(string[] args) + public static void Initialize() { // Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched DiscordIntegrationModule.StartedAt = Timestamps.Now; @@ -81,32 +81,17 @@ namespace Ryujinx.Headless => Program.ProcessUnhandledException(sender, e.ExceptionObject as Exception, e.IsTerminating); AppDomain.CurrentDomain.ProcessExit += (_, _) => Program.Exit(); - // Setup base data directory. - AppDataManager.Initialize(CommandLineState.BaseDirPathArg); - - // Set the delegate for localizing the word "never" in the UI - ApplicationData.LocalizedNever = () => LocaleManager.Instance[LocaleKeys.Never]; - // Initialize the configuration. ConfigurationState.Initialize(); - // Initialize the logger system. - LoggerModule.Initialize(); - // Initialize Discord integration. DiscordIntegrationModule.Initialize(); - // Initialize SDL2 driver - SDL2Driver.MainThreadDispatcher = action => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Input); - - Program.ReloadConfig(); + ReloadConfig(); // Logging system information. Program.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"))) { @@ -115,8 +100,6 @@ namespace Ryujinx.Headless Logger.Error?.Print(LogClass.Application, "Keys not found"); } } - - Entrypoint(args); } public static void Entrypoint(string[] args) @@ -124,7 +107,7 @@ namespace Ryujinx.Headless // Make process DPI aware for proper window sizing on high-res screens. ForceDpiAware.Windows(); - Console.Title = $"Ryujinx Console {Program.Version} (Headless SDL2)"; + Console.Title = $"Ryujinx Console {Program.Version} (Headless)"; if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()) { @@ -152,7 +135,7 @@ namespace Ryujinx.Headless } Parser.Default.ParseArguments(args) - .WithParsed(Load) + .WithParsed(options => Load(args, options)) .WithNotParsed(errors => { Logger.Error?.PrintMsg(LogClass.Application, "Error parsing command-line arguments:"); @@ -160,6 +143,53 @@ namespace Ryujinx.Headless errors.ForEach(err => Logger.Error?.PrintMsg(LogClass.Application, $" - {err.Tag}")); }); } + + public static void ReloadConfig(string customConfigPath = null) + { + string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName); + string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName); + + string configurationPath = null; + + // Now load the configuration as the other subsystems are now registered + if (File.Exists(localConfigurationPath)) + { + configurationPath = localConfigurationPath; + } + else if (File.Exists(appDataConfigurationPath)) + { + configurationPath = appDataConfigurationPath; + } + else if (File.Exists(customConfigPath)) + { + configurationPath = customConfigPath; + } + + 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}"); + + ConfigurationState.Instance.LoadDefault(); + } + } + } private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index) { @@ -389,8 +419,13 @@ namespace Ryujinx.Headless return config; } - static void Load(Options option) + static void Load(string[] originalArgs, Options option) { + Initialize(); + + if (option.InheritConfig) + option.InheritMainConfig(originalArgs, ConfigurationState.Instance, out _inputConfiguration); + AppDataManager.Initialize(option.BaseDataDir); _virtualFileSystem = VirtualFileSystem.CreateInstance(); @@ -466,15 +501,18 @@ namespace Ryujinx.Headless } } - LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1); - LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2); - LoadPlayerConfiguration(option.InputProfile3Name, option.InputId3, PlayerIndex.Player3); - LoadPlayerConfiguration(option.InputProfile4Name, option.InputId4, PlayerIndex.Player4); - LoadPlayerConfiguration(option.InputProfile5Name, option.InputId5, PlayerIndex.Player5); - LoadPlayerConfiguration(option.InputProfile6Name, option.InputId6, PlayerIndex.Player6); - LoadPlayerConfiguration(option.InputProfile7Name, option.InputId7, PlayerIndex.Player7); - LoadPlayerConfiguration(option.InputProfile8Name, option.InputId8, PlayerIndex.Player8); - LoadPlayerConfiguration(option.InputProfileHandheldName, option.InputIdHandheld, PlayerIndex.Handheld); + if (!option.InheritConfig) + { + LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1); + LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2); + LoadPlayerConfiguration(option.InputProfile3Name, option.InputId3, PlayerIndex.Player3); + LoadPlayerConfiguration(option.InputProfile4Name, option.InputId4, PlayerIndex.Player4); + LoadPlayerConfiguration(option.InputProfile5Name, option.InputId5, PlayerIndex.Player5); + LoadPlayerConfiguration(option.InputProfile6Name, option.InputId6, PlayerIndex.Player6); + LoadPlayerConfiguration(option.InputProfile7Name, option.InputId7, PlayerIndex.Player7); + LoadPlayerConfiguration(option.InputProfile8Name, option.InputId8, PlayerIndex.Player8); + LoadPlayerConfiguration(option.InputProfileHandheldName, option.InputIdHandheld, PlayerIndex.Handheld); + } if (_inputConfiguration.Count == 0) { @@ -486,7 +524,7 @@ namespace Ryujinx.Headless Logger.SetEnable(LogLevel.Stub, !option.LoggingDisableStub); Logger.SetEnable(LogLevel.Info, !option.LoggingDisableInfo); Logger.SetEnable(LogLevel.Warning, !option.LoggingDisableWarning); - Logger.SetEnable(LogLevel.Error, option.LoggingEnableError); + Logger.SetEnable(LogLevel.Error, !option.LoggingDisableError); Logger.SetEnable(LogLevel.Trace, option.LoggingEnableTrace); Logger.SetEnable(LogLevel.Guest, !option.LoggingDisableGuest); Logger.SetEnable(LogLevel.AccessLog, option.LoggingEnableFsAccessLog); diff --git a/src/Ryujinx/Headless/Options.cs b/src/Ryujinx/Headless/Options.cs index db873b921..3a807cb42 100644 --- a/src/Ryujinx/Headless/Options.cs +++ b/src/Ryujinx/Headless/Options.cs @@ -1,8 +1,15 @@ using CommandLine; +using Gommon; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; using Ryujinx.HLE; +using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.UI.Common.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; namespace Ryujinx.Headless { @@ -10,9 +17,152 @@ namespace Ryujinx.Headless { // General - public void InheritMainConfig(ConfigurationState configurationState) + public void InheritMainConfig(string[] originalArgs, ConfigurationState configurationState, out List inputConfigs) { + if (NeedsOverride(nameof(UserProfile)) && AccountSaveDataManager.GetLastUsedUser().TryGet(out var profile)) + UserProfile = profile.Name; + if (NeedsOverride(nameof(IsFullscreen))) + IsFullscreen = configurationState.UI.StartFullscreen; + + if (NeedsOverride(nameof(EnableKeyboard))) + EnableKeyboard = configurationState.Hid.EnableKeyboard; + + if (NeedsOverride(nameof(EnableMouse))) + EnableMouse = configurationState.Hid.EnableMouse; + + if (NeedsOverride(nameof(HideCursorMode))) + HideCursorMode = configurationState.HideCursor; + + if (NeedsOverride(nameof(DisablePTC))) + DisablePTC = !configurationState.System.EnablePtc; + + if (NeedsOverride(nameof(EnableInternetAccess))) + EnableInternetAccess = configurationState.System.EnableInternetAccess; + + if (NeedsOverride(nameof(DisableFsIntegrityChecks))) + DisableFsIntegrityChecks = configurationState.System.EnableFsIntegrityChecks; + + if (NeedsOverride(nameof(FsGlobalAccessLogMode))) + FsGlobalAccessLogMode = configurationState.System.FsGlobalAccessLogMode; + + if (NeedsOverride(nameof(VSyncMode))) + VSyncMode = configurationState.Graphics.VSyncMode; + + if (NeedsOverride(nameof(CustomVSyncInterval))) + CustomVSyncInterval = configurationState.Graphics.CustomVSyncInterval; + + if (NeedsOverride(nameof(DisableShaderCache))) + DisableShaderCache = !configurationState.Graphics.EnableShaderCache; + + if (NeedsOverride(nameof(EnableTextureRecompression))) + EnableTextureRecompression = configurationState.Graphics.EnableTextureRecompression; + + if (NeedsOverride(nameof(DisableDockedMode))) + DisableDockedMode = !configurationState.System.EnableDockedMode; + + if (NeedsOverride(nameof(SystemLanguage))) + SystemLanguage = (SystemLanguage)(int)configurationState.System.Language.Value; + + if (NeedsOverride(nameof(SystemRegion))) + SystemRegion = (RegionCode)(int)configurationState.System.Region.Value; + + if (NeedsOverride(nameof(SystemTimeZone))) + SystemTimeZone = configurationState.System.TimeZone; + + if (NeedsOverride(nameof(SystemTimeOffset))) + SystemTimeOffset = configurationState.System.SystemTimeOffset; + + if (NeedsOverride(nameof(MemoryManagerMode))) + MemoryManagerMode = configurationState.System.MemoryManagerMode; + + if (NeedsOverride(nameof(AudioVolume))) + AudioVolume = configurationState.System.AudioVolume; + + if (NeedsOverride(nameof(UseHypervisor)) && OperatingSystem.IsMacOS()) + UseHypervisor = configurationState.System.UseHypervisor; + + if (NeedsOverride(nameof(MultiplayerLanInterfaceId))) + MultiplayerLanInterfaceId = configurationState.Multiplayer.LanInterfaceId; + + if (NeedsOverride(nameof(DisableFileLog))) + DisableFileLog = !configurationState.Logger.EnableFileLog; + + if (NeedsOverride(nameof(LoggingEnableDebug))) + LoggingEnableDebug = configurationState.Logger.EnableDebug; + + if (NeedsOverride(nameof(LoggingDisableStub))) + LoggingDisableStub = !configurationState.Logger.EnableStub; + + if (NeedsOverride(nameof(LoggingDisableInfo))) + LoggingDisableInfo = !configurationState.Logger.EnableInfo; + + if (NeedsOverride(nameof(LoggingDisableWarning))) + LoggingDisableWarning = !configurationState.Logger.EnableWarn; + + if (NeedsOverride(nameof(LoggingDisableError))) + LoggingDisableError = !configurationState.Logger.EnableError; + + if (NeedsOverride(nameof(LoggingEnableTrace))) + LoggingEnableTrace = configurationState.Logger.EnableTrace; + + if (NeedsOverride(nameof(LoggingDisableGuest))) + LoggingDisableGuest = !configurationState.Logger.EnableGuest; + + if (NeedsOverride(nameof(LoggingEnableFsAccessLog))) + LoggingEnableFsAccessLog = configurationState.Logger.EnableFsAccessLog; + + if (NeedsOverride(nameof(LoggingGraphicsDebugLevel))) + LoggingGraphicsDebugLevel = configurationState.Logger.GraphicsDebugLevel; + + if (NeedsOverride(nameof(ResScale))) + ResScale = configurationState.Graphics.ResScale; + + if (NeedsOverride(nameof(MaxAnisotropy))) + MaxAnisotropy = configurationState.Graphics.MaxAnisotropy; + + if (NeedsOverride(nameof(AspectRatio))) + AspectRatio = configurationState.Graphics.AspectRatio; + + if (NeedsOverride(nameof(BackendThreading))) + BackendThreading = configurationState.Graphics.BackendThreading; + + if (NeedsOverride(nameof(DisableMacroHLE))) + DisableMacroHLE = !configurationState.Graphics.EnableMacroHLE; + + if (NeedsOverride(nameof(GraphicsShadersDumpPath))) + GraphicsShadersDumpPath = configurationState.Graphics.ShadersDumpPath; + + if (NeedsOverride(nameof(GraphicsBackend))) + GraphicsBackend = configurationState.Graphics.GraphicsBackend; + + if (NeedsOverride(nameof(AntiAliasing))) + AntiAliasing = configurationState.Graphics.AntiAliasing; + + if (NeedsOverride(nameof(ScalingFilter))) + ScalingFilter = configurationState.Graphics.ScalingFilter; + + if (NeedsOverride(nameof(ScalingFilterLevel))) + ScalingFilterLevel = configurationState.Graphics.ScalingFilterLevel; + + if (NeedsOverride(nameof(DramSize))) + DramSize = configurationState.System.DramSize; + + if (NeedsOverride(nameof(IgnoreMissingServices))) + IgnoreMissingServices = configurationState.System.IgnoreMissingServices; + + if (NeedsOverride(nameof(IgnoreControllerApplet))) + IgnoreControllerApplet = configurationState.IgnoreApplet; + + + inputConfigs = configurationState.Hid.InputConfig; + + return; + + bool NeedsOverride(string argKey) => originalArgs.None(arg => arg.TrimStart('-').EqualsIgnoreCase(OptionName(argKey))); + + string OptionName(string propertyName) => + typeof(Options)!.GetProperty(propertyName)!.GetCustomAttribute()!.LongName; } [Option("use-main-config", Required = false, Default = false, HelpText = "Use the settings from what was configured via the UI.")] @@ -181,7 +331,7 @@ namespace Ryujinx.Headless public bool LoggingDisableWarning { get; set; } [Option("disable-error-logs", Required = false, HelpText = "Disables printing error log messages.")] - public bool LoggingEnableError { get; set; } + public bool LoggingDisableError { get; set; } [Option("enable-trace-logs", Required = false, Default = false, HelpText = "Enables printing trace log messages.")] public bool LoggingEnableTrace { get; set; } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 35cdb6c0e..997b3dcde 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -53,15 +53,15 @@ namespace Ryujinx.Ava } PreviewerDetached = true; - - Initialize(args); - + if (args[0] is "--no-gui" or "nogui") { - HeadlessRyujinx.Initialize(args[1..]); + HeadlessRyujinx.Entrypoint(args[1..]); return 0; } + Initialize(args); + LoggerAdapter.Register(); IconProvider.Current @@ -114,9 +114,6 @@ namespace Ryujinx.Ava => ProcessUnhandledException(sender, e.ExceptionObject as Exception, e.IsTerminating); AppDomain.CurrentDomain.ProcessExit += (_, _) => Exit(); - // Setup base data directory. - AppDataManager.Initialize(CommandLineState.BaseDirPathArg); - // Set the delegate for localizing the word "never" in the UI ApplicationData.LocalizedNever = () => LocaleManager.Instance[LocaleKeys.Never]; @@ -157,7 +154,7 @@ namespace Ryujinx.Ava } } - public static void ReloadConfig() + public static void ReloadConfig(string customConfigPath = null) { string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName); string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName); @@ -171,6 +168,10 @@ namespace Ryujinx.Ava { ConfigurationPath = appDataConfigurationPath; } + else if (File.Exists(customConfigPath)) + { + ConfigurationPath = customConfigPath; + } if (ConfigurationPath == null) {