using ARMeilleure.Translation; using CommandLine; using LibHac.Tools.FsSystem; using Ryujinx.Audio.Backends.SDL2; using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Controller.Motion; using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Logging; using Ryujinx.Common.Logging.Targets; using Ryujinx.Common.SystemInterop; using Ryujinx.Common.Utilities; using Ryujinx.Cpu; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL.Multithreading; using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.Gpu.Shader; using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.Vulkan; using Ryujinx.Headless.SDL2.OpenGL; using Ryujinx.Headless.SDL2.Vulkan; using Ryujinx.HLE; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.Input; using Ryujinx.Input.HLE; using Ryujinx.Input.SDL2; using Ryujinx.SDL2.Common; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration.System; using Ryujinx.Ui.Common; using Silk.NET.Vulkan; using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Text.Json; using System.Threading; using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; using Key = Ryujinx.Common.Configuration.Hid.Key; using System.Linq; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS; using Ryujinx.Input.HLE; using Ryujinx.HLE; using System; using System.Runtime.InteropServices; using Ryujinx.Common.Configuration; using LibHac.Tools.FsSystem; using Ryujinx.Graphics.GAL.Multithreading; using Ryujinx.Audio.Backends.Dummy; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Audio.Integration; using Ryujinx.Audio.Backends.SDL2; using System.IO; using LibHac.Common.Keys; using LibHac.Common; using LibHac.Ns; using LibHac.Tools.Fs; using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Fs.Fsa; using LibHac.FsSystem; using LibHac.Fs; using Path = System.IO.Path; using LibHac; using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.HLE.Loaders.Npdm; using Ryujinx.Common.Utilities; using System.Globalization; using Ryujinx.Ui.Common.Configuration.System; using Ryujinx.Common.Logging.Targets; using System.Collections.Generic; using LibHac.Bcat; using Ryujinx.Ui.App.Common; using System.Text; using Ryujinx.HLE.Ui; using ARMeilleure.Translation; using LibHac.Ncm; using LibHac.Tools.FsSystem.NcaUtils; using Microsoft.Win32.SafeHandles; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.Input.HLE; using Silk.NET.Vulkan; using System; using System.IO; using System.Runtime.InteropServices; using SDL2; namespace Ryujinx.Headless.SDL2 { class Program { public static string Version { get; private set; } private static VirtualFileSystem _virtualFileSystem; private static ContentManager _contentManager; private static AccountManager _accountManager; private static LibHacHorizonManager _libHacHorizonManager; private static UserChannelPersistence _userChannelPersistence; private static InputManager _inputManager; private static Switch _emulationContext; private static WindowBase _window; private static WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution; private static List _inputConfiguration; private static bool _enableKeyboard; private static bool _enableMouse; private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); [UnmanagedCallersOnly(EntryPoint = "main_ryujinx_sdl")] public static unsafe int MainExternal(int argCount, IntPtr* pArgs) { string[] args = new string[argCount]; try { for (int i = 0; i < argCount; i++) { args[i] = Marshal.PtrToStringAnsi(pArgs[i]); Console.WriteLine(args[i]); } Main(args); } catch (Exception e) { Console.WriteLine(e.ToString()); return -1; } return 0; } [UnmanagedCallersOnly(EntryPoint = "get_dlc_nca_list")] public static unsafe DlcNcaList GetDlcNcaList(IntPtr titleIdPtr, IntPtr pathPtr) { var titleId = Marshal.PtrToStringAnsi(titleIdPtr); var containerPath = Marshal.PtrToStringAnsi(pathPtr); if (!File.Exists(containerPath)) { return new DlcNcaList { success = false }; } using FileStream containerFile = File.OpenRead(containerPath); PartitionFileSystem pfs = new(); pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); bool containsDlc = false; _virtualFileSystem.ImportTickets(pfs); // TreeIter? parentIter = null; List listItems = new(); foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) { using var ncaFile = new UniqueRef(); pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath); if (nca == null) { continue; } if (nca.Header.ContentType == NcaContentType.PublicData) { if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != titleId) { break; } Logger.Warning?.Print(LogClass.Application, $"ContainerPath: {containerPath}"); Logger.Warning?.Print(LogClass.Application, $"TitleId: {nca.Header.TitleId}"); Logger.Warning?.Print(LogClass.Application, $"fileEntry.FullPath: {fileEntry.FullPath}"); // parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath); // ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); DlcNcaListItem item = new(); CopyStringToFixedArray(fileEntry.FullPath, item.Path, 256); item.TitleId = nca.Header.TitleId; listItems.Add(item); containsDlc = true; } } if (!containsDlc) { return new DlcNcaList { success = false }; // GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!"); } var list = new DlcNcaList { success = true, size = (uint) listItems.Count }; DlcNcaListItem[] items = listItems.ToArray(); fixed (DlcNcaListItem* p = &items[0]) { list.items = p; } return list; } private static Nca TryCreateNca(IStorage ncaStorage, string containerPath) { try { return new Nca(_virtualFileSystem.KeySet, ncaStorage); } catch (Exception exception) { // ignored } return null; } [UnmanagedCallersOnly(EntryPoint = "get_current_fps")] public static unsafe int GetFPS() { if (_window != null) { Switch Device = _window.Device; int intValue = (int)Device.Statistics.GetGameFrameRate(); return intValue; } return 0; } [UnmanagedCallersOnly(EntryPoint = "initialize")] public static unsafe void Initialize() { AppDataManager.Initialize(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)); if (_virtualFileSystem == null) { _virtualFileSystem = VirtualFileSystem.CreateInstance(); } if (_libHacHorizonManager == null) { _libHacHorizonManager = new LibHacHorizonManager(); _libHacHorizonManager.InitializeFsServer(_virtualFileSystem); _libHacHorizonManager.InitializeArpServer(); _libHacHorizonManager.InitializeBcatServer(); _libHacHorizonManager.InitializeSystemClients(); } if (_contentManager == null) { _contentManager = new ContentManager(_virtualFileSystem); } if (_accountManager == null) { _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, ""); } if (_userChannelPersistence == null) { _userChannelPersistence = new UserChannelPersistence(); } } static void Main(string[] args) { // Make process DPI aware for proper window sizing on high-res screens. ForceDpiAware.Windows(); Silk.NET.Core.Loader.SearchPathContainer.Platform = Silk.NET.Core.Loader.UnderlyingPlatform.MacOS; Version = ReleaseInformation.GetVersion(); if (!OperatingSystem.IsIOS()) { Console.Title = $"Ryujinx Console {Version} (Headless SDL2)"; } if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsLinux()) { AutoResetEvent invoked = new(false); // MacOS must perform SDL polls from the main thread. SDL2Driver.MainThreadDispatcher = action => { invoked.Reset(); WindowBase.QueueMainThreadAction(() => { action(); invoked.Set(); }); invoked.WaitOne(); }; } var result = Parser.Default.ParseArguments(args) .WithParsed(options => { Load(options); // Load is called with the parsed options }) .WithNotParsed(errors => errors.Output()); } [UnmanagedCallersOnly(EntryPoint = "install_firmware")] public static void InstallFirmwareNative(IntPtr inputPtr) { try { if (inputPtr == IntPtr.Zero) { Console.Error.WriteLine("Error: inputPtr is null."); return; } string inputString = Marshal.PtrToStringAnsi(inputPtr); if (string.IsNullOrEmpty(inputString)) { Console.Error.WriteLine("Error: inputString is null or empty."); return; } InstallFirmware(inputString); } catch (Exception ex) { Console.Error.WriteLine($"Error in InstallFirmwareNative: {ex.Message}"); } } public static void InstallFirmware(string filePath) { if (string.IsNullOrEmpty(filePath)) { throw new ArgumentException("File path cannot be null or empty.", nameof(filePath)); } if (_contentManager == null) { throw new InvalidOperationException("_contentManager is not initialized."); } _contentManager.InstallFirmware(filePath); } [UnmanagedCallersOnly(EntryPoint = "installed_firmware_version")] public static IntPtr GetInstalledFirmwareVersionNative() { var result = GetInstalledFirmwareVersion(); return Marshal.StringToHGlobalAnsi(result); } public static string GetInstalledFirmwareVersion() { var version = _contentManager.GetCurrentFirmwareVersion(); if (version != null) { return version.VersionString; } return String.Empty; } [UnmanagedCallersOnly(EntryPoint = "stop_emulation")] public static void StopEmulation() { if (_window != null) { _window.Exit(); _emulationContext.Dispose(); _emulationContext = null; } } [UnmanagedCallersOnly(EntryPoint = "get_game_info")] public static GameInfoNative GetGameInfoNative(int descriptor, IntPtr extensionPtr) { if (_virtualFileSystem == null) { _virtualFileSystem = VirtualFileSystem.CreateInstance(); } var extension = Marshal.PtrToStringAnsi(extensionPtr); var stream = OpenFile(descriptor); var gameInfo = GetGameInfo(stream, extension); if (gameInfo == null) { return new GameInfoNative(0, "", "", "", "", new byte[0]); } return new GameInfoNative( (ulong)gameInfo.FileSize, gameInfo.TitleName, gameInfo.TitleId, gameInfo.Developer, gameInfo.Version, gameInfo.Icon ); } public static GameInfo? GetGameInfo(Stream gameStream, string extension) { var gameInfo = new GameInfo { FileSize = gameStream.Length * 0.000000000931, TitleName = "Unknown", TitleId = "0000000000000000", Developer = "Unknown", Version = "0", Icon = null }; const Language TitleLanguage = Language.AmericanEnglish; BlitStruct controlHolder = new(1); try { try { if (extension == "nsp" || extension == "pfs0" || extension == "xci") { IFileSystem pfs; bool isExeFs = false; if (extension == "xci") { Xci xci = new(_virtualFileSystem.KeySet, gameStream.AsStorage()); pfs = xci.OpenPartition(XciPartitionType.Secure); } else { var pfsTemp = new PartitionFileSystem(); pfsTemp.Initialize(gameStream.AsStorage()).ThrowIfFailure(); pfs = pfsTemp; // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. bool hasMainNca = false; foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) { if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") { using UniqueRef ncaFile = new(); pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage()); int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); // Some main NCAs don't have a data partition, so check if the partition exists before opening it if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())) { hasMainNca = true; break; } } else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") { isExeFs = true; } } if (!hasMainNca && !isExeFs) { return null; } } if (isExeFs) { using UniqueRef npdmFile = new(); LibHac.Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read); if (ResultFs.PathNotFound.Includes(result)) { Npdm npdm = new(npdmFile.Get.AsStream()); gameInfo.TitleName = npdm.TitleName; gameInfo.TitleId = npdm.Aci0.TitleId.ToString("x16"); } } else { GetControlFsAndTitleId(pfs, out IFileSystem? controlFs, out string? id); gameInfo.TitleId = id; if (controlFs == null) { Logger.Error?.Print(LogClass.Application, $"No control FS was returned. Unable to process game any further: {gameInfo.TitleName}"); return null; } // Check if there is an update available. if (IsUpdateApplied(gameInfo.TitleId, out IFileSystem? updatedControlFs)) { // Replace the original ControlFs by the updated one. controlFs = updatedControlFs; } ReadControlData(controlFs, controlHolder.ByteSpan); GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out gameInfo.TitleId, out gameInfo.Developer, out gameInfo.Version); // Read the icon from the ControlFS and store it as a byte array try { using UniqueRef icon = new(); controlFs?.OpenFile(ref icon.Ref, $"/icon_{TitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); using MemoryStream stream = new(); icon.Get.AsStream().CopyTo(stream); gameInfo.Icon = stream.ToArray(); } catch (HorizonResultException) { foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) { if (entry.Name == "control.nacp") { continue; } using var icon = new UniqueRef(); controlFs?.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); using MemoryStream stream = new(); icon.Get.AsStream().CopyTo(stream); gameInfo.Icon = stream.ToArray(); if (gameInfo.Icon != null) { break; } } } } } else if (extension == "nro") { BinaryReader reader = new(gameStream); byte[] Read(long position, int size) { gameStream.Seek(position, SeekOrigin.Begin); return reader.ReadBytes(size); } gameStream.Seek(24, SeekOrigin.Begin); int assetOffset = reader.ReadInt32(); if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") { byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); ulong nacpOffset = reader.ReadUInt64(); ulong nacpSize = reader.ReadUInt64(); // Reads and stores game icon as byte array if (iconSize > 0) { gameInfo.Icon = Read(assetOffset + iconOffset, (int)iconSize); } // Read the NACP data Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out gameInfo.TitleId, out gameInfo.Developer, out gameInfo.Version); } } } catch (MissingKeyException exception) { Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); } catch (InvalidDataException exception) { Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. {exception}"); } catch (Exception exception) { Logger.Warning?.Print(LogClass.Application, $"The gameStream encountered was not of a valid type. Error: {exception}"); return null; } } catch (IOException exception) { Logger.Warning?.Print(LogClass.Application, exception.Message); } void ReadControlData(IFileSystem? controlFs, Span outProperty) { using UniqueRef controlFile = new(); controlFs?.OpenFile(ref controlFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); controlFile.Get.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure(); } void GetGameInformation(ref ApplicationControlProperty controlData, out string? titleName, out string titleId, out string? publisher, out string? version) { _ = Enum.TryParse(TitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage) { titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); } else { titleName = null; publisher = null; } if (string.IsNullOrWhiteSpace(titleName)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.NameString.IsEmpty()) { titleName = controlTitle.NameString.ToString(); break; } } } if (string.IsNullOrWhiteSpace(publisher)) { foreach (ref readonly var controlTitle in controlData.Title.ItemsRo) { if (!controlTitle.PublisherString.IsEmpty()) { publisher = controlTitle.PublisherString.ToString(); break; } } } if (controlData.PresenceGroupId != 0) { titleId = controlData.PresenceGroupId.ToString("x16"); } else if (controlData.SaveDataOwnerId != 0) { titleId = controlData.SaveDataOwnerId.ToString(); } else if (controlData.AddOnContentBaseId != 0) { titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); } else { titleId = "0000000000000000"; } version = controlData.DisplayVersionString.ToString(); } void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem? controlFs, out string? titleId) { (_, _, Nca? controlNca) = GetGameData(_virtualFileSystem, pfs, 0); if (controlNca == null) { Logger.Warning?.Print(LogClass.Application, "Control NCA is null. Unable to load control FS."); } // Return the ControlFS controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); titleId = controlNca?.Header.TitleId.ToString("x16"); } (Nca? mainNca, Nca? patchNca, Nca? controlNca) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex) { Nca? mainNca = null; Nca? patchNca = null; Nca? controlNca = null; fileSystem.ImportTickets(pfs); foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) { using var ncaFile = new UniqueRef(); Logger.Info?.Print(LogClass.Application, $"Loading file from PFS: {fileEntry.FullPath}"); pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); if (ncaProgramIndex != programIndex) { continue; } if (nca.Header.ContentType == NcaContentType.Program) { int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()) { patchNca = nca; } else { mainNca = nca; } } else if (nca.Header.ContentType == NcaContentType.Control) { controlNca = nca; } } return (mainNca, patchNca, controlNca); } bool IsUpdateApplied(string? titleId, out IFileSystem? updatedControlFs) { updatedControlFs = null; string? updatePath = "(unknown)"; if (_virtualFileSystem == null) { Logger.Error?.Print(LogClass.Application, "SwitchDevice was not initialized."); return false; } try { (Nca? patchNca, Nca? controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); if (patchNca != null && controlNca != null) { updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); return true; } } catch (InvalidDataException) { Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}"); } catch (MissingKeyException exception) { Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}"); } return false; } (Nca? patch, Nca? control) GetGameUpdateData(VirtualFileSystem fileSystem, string? titleId, int programIndex, out string? updatePath) { updatePath = ""; if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase)) { // Clear the program index part. titleIdBase &= ~0xFUL; // Load update information if exists. string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json"); if (File.Exists(titleUpdateMetadataPath)) { string updatePathRelative = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; updatePath = Path.Combine(AppDataManager.BaseDirPath, updatePathRelative); if (File.Exists(updatePath)) { FileStream file = new(updatePath, FileMode.Open, FileAccess.Read); PartitionFileSystem nsp = new(); nsp.Initialize(file.AsStorage()).ThrowIfFailure(); return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex); } } } return (null, null); } (Nca? patchNca, Nca? controlNca) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex) { Nca? patchNca = null; Nca? controlNca = null; fileSystem.ImportTickets(pfs); foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) { using var ncaFile = new UniqueRef(); pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage()); int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF); if (ncaProgramIndex != programIndex) { continue; } if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId) { break; } if (nca.Header.ContentType == NcaContentType.Program) { patchNca = nca; } else if (nca.Header.ContentType == NcaContentType.Control) { controlNca = nca; } } return (patchNca, controlNca); } return gameInfo; } private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index, Options option) { if (inputId == null) { if (index == PlayerIndex.Player1) { Logger.Info?.Print(LogClass.Application, $"{index} not configured, defaulting to default keyboard."); // Default to keyboard inputId = "0"; } else { Logger.Info?.Print(LogClass.Application, $"{index} not configured"); return null; } } IGamepad gamepad; bool isKeyboard = true; gamepad = _inputManager.KeyboardDriver.GetGamepad(inputId); if (gamepad == null) { gamepad = _inputManager.GamepadDriver.GetGamepad(inputId); isKeyboard = false; if (gamepad == null) { Logger.Error?.Print(LogClass.Application, $"{index} gamepad not found (\"{inputId}\")"); inputId = "0"; gamepad = _inputManager.KeyboardDriver.GetGamepad(inputId); isKeyboard = true; } } string gamepadName = gamepad.Name; gamepad.Dispose(); InputConfig config; if (inputProfileName == null || inputProfileName.Equals("default")) { if (isKeyboard) { config = new StandardKeyboardInputConfig { Version = InputConfig.CurrentVersion, Backend = InputBackendType.WindowKeyboard, Id = null, ControllerType = ControllerType.JoyconPair, LeftJoycon = new LeftJoyconCommonConfig { DpadUp = Key.Up, DpadDown = Key.Down, DpadLeft = Key.Left, DpadRight = Key.Right, ButtonMinus = Key.Minus, ButtonL = Key.E, ButtonZl = Key.Q, ButtonSl = Key.Unbound, ButtonSr = Key.Unbound, }, LeftJoyconStick = new JoyconConfigKeyboardStick { StickUp = Key.W, StickDown = Key.S, StickLeft = Key.A, StickRight = Key.D, StickButton = Key.F, }, RightJoycon = new RightJoyconCommonConfig { ButtonA = Key.Z, ButtonB = Key.X, ButtonX = Key.C, ButtonY = Key.V, ButtonPlus = Key.Plus, ButtonR = Key.U, ButtonZr = Key.O, ButtonSl = Key.Unbound, ButtonSr = Key.Unbound, }, RightJoyconStick = new JoyconConfigKeyboardStick { StickUp = Key.I, StickDown = Key.K, StickLeft = Key.J, StickRight = Key.L, StickButton = Key.H, }, }; } else { bool isAppleController = gamepadName.Contains("Apple") ? option.OnScreenCorrespond : false; bool isNintendoStyle = gamepadName.Contains("Nintendo") || isAppleController; config = new StandardControllerInputConfig { Version = InputConfig.CurrentVersion, Backend = InputBackendType.GamepadSDL2, Id = null, ControllerType = ControllerType.JoyconPair, DeadzoneLeft = 0.1f, DeadzoneRight = 0.1f, RangeLeft = 1.0f, RangeRight = 1.0f, TriggerThreshold = 0.5f, LeftJoycon = new LeftJoyconCommonConfig { DpadUp = ConfigGamepadInputId.DpadUp, DpadDown = ConfigGamepadInputId.DpadDown, DpadLeft = ConfigGamepadInputId.DpadLeft, DpadRight = ConfigGamepadInputId.DpadRight, ButtonMinus = ConfigGamepadInputId.Minus, ButtonL = ConfigGamepadInputId.LeftShoulder, ButtonZl = ConfigGamepadInputId.LeftTrigger, ButtonSl = ConfigGamepadInputId.Unbound, ButtonSr = ConfigGamepadInputId.Unbound, }, LeftJoyconStick = new JoyconConfigControllerStick { Joystick = ConfigStickInputId.Left, StickButton = ConfigGamepadInputId.LeftStick, InvertStickX = false, InvertStickY = false, Rotate90CW = false, }, RightJoycon = new RightJoyconCommonConfig { ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B, ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A, ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y, ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X, ButtonPlus = ConfigGamepadInputId.Plus, ButtonR = ConfigGamepadInputId.RightShoulder, ButtonZr = ConfigGamepadInputId.RightTrigger, ButtonSl = ConfigGamepadInputId.Unbound, ButtonSr = ConfigGamepadInputId.Unbound, }, RightJoyconStick = new JoyconConfigControllerStick { Joystick = ConfigStickInputId.Right, StickButton = ConfigGamepadInputId.RightStick, InvertStickX = false, InvertStickY = false, Rotate90CW = false, }, Motion = new StandardMotionConfigController { MotionBackend = MotionInputBackendType.GamepadDriver, EnableMotion = true, Sensitivity = 100, GyroDeadzone = 1, }, Rumble = new RumbleConfigController { StrongRumble = 1f, WeakRumble = 1f, EnableRumble = false, }, }; } } else { string profileBasePath; if (isKeyboard) { profileBasePath = Path.Combine(AppDataManager.ProfilesDirPath, "keyboard"); } else { profileBasePath = Path.Combine(AppDataManager.ProfilesDirPath, "controller"); } string path = Path.Combine(profileBasePath, inputProfileName + ".json"); if (!File.Exists(path)) { Logger.Error?.Print(LogClass.Application, $"Input profile \"{inputProfileName}\" not found for \"{inputId}\""); return null; } try { config = JsonHelper.DeserializeFromFile(path, _serializerContext.InputConfig); } catch (JsonException) { Logger.Error?.Print(LogClass.Application, $"Input profile \"{inputProfileName}\" parsing failed for \"{inputId}\""); return null; } } config.Id = inputId; config.PlayerIndex = index; string inputTypeName = isKeyboard ? "Keyboard" : "Gamepad"; Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} configured with {inputTypeName} \"{config.Id}\""); // If both stick ranges are 0 (usually indicative of an outdated profile load) then both sticks will be set to 1.0. if (config is StandardControllerInputConfig controllerConfig) { if (controllerConfig.RangeLeft <= 0.0f && controllerConfig.RangeRight <= 0.0f) { controllerConfig.RangeLeft = 1.0f; controllerConfig.RangeRight = 1.0f; Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} stick range reset. Save the profile now to update your configuration"); } } return config; } static void Load(Options option) { if (_virtualFileSystem == null) { _virtualFileSystem = VirtualFileSystem.CreateInstance(); } if (_libHacHorizonManager == null) { _libHacHorizonManager = new LibHacHorizonManager(); _libHacHorizonManager.InitializeFsServer(_virtualFileSystem); _libHacHorizonManager.InitializeArpServer(); _libHacHorizonManager.InitializeBcatServer(); _libHacHorizonManager.InitializeSystemClients(); } if (_contentManager == null) { _contentManager = new ContentManager(_virtualFileSystem); } if (_accountManager == null) { _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile); } if (_userChannelPersistence == null) { _userChannelPersistence = new UserChannelPersistence(); } if (_inputManager == null) { _inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver()); } GraphicsConfig.EnableShaderCache = true; if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) { if (option.GraphicsBackend == GraphicsBackend.OpenGl) { option.GraphicsBackend = GraphicsBackend.Vulkan; Logger.Warning?.Print(LogClass.Application, "OpenGL is not supported on Apple platforms, switching to Vulkan!"); } } IGamepad gamepad; if (option.ListInputIds) { Logger.Info?.Print(LogClass.Application, "Input Ids:"); foreach (string id in _inputManager.KeyboardDriver.GamepadsIds) { gamepad = _inputManager.KeyboardDriver.GetGamepad(id); Logger.Info?.Print(LogClass.Application, $"- {id} (\"{gamepad.Name}\")"); gamepad.Dispose(); } foreach (string id in _inputManager.GamepadDriver.GamepadsIds) { gamepad = _inputManager.GamepadDriver.GetGamepad(id); Logger.Info?.Print(LogClass.Application, $"- {id} (\"{gamepad.Name}\")"); gamepad.Dispose(); } return; } if (option.InputPath == null) { Logger.Error?.Print(LogClass.Application, "Please provide a file to load"); return; } if (option.InputPath == "MiiMaker") { string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); option.InputPath = contentPath; } _inputConfiguration = new List(); _enableKeyboard = option.EnableKeyboard; _enableMouse = option.EnableMouse; static void LoadPlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index, Options option) { InputConfig inputConfig = HandlePlayerConfiguration(inputProfileName, inputId, index, option); if (inputConfig != null) { _inputConfiguration.Add(inputConfig); } } LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1, option); LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2, option); LoadPlayerConfiguration(option.InputProfile3Name, option.InputId3, PlayerIndex.Player3, option); LoadPlayerConfiguration(option.InputProfile4Name, option.InputId4, PlayerIndex.Player4, option); LoadPlayerConfiguration(option.InputProfile5Name, option.InputId5, PlayerIndex.Player5, option); LoadPlayerConfiguration(option.InputProfile6Name, option.InputId6, PlayerIndex.Player6, option); LoadPlayerConfiguration(option.InputProfile7Name, option.InputId7, PlayerIndex.Player7, option); LoadPlayerConfiguration(option.InputProfile8Name, option.InputId8, PlayerIndex.Player8, option); LoadPlayerConfiguration(option.InputProfileHandheldName, option.InputIdHandheld, PlayerIndex.Handheld, option); if (_inputConfiguration.Count == 0) { return; } // Setup logging level Logger.SetEnable(LogLevel.Debug, option.LoggingEnableDebug); 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.Trace, option.LoggingEnableTrace); Logger.SetEnable(LogLevel.Guest, !option.LoggingDisableGuest); Logger.SetEnable(LogLevel.AccessLog, option.LoggingEnableFsAccessLog); if (!option.DisableFileLog) { Logger.AddTarget(new AsyncLogTargetWrapper( new FileLogTarget(ReleaseInformation.GetBaseApplicationDirectory(), "file"), 1000, AsyncLogTargetOverflowAction.Block )); } // Setup graphics configuration GraphicsConfig.EnableShaderCache = !option.DisableShaderCache; GraphicsConfig.EnableTextureRecompression = option.EnableTextureRecompression; GraphicsConfig.ResScale = option.ResScale; GraphicsConfig.MaxAnisotropy = option.MaxAnisotropy; GraphicsConfig.ShadersDumpPath = option.GraphicsShadersDumpPath; GraphicsConfig.EnableMacroHLE = !option.DisableMacroHLE; while (true) { LoadApplication(option); if (_userChannelPersistence.PreviousIndex == -1 || !_userChannelPersistence.ShouldRestart) { break; } _userChannelPersistence.ShouldRestart = false; } _inputManager.Dispose(); } private static void SetupProgressHandler() { if (_emulationContext.Processes.ActiveApplication.DiskCacheLoadState != null) { _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged -= ProgressHandler; _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged += ProgressHandler; } _emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler; _emulationContext.Gpu.ShaderCacheStateChanged += ProgressHandler; } private static void ProgressHandler(T state, int current, int total) where T : Enum { string label = state switch { LoadState => $"PTC : {current}/{total}", ShaderCacheState => $"Shaders : {current}/{total}", _ => throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}"), }; Logger.Info?.Print(LogClass.Application, label); } private static WindowBase CreateWindow(Options options) { return options.GraphicsBackend == GraphicsBackend.Vulkan ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode) : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode); } private static IRenderer CreateRenderer(Options options, WindowBase window) { if (options.GraphicsBackend == GraphicsBackend.Vulkan && window is VulkanWindow vulkanWindow) { string preferredGpuId = string.Empty; Vk api = Vk.GetApi(); if (!string.IsNullOrEmpty(options.PreferredGPUVendor)) { string preferredGpuVendor = options.PreferredGPUVendor.ToLowerInvariant(); var devices = VulkanRenderer.GetPhysicalDevices(api); foreach (var device in devices) { if (device.Vendor.ToLowerInvariant() == preferredGpuVendor) { preferredGpuId = device.Id; break; } } } return new VulkanRenderer( api, (instance, vk) => new SurfaceKHR((ulong)(vulkanWindow.CreateWindowSurface(instance.Handle))), vulkanWindow.GetRequiredInstanceExtensions, preferredGpuId); } return new OpenGLRenderer(); } private static Switch InitializeEmulationContext(WindowBase window, IRenderer renderer, Options options) { BackendThreading threadingMode = options.BackendThreading; bool threadedGAL = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading); if (threadedGAL) { renderer = new ThreadedRenderer(renderer); } bool AppleHV = false; if ((!OperatingSystem.IsIOSVersionAtLeast(16, 4)) && options.UseHypervisor) { AppleHV = true; } else if (OperatingSystem.IsIOS()) { AppleHV = false; } else { AppleHV = options.UseHypervisor; } HLEConfiguration configuration = new(_virtualFileSystem, _libHacHorizonManager, _contentManager, _accountManager, _userChannelPersistence, renderer, new SDL2HardwareDeviceDriver(), options.ExpandRAM ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB, window, options.SystemLanguage, options.SystemRegion, !options.DisableVSync, !options.DisableDockedMode, !options.DisablePTC, options.EnableInternetAccess, !options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None, options.FsGlobalAccessLogMode, options.SystemTimeOffset, options.SystemTimeZone, options.MemoryManagerMode, options.IgnoreMissingServices, options.AspectRatio, options.AudioVolume, AppleHV, options.MultiplayerLanInterfaceId, Common.Configuration.Multiplayer.MultiplayerMode.LdnMitm); return new Switch(configuration); } private static void ExecutionEntrypoint() { if (OperatingSystem.IsWindows()) { _windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1); } DisplaySleep.Prevent(); _window.Initialize(_emulationContext, _inputConfiguration, _enableKeyboard, _enableMouse); _window.Execute(); _emulationContext.Dispose(); _window.Dispose(); if (OperatingSystem.IsWindows()) { _windowsMultimediaTimerResolution?.Dispose(); _windowsMultimediaTimerResolution = null; } } private static bool LoadApplication(Options options) { string path = options.InputPath; Logger.RestartTime(); WindowBase window = CreateWindow(options); IRenderer renderer = CreateRenderer(options, window); _window = window; _window.IsFullscreen = options.IsFullscreen; _window.DisplayId = options.DisplayId; _window.IsExclusiveFullscreen = options.IsExclusiveFullscreen; _window.ExclusiveFullscreenWidth = options.ExclusiveFullscreenWidth; _window.ExclusiveFullscreenHeight = options.ExclusiveFullscreenHeight; _window.AntiAliasing = options.AntiAliasing; _window.ScalingFilter = options.ScalingFilter; _window.ScalingFilterLevel = options.ScalingFilterLevel; _emulationContext = InitializeEmulationContext(window, renderer, options); SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion(); Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}"); bool isFirmwareTitle = false; if (path.StartsWith("@SystemContent")) { path = VirtualFileSystem.SwitchPathToSystemPath(path); isFirmwareTitle = true; } if (Directory.Exists(path)) { string[] romFsFiles = Directory.GetFiles(path, "*.istorage"); if (romFsFiles.Length == 0) { romFsFiles = Directory.GetFiles(path, "*.romfs"); } if (romFsFiles.Length > 0) { Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS."); if (!_emulationContext.LoadCart(path, romFsFiles[0])) { _emulationContext.Dispose(); return false; } } else { Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS."); if (!_emulationContext.LoadCart(path)) { _emulationContext.Dispose(); return false; } } } else if (File.Exists(path)) { switch (Path.GetExtension(path).ToLowerInvariant()) { case ".xci": Logger.Info?.Print(LogClass.Application, "Loading as XCI."); if (!_emulationContext.LoadXci(path)) { _emulationContext.Dispose(); return false; } break; case ".nca": Logger.Info?.Print(LogClass.Application, "Loading as NCA."); if (!_emulationContext.LoadNca(path)) { _emulationContext.Dispose(); return false; } break; case ".nsp": case ".pfs0": Logger.Info?.Print(LogClass.Application, "Loading as NSP."); if (!_emulationContext.LoadNsp(path)) { _emulationContext.Dispose(); return false; } break; default: if (isFirmwareTitle) { Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA)."); if (!_emulationContext.LoadNca(path)) { _emulationContext.Dispose(); return false; } } else { Logger.Info?.Print(LogClass.Application, "Loading as Homebrew."); try { if (!_emulationContext.LoadProgram(path)) { _emulationContext.Dispose(); return false; } } catch (ArgumentOutOfRangeException) { Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx."); _emulationContext.Dispose(); return false; } } break; } } else { Logger.Warning?.Print(LogClass.Application, $"Couldn't load '{options.InputPath}'. Please specify a valid XCI/NCA/NSP/PFS0/NRO file."); _emulationContext.Dispose(); return false; } SetupProgressHandler(); Translator.IsReadyForTranslation.Reset(); ExecutionEntrypoint(); return true; } private static FileStream OpenFile(int descriptor) { var safeHandle = new SafeFileHandle(descriptor, false); return new FileStream(safeHandle, FileAccess.ReadWrite); } public class GameInfo { public double FileSize; public string? TitleName; public string? TitleId; public string? Developer; public string? Version; public byte[]? Icon; } public unsafe struct DlcNcaListItem { public fixed byte Path[256]; public ulong TitleId; } public unsafe struct DlcNcaList { public bool success; public uint size; public unsafe DlcNcaListItem* items; } public unsafe struct GameInfoNative { public ulong FileSize; public fixed byte TitleName[512]; public fixed byte TitleId[32]; public fixed byte Developer[256]; public fixed byte Version[16]; public byte* ImageData; public uint ImageSize; public GameInfoNative(ulong fileSize, string titleName, string titleId, string developer, string version, byte[] imageData) { FileSize = fileSize; fixed (byte* titleNamePtr = TitleName) fixed (byte* titleIdPtr = TitleId) fixed (byte* developerPtr = Developer) fixed (byte* versionPtr = Version) { CopyStringToFixedArray(titleName, titleNamePtr, 512); CopyStringToFixedArray(titleId, titleIdPtr, 32); CopyStringToFixedArray(developer, developerPtr, 256); CopyStringToFixedArray(version, versionPtr, 16); } if (imageData == null || imageData.Length > 4096 * 4096) { ImageSize = 0; ImageData = null; } else { ImageSize = (uint)imageData.Length; ImageData = (byte*)Marshal.AllocHGlobal(imageData.Length); Marshal.Copy(imageData, 0, (IntPtr)ImageData, imageData.Length); } } // Free allocated memory for ImageData public void Dispose() { if (ImageData != null) { Marshal.FreeHGlobal((IntPtr)ImageData); ImageData = null; } } } private static unsafe void CopyStringToFixedArray(string source, byte* destination, int length) { var span = new Span(destination, length); span.Clear(); Encoding.UTF8.GetBytes(source, span); } } }