diff --git a/Directory.Packages.props b/Directory.Packages.props index fba3792db..9e3e9c97d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,13 +3,13 @@ true - - - - - - - + + + + + + + @@ -22,7 +22,7 @@ - + diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index c427ea78b..ac9aa09a4 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -575,6 +575,31 @@ "zh_TW": "停止模擬" } }, + { + "ID": "MenuBarOptionsRestartEmulation", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Restart Emulation", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "MenuBarOptionsSettings", "Translations": { @@ -24876,4 +24901,4 @@ } } ] -} +} \ No newline at end of file diff --git a/src/Ryujinx.Common/Utilities/OsUtils.cs b/src/Ryujinx.Common/Utilities/OsUtils.cs index 29c6e187c..79fe8b722 100644 --- a/src/Ryujinx.Common/Utilities/OsUtils.cs +++ b/src/Ryujinx.Common/Utilities/OsUtils.cs @@ -22,10 +22,11 @@ namespace Ryujinx.Common.Utilities } // "dumpable" attribute of the calling process + private const int PR_GET_DUMPABLE = 3; private const int PR_SET_DUMPABLE = 4; - [DllImport("libc", SetLastError = true)] - private static extern int prctl(int option, int arg2); + [LibraryImport("libc", SetLastError = true)] + private static partial int prctl(int option, int arg2); public static void SetCoreDumpable(bool dumpable) { @@ -36,5 +37,13 @@ namespace Ryujinx.Common.Utilities Debug.Assert(result == 0); } } + + // Use the below line to display dumpable status in the console: + // Console.WriteLine($"{OsUtils.IsCoreDumpable()}"); + public static bool IsCoreDumpable() + { + int result = prctl(PR_GET_DUMPABLE, 0); + return result == 1; + } } } diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index d0fe0f1a7..d6eedd32f 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -488,6 +488,8 @@ namespace Ryujinx.HLE.FileSystem if (keyPaths.Length is 0) throw new FileNotFoundException($"Directory '{keysSource}' contained no '.keys' files."); + List failedFiles = new(); + foreach (string filePath in keyPaths) { try @@ -497,17 +499,20 @@ namespace Ryujinx.HLE.FileSystem catch (Exception e) { Logger.Error?.Print(LogClass.Application, e.Message); + failedFiles.Add(Path.GetFileName(filePath)); continue; } string destPath = Path.Combine(installDirectory, Path.GetFileName(filePath)); - if (File.Exists(destPath)) - File.Delete(destPath); - File.Copy(filePath, destPath, true); } + if (failedFiles.Count > 0) + { + throw new InvalidOperationException($"Failed to install the following key files: {string.Join(", ", failedFiles)}"); + } + return; } @@ -518,8 +523,6 @@ namespace Ryujinx.HLE.FileSystem FileInfo info = new(keysSource); - using FileStream file = File.OpenRead(keysSource); - if (info.Extension is not ".keys") throw new InvalidFirmwarePackageException("Input file extension is not .keys"); @@ -534,10 +537,6 @@ namespace Ryujinx.HLE.FileSystem string dest = Path.Combine(installDirectory, info.Name); - if (File.Exists(dest)) - File.Delete(dest); - - // overwrite: true seems to not work on its own? https://github.com/Ryubing/Issues/issues/189 File.Copy(keysSource, dest, true); } @@ -1059,7 +1058,7 @@ namespace Ryujinx.HLE.FileSystem } } - public static bool AreKeysAlredyPresent(string pathToCheck) + public static bool AreKeysAlreadyPresent(string pathToCheck) { string[] fileNames = ["prod.keys", "title.keys", "console.keys", "dev.keys"]; foreach (string file in fileNames) diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs index b9235b033..990b2e288 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs @@ -56,6 +56,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid _activeCount = 0; JoyHold = NpadJoyHoldType.Vertical; + SixAxisActive = false; } internal ref KEvent GetStyleSetUpdateEvent(PlayerIndex player) @@ -580,6 +581,24 @@ namespace Ryujinx.HLE.HOS.Services.Hid return needUpdateRight; } + + public bool isAtRest(int playerNumber) + { + + ref NpadInternalState currentNpad = ref _device.Hid.SharedMemory.Npads[playerNumber].InternalState; + ref SixAxisSensorState storage = ref GetSixAxisSensorLifo(ref currentNpad, false).GetCurrentEntryRef(); + + float acceleration = Math.Abs(storage.Acceleration.X) + + Math.Abs(storage.Acceleration.Y) + + Math.Abs(storage.Acceleration.Z); + + float angularVelocity = Math.Abs(storage.AngularVelocity.X) + + Math.Abs(storage.AngularVelocity.Y) + + Math.Abs(storage.AngularVelocity.Z); + + // TODO: check against config deadzone and add sensitivity setting + return ((acceleration <= 1.0F) && (angularVelocity <= 1.0F)); + } private void UpdateDisconnectedInputSixAxis(PlayerIndex index) { diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs index 28db75663..08e2a2d68 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs @@ -602,19 +602,33 @@ namespace Ryujinx.HLE.HOS.Services.Hid } [CommandCmif(82)] - // IsSixAxisSensorAtRest(nn::hid::SixAxisSensorHandle, nn::applet::AppletResourceUserId) -> bool IsAsRest + // IsSixAxisSensorAtRest(nn::hid::SixAxisSensorHandle, nn::applet::AppletResourceUserId) -> bool IsAtRest public ResultCode IsSixAxisSensorAtRest(ServiceCtx context) { int sixAxisSensorHandle = context.RequestData.ReadInt32(); + + // 4 byte struct w/ 4-byte alignment + + // uint typeValue = (uint) sixAxisSensorHandle; // 0x0 0x4 TypeValue + // uint npadStyleIndex = (uint) sixAxisSensorHandle & 0xff; // 0x0 0x1 NpadStyleIndex + int playerNumber = (sixAxisSensorHandle << 8) & 0xff; // 0x1 0x1 PlayerNumber + // uint deviceIdx= ((uint) sixAxisSensorHandle << 16) & 0xff; // 0x2 0x1 DeviceIdx + // uint unknown = ((uint) sixAxisSensorHandle << 24) & 0xff; + + // 32bit sign extension padding -> if = 0, + offset, else - offset + + // npadStyleIndex = ((npadStyleIndex & 0x8000) == 0) ? npadStyleIndex | 0xFFFF0000 : npadStyleIndex & 0xFFFF0000; + // playerNumber = ((playerNumber & 0x8000) == 0) ? playerNumber | 0xFFFF0000 : playerNumber & 0xFFFF0000; + // deviceIdx = ((deviceIdx & 0x8000) == 0) ? deviceIdx | 0xFFFF0000 : deviceIdx & 0xFFFF0000; + // unknown = ((unknown & 0x8000) == 0) ? unknown | 0xFFFF0000 : unknown & 0xFFFF0000; + context.RequestData.BaseStream.Position += 4; // Padding long appletResourceUserId = context.RequestData.ReadInt64(); - - bool isAtRest = true; - - context.ResponseData.Write(isAtRest); - - Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, isAtRest }); - + + // TODO: link to context.Device.Hid.Npads.SixAxisActive when properly implemented + // We currently do not support stopping or starting SixAxisTracking. + + context.ResponseData.Write(context.Device.Hid.Npads.isAtRest(playerNumber)); return ResultCode.Success; } @@ -629,7 +643,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid context.ResponseData.Write(_isFirmwareUpdateAvailableForSixAxisSensor); Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _isFirmwareUpdateAvailableForSixAxisSensor }); - + return ResultCode.Success; } diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/ISslService.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/ISslService.cs index 8f38f293f..3d8fb5d51 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ssl/ISslService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/ISslService.cs @@ -17,7 +17,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl public ISslService(ServiceCtx context) { } [CommandCmif(0)] - // CreateContext(nn::ssl::sf::SslVersion, u64, pid) -> object + // CreateContext(nn::ssl::sf::SslVersion, u64 pid_placeholder, pid) -> object public ResultCode CreateContext(ServiceCtx context) { SslVersion sslVersion = (SslVersion)context.RequestData.ReadUInt32(); @@ -126,14 +126,18 @@ namespace Ryujinx.HLE.HOS.Services.Ssl } [CommandCmif(100)] - // CreateContextForSystem(u64 pid, nn::ssl::sf::SslVersion, u64) + // CreateContextForSystem(nn::ssl::sf::SslVersion, u64 pid_placeholder, pid) -> object public ResultCode CreateContextForSystem(ServiceCtx context) { - ulong pid = context.RequestData.ReadUInt64(); SslVersion sslVersion = (SslVersion)context.RequestData.ReadUInt32(); +#pragma warning disable IDE0059 // Remove unnecessary value assignment ulong pidPlaceholder = context.RequestData.ReadUInt64(); +#pragma warning restore IDE0059 - Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { pid, sslVersion, pidPlaceholder }); + // Note: We use ISslContext here instead of ISslContextForSystem class because Ryujinx implements both in one class. + MakeObject(context, new ISslContext(context.Request.HandleDesc.PId, sslVersion)); + + Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { sslVersion }); return ResultCode.Success; } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 8d03f81da..d1b85c6b0 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -42,6 +42,7 @@ namespace Ryujinx.Ava public static bool PreviewerDetached { get; private set; } public static bool UseHardwareAcceleration { get; private set; } public static string BackendThreadingArg { get; private set; } + public static bool CoreDumpArg { get; private set; } private const uint MbIconwarning = 0x30; @@ -81,6 +82,8 @@ namespace Ryujinx.Ava bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui"); bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps"); + CoreDumpArg = coreDumpArg; + // 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. diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 715460274..489b2c313 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -49,7 +49,7 @@ - + diff --git a/src/Ryujinx/UI/RyujinxApp.axaml.cs b/src/Ryujinx/UI/RyujinxApp.axaml.cs index c778f27fb..22b98dea7 100644 --- a/src/Ryujinx/UI/RyujinxApp.axaml.cs +++ b/src/Ryujinx/UI/RyujinxApp.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Markup.Xaml; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Threading; +using FluentAvalonia.Core; using FluentAvalonia.UI.Windowing; using Gommon; using Ryujinx.Ava.Common.Locale; @@ -53,6 +54,9 @@ namespace Ryujinx.Ava { Name = FormatTitle(); + // Disable menu animations + FAUISettings.SetAnimationsEnabledAtAppLevel(false); + AvaloniaXamlLoader.Load(this); if (OperatingSystem.IsMacOS()) diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 48e18a12e..ae84a15a2 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -174,6 +174,7 @@ namespace Ryujinx.Ava.UI.ViewModels private string _screenshotKey = "F8"; private float _volume; private ApplicationData _currentApplicationData; + private bool _pendingRestart; private readonly AutoResetEvent _rendererWaitEvent; private int _customVSyncInterval; private int _customVSyncIntervalPercentageProxy; @@ -1062,7 +1063,7 @@ namespace Ryujinx.Ava.UI.ViewModels string dialogMessage = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallMessage); - if (ContentManager.AreKeysAlredyPresent(systemDirectory)) + if (ContentManager.AreKeysAlreadyPresent(systemDirectory)) { dialogMessage += LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys @@ -1250,6 +1251,14 @@ namespace Ryujinx.Ava.UI.ViewModels await LoadApplication(_currentApplicationData); } + else if (_pendingRestart) + { + _pendingRestart = false; + + Logger.Info?.Print(LogClass.Application, $"Restarting emulation for '{_currentApplicationData.Name}'"); + + await LoadApplication(_currentApplicationData); + } else { // Otherwise, clear state. @@ -1258,6 +1267,21 @@ namespace Ryujinx.Ava.UI.ViewModels } } + public void RestartEmulation() + { + if (AppHost is null || _currentApplicationData is null) + { + Logger.Warning?.Print(LogClass.Application, "RestartEmulation called but no application is running."); + + return; + } + + Logger.Info?.Print(LogClass.Application, $"Restart requested for '{_currentApplicationData.Name}'"); + + _pendingRestart = true; + AppHost.Stop(); + } + private void Update_StatusBar(object sender, StatusUpdatedEventArgs args) { if (ShowMenuAndStatusBar && !ShowLoadProgress) diff --git a/src/Ryujinx/UI/Views/Input/LedInputView.axaml b/src/Ryujinx/UI/Views/Input/LedInputView.axaml index c6319f424..327f27718 100644 --- a/src/Ryujinx/UI/Views/Input/LedInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/LedInputView.axaml @@ -47,18 +47,13 @@ - - + diff --git a/src/Ryujinx/UI/Views/Input/LedInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/LedInputView.axaml.cs index dab553f82..d0407beb3 100644 --- a/src/Ryujinx/UI/Views/Input/LedInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/LedInputView.axaml.cs @@ -1,4 +1,5 @@ using Avalonia; +using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Controls; @@ -30,19 +31,17 @@ namespace Ryujinx.UI.Views.Input InitializeComponent(); } - private void ColorPickerButton_OnColorChanged(ColorPickerButton sender, ColorButtonColorChangedEventArgs args) + private void ColorPicker_OnColorChanged(object sender, ColorChangedEventArgs args) { - if (!args.NewColor.HasValue) - return; if (!ViewModel.EnableLedChanging) return; if (ViewModel.TurnOffLed) return; - ViewModel.ParentModel.SelectedGamepad.SetLed(args.NewColor.Value.ToUInt32()); + ViewModel.ParentModel.SelectedGamepad.SetLed(args.NewColor.ToUInt32()); } - private void ColorPickerButton_OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) + private void ColorPicker_OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) { if (!ViewModel.EnableLedChanging) return; diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index d5a59c181..5e41017d8 100755 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -167,6 +167,12 @@ Icon="{ext:Icon fa-solid fa-stop}" InputGesture="Escape" IsEnabled="{Binding IsGameRunning}" /> + ViewModel.AppHost?.Pause()); ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume()); StopEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted()); + RestartEmulationMenuItem.Command = Commands.Create(() => ViewModel.RestartEmulation()); CheatManagerMenuItem.Command = Commands.CreateSilentFail(OpenCheatManagerForCurrentApp); InstallFileTypesMenuItem.Command = Commands.Create(InstallFileTypes); UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes); diff --git a/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml b/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml index c22624fd5..9028d5802 100644 --- a/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml +++ b/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml @@ -78,22 +78,16 @@ Spacing="10" Margin="0 24 0 0" HorizontalAlignment="Right"> - - + - - + +