From 8705fabdb012c201baeb0dc578373753fbf9ae13 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 2 May 2026 02:30:57 +0000 Subject: [PATCH 01/61] UI: LoadGuestApplication asynchronous cancellation (#1) Fixed LoadGuestApplication hanging when cancelled. Since startup procedure has technically changed, we should consider testing this with a variety of game formats to ensure regressions do not occur. Closes [#20](https://github.com/Ryubing/Issues/issues/20) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/1 --- src/Ryujinx.Graphics.Vulkan/Auto.cs | 5 +- .../Loaders/Processes/ProcessLoader.cs | 10 +- .../Loaders/Processes/ProcessResult.cs | 11 ++- src/Ryujinx/Systems/AppHost.cs | 98 ++++++++++++------- .../Systems/AppLibrary/ApplicationLibrary.cs | 16 ++- .../Systems/DiscordIntegrationModule.cs | 2 +- src/Ryujinx/UI/Models/SaveModel.cs | 11 ++- .../UI/ViewModels/MainWindowViewModel.cs | 31 ++++-- 8 files changed, 129 insertions(+), 55 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/Auto.cs b/src/Ryujinx.Graphics.Vulkan/Auto.cs index 7ce309a5d..9c8c7ff4b 100644 --- a/src/Ryujinx.Graphics.Vulkan/Auto.cs +++ b/src/Ryujinx.Graphics.Vulkan/Auto.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using System; using System.Diagnostics; using System.Threading; @@ -114,7 +115,7 @@ namespace Ryujinx.Graphics.Vulkan cbs.AddDependant(this); // We need to add a dependency on the command buffer to all objects this object - // references aswell. + // references as well. if (_referencedObjs != null) { for (int i = 0; i < _referencedObjs.Length; i++) @@ -176,6 +177,8 @@ namespace Ryujinx.Graphics.Vulkan } } + // This can somehow become -1. + // Logger.Info?.PrintMsg(LogClass.Gpu, $"_referenceCount: {_referenceCount}"); Debug.Assert(_referenceCount >= 0); } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 48b5b724c..e0edd2df5 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -14,6 +14,7 @@ using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Processes.Extensions; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using Path = System.IO.Path; @@ -27,10 +28,15 @@ namespace Ryujinx.HLE.Loaders.Processes private ulong _latestPid; - public ProcessResult ActiveApplication + public ProcessResult? ActiveApplication { get { + return _processesByPid.GetValueOrDefault(_latestPid); + + // Using this if statement locks up the UI and prevents a new game from loading. + // Haven't quite deduced why yet. + if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value)) throw new RyujinxException( $"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?"); @@ -144,7 +150,7 @@ namespace Ryujinx.HLE.Loaders.Processes public bool LoadUnpackedNca(string exeFsDirPath, string romFsPath = null) { ProcessResult processResult = new LocalFileSystem(exeFsDirPath).Load(_device, romFsPath); - + if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult)) { if (processResult.Start(_device)) diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index d6e492317..66bdd57ef 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -4,7 +4,6 @@ using LibHac.Ns; using Ryujinx.Common.Logging; using Ryujinx.Cpu; using Ryujinx.HLE.HOS.SystemState; -using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.Horizon.Common; namespace Ryujinx.HLE.Loaders.Processes @@ -52,6 +51,7 @@ namespace Ryujinx.HLE.Loaders.Processes if (metaLoader is not null) { + Logger.Info?.Print(LogClass.Application,$"metaLoader: {metaLoader}"); ulong programId = metaLoader.ProgramId; Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString(); @@ -71,8 +71,15 @@ namespace Ryujinx.HLE.Loaders.Processes ProgramId = programId; ProgramIdText = $"{programId:x16}"; Is64Bit = metaLoader.IsProgram64Bit; + } + + else + { + Logger.Error?.Print(LogClass.Application,$"metaLoader is null !!!"); + ProcessId = 0; + return; } - + DiskCacheEnabled = diskCacheEnabled; AllowCodeMemoryForJit = allowCodeMemoryForJit; } diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 4b1e9cdb5..6675972be 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -62,7 +62,7 @@ using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.Ava.Systems { - internal class AppHost + internal class AppHost : IDisposable { private const int CursorHideIdleTime = 5; // Hide Cursor seconds. private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping. @@ -438,7 +438,7 @@ namespace Ryujinx.Ava.Systems SaveBitmapAsPng(bitmapToSave, path); - Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot"); + Logger.Notice.Print(LogClass.Application, $"Screenshot saved to '{path}'.", "Screenshot"); } }); } @@ -611,27 +611,40 @@ namespace Ryujinx.Ava.Systems _isActive = false; - // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose. - // We only need to wait for all commands submitted during the main gpu loop to be processed. - _gpuDoneEvent.WaitOne(); - _gpuDoneEvent.Dispose(); - DisplaySleep.Restore(); NpadManager.Dispose(); TouchScreenManager.Dispose(); Device.Dispose(); + + // NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose. + // We only need to wait for all commands submitted during the main gpu loop to be processed. + // If the GPU has no work and is cancelled, we need to handle that as well. + WaitHandle.WaitAny(new[] { _gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle }); + _gpuCancellationTokenSource.Dispose(); + + // Waiting for work to be finished before we dispose. + if (_renderingStarted) + { + Device.Gpu.WaitUntilGpuReady(); + } + + _gpuDoneEvent.Dispose(); + DisposeGpu(); - AppExit?.Invoke(this, EventArgs.Empty); } - private void Dispose() + // MUST be public to inherit from IDisposable + public void Dispose() { if (Device.Processes != null) - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _playTimer.Elapsed); - + { + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText, + _playTimer.Elapsed); + } + ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState; @@ -646,7 +659,6 @@ namespace Ryujinx.Ava.Systems _topLevel.PointerExited -= TopLevel_PointerExited; _gpuCancellationTokenSource.Cancel(); - _gpuCancellationTokenSource.Dispose(); _chrono.Stop(); _playTimer.Stop(); @@ -672,6 +684,12 @@ namespace Ryujinx.Ava.Systems } else { + // No use waiting on something that never started work + if (_renderingStarted) + { + Device.Gpu.WaitUntilGpuReady(); + } + Device.DisposeGpu(); } } @@ -686,7 +704,7 @@ namespace Ryujinx.Ava.Systems _cursorState = CursorStates.ForceChangeCursor; } - public async Task LoadGuestApplication(BlitStruct? customNacpData = null) + public async Task LoadGuestApplication(CancellationTokenSource cts, BlitStruct? customNacpData = null) { DiscordIntegrationModule.GuestAppStartedAt = Timestamps.Now; @@ -715,7 +733,8 @@ namespace Ryujinx.Ava.Systems await UserErrorDialog.ShowUserErrorDialog(userError); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } @@ -724,10 +743,11 @@ namespace Ryujinx.Ava.Systems await UserErrorDialog.ShowUserErrorDialog(userError); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } - // Tell the user that we installed a firmware for them. + // Tell the user that we installed firmware for them. if (userError is UserError.NoFirmware) { firmwareVersion = ContentManager.GetCurrentFirmwareVersion(); @@ -747,7 +767,8 @@ namespace Ryujinx.Ava.Systems await UserErrorDialog.ShowUserErrorDialog(userError); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } } @@ -762,7 +783,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } else if (Directory.Exists(ApplicationPath)) @@ -782,20 +804,24 @@ namespace Ryujinx.Ava.Systems if (!Device.LoadCart(ApplicationPath, romFsFiles[0])) { + await ContentDialogHelper.CreateErrorDialog( + "Please specify an unpacked game directory with a valid exefs or NSO/NRO."); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } else { Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS."); - if (!Device.LoadCart(ApplicationPath)) { + await ContentDialogHelper.CreateErrorDialog( + "Please specify an unpacked game directory with a valid exefs or NSO/NRO."); Device.Dispose(); - - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } } @@ -813,7 +839,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -826,7 +853,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -840,7 +868,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -855,7 +884,8 @@ namespace Ryujinx.Ava.Systems { Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } } catch (ArgumentOutOfRangeException) @@ -864,7 +894,8 @@ namespace Ryujinx.Ava.Systems Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } break; @@ -873,19 +904,18 @@ namespace Ryujinx.Ava.Systems } else { - Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file."); + Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NSO/NRO file."); Device.Dispose(); - return false; + cts.Cancel(); + throw new OperationCanceledException(cts.Token); } ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => appMetadata.UpdatePreGame() ); _playTimer.Start(); - - return true; } internal void Resume() @@ -895,7 +925,7 @@ namespace Ryujinx.Ava.Systems _viewModel.IsPaused = false; _playTimer.Start(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI); - Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed"); + Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed."); } internal void Pause() @@ -905,7 +935,7 @@ namespace Ryujinx.Ava.Systems _viewModel.IsPaused = true; _playTimer.Stop(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]); - Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); + Logger.Info?.Print(LogClass.Emulation, "Emulation was paused."); } private void InitEmulatedSwitch() @@ -1104,7 +1134,9 @@ namespace Ryujinx.Ava.Systems // Make sure all commands in the run loop are fully executed before leaving the loop. if (Device.Gpu.Renderer is ThreadedRenderer threaded) { + Logger.Info?.PrintMsg(LogClass.Gpu, "Flushing threaded commands..."); threaded.FlushThreadedCommands(); + Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!"); } _gpuDoneEvent.Set(); diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs index a36ba7f30..a397e48cc 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs @@ -849,7 +849,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary foreach (ApplicationData installedApplication in Applications.Items) { - temporary += LoadAndSaveMetaData(installedApplication.IdString).TimePlayed; + // this should always exist... should... + temporary += LoadAndSaveMetaData(installedApplication.IdString).Value.TimePlayed; } TotalTimePlayed = temporary; @@ -1159,15 +1160,22 @@ namespace Ryujinx.Ava.Systems.AppLibrary ApplicationCountUpdated?.Invoke(null, e); } - public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) + public static Gommon.Optional LoadAndSaveMetaData(string titleId, Action modifyFunction = null) { + if (titleId is null) + { + Logger.Warning?.PrintMsg(LogClass.Application, "Cannot save metadata because title ID is invalid."); + return null; + } + string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); string metadataFile = Path.Combine(metadataFolder, "metadata.json"); ApplicationMetadata appMetadata; - + if (!File.Exists(metadataFile)) { + Logger.Info?.Print(LogClass.Application, $"Metadata file does not exist. Creating metadata for {titleId}..."); Directory.CreateDirectory(metadataFolder); appMetadata = new ApplicationMetadata(); @@ -1177,12 +1185,12 @@ namespace Ryujinx.Ava.Systems.AppLibrary try { + Logger.Debug?.Print(LogClass.Application, $"Deserializing metadata for {titleId}..."); appMetadata = JsonHelper.DeserializeFromFile(metadataFile, _serializerContext.ApplicationMetadata); } catch (JsonException) { Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults."); - appMetadata = new ApplicationMetadata(); } diff --git a/src/Ryujinx/Systems/DiscordIntegrationModule.cs b/src/Ryujinx/Systems/DiscordIntegrationModule.cs index 5b61340b6..da6371682 100644 --- a/src/Ryujinx/Systems/DiscordIntegrationModule.cs +++ b/src/Ryujinx/Systems/DiscordIntegrationModule.cs @@ -82,7 +82,7 @@ namespace Ryujinx.Ava.Systems public static void Use(Optional titleId) { - if (titleId.TryGet(out string tid)) + if (titleId.TryGet(out string tid) && Switch.Shared.Processes.ActiveApplication is not null) SwitchToPlayingState( ApplicationLibrary.LoadAndSaveMetaData(tid), Switch.Shared.Processes.ActiveApplication diff --git a/src/Ryujinx/UI/Models/SaveModel.cs b/src/Ryujinx/UI/Models/SaveModel.cs index d245ed4d9..bd9c93f5d 100644 --- a/src/Ryujinx/UI/Models/SaveModel.cs +++ b/src/Ryujinx/UI/Models/SaveModel.cs @@ -57,8 +57,15 @@ namespace Ryujinx.Ava.UI.Models } else { - ApplicationMetadata appMetadata = ApplicationLibrary.LoadAndSaveMetaData(TitleIdString); - Title = appMetadata.Title ?? TitleIdString; + Gommon.Optional appMetadata = ApplicationLibrary.LoadAndSaveMetaData(TitleIdString); + if (appMetadata != null) + { + Title = appMetadata.Value.Title ?? TitleIdString; + } + else + { + Title = ""; + } } Task.Run(() => diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index ae84a15a2..9e139c186 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1760,11 +1760,6 @@ namespace Ryujinx.Ava.UI.ViewModels Logger.RestartTime(); - SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, - ConfigurationState.Instance.System.Language, application.Id); - - PrepareLoadScreen(); - RendererHostControl = new RendererHost(); AppHost = new AppHost( @@ -1778,18 +1773,34 @@ namespace Ryujinx.Ava.UI.ViewModels UserChannelPersistence, this, TopLevel); + + CancellationTokenSource cts = new CancellationTokenSource(); - if (!await AppHost.LoadGuestApplication(customNacpData)) + try { + await AppHost.LoadGuestApplication(cts, customNacpData); + } + catch (OperationCanceledException exception) + { + Logger.Info?.Print(LogClass.Application, + "LoadGuestApplication was interrupted !!! " + exception.Message); AppHost.DisposeContext(); AppHost = null; - return; } - + finally + { + cts.Dispose(); + } + CanUpdate = false; application.Name ??= AppHost.Device.Processes.ActiveApplication.Name; + + SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, + ConfigurationState.Instance.System.Language, application.Id); + + PrepareLoadScreen(); LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, application.Name); @@ -1811,9 +1822,9 @@ namespace Ryujinx.Ava.UI.ViewModels RendererHostControl.Focus(); }); - public static void UpdateGameMetadata(string titleId, TimeSpan playTime) + public static void UpdateGameMetadata(string titleId, TimeSpan playTime) => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); - + public void RefreshFirmwareStatus() { SystemVersion version = null; From 47c0180ba416a6412586e8f9b2d8c927a8d68bd1 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 2 May 2026 02:49:59 +0000 Subject: [PATCH 02/61] [CPU] Increased base JIT cache size (#22) @MaxLastBreath found that The Legend of Zelda: Tears of the Kingdom experiences inconsistent crashing with a limited JIT cache size. Increasing the cache size seems to help with this, and increasing it shouldn't degrade performance for other titles. @LotP plans to look this over later, but for now, this should suffice as a fix for affected users. Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/22 --- src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs index 4ab54ecb7..a96e3554a 100644 --- a/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs +++ b/src/Ryujinx.Cpu/LightningJit/Cache/JitCache.cs @@ -17,7 +17,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache private static readonly int _pageMask = _pageSize - 1; private const int CodeAlignment = 4; // Bytes. - private const int CacheSize = 256 * 1024 * 1024; + // TODO: JIT Cache size should be application dependent, not global. + private const int CacheSize = 1024 * (1024 * 1024); // Megabytes * Size of Megabytes (since its in bytes). private static JitCacheInvalidation _jitCacheInvalidator; @@ -33,6 +34,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache [SupportedOSPlatform("windows")] [LibraryImport("kernel32.dll", SetLastError = true)] public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize); + + [SupportedOSPlatform("macos")] + [LibraryImport("libSystem.dylib", EntryPoint = "sys_icache_invalidate")] + internal static partial void SysICacheInvalidate(nint start, nuint len); + + [SupportedOSPlatform("linux")] + [LibraryImport("libgcc_s.so.1", EntryPoint = "__clear_cache")] + internal static partial void ClearCache(nint begin, nint end); public static void Initialize(IJitMemoryAllocator allocator) { From 0c0b6404b141ecd7e2087cf36d41d621c5ce2824 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 2 May 2026 03:34:38 +0000 Subject: [PATCH 03/61] Configure Renovate (#49) This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Co-authored-by: GreemDev Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/49 --- .forgejo/renovate.json | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .forgejo/renovate.json diff --git a/.forgejo/renovate.json b/.forgejo/renovate.json new file mode 100644 index 000000000..009ebf659 --- /dev/null +++ b/.forgejo/renovate.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "renovate/config" + ], + "enabledManagers": ["nuget", "github-actions"], + "packageRules": [ + { + // require approval for *all* NuGet package updates, not just major versions. + "matchDepTypes": "nuget", + "dependencyDashboardApproval": true + }, + { + // Ignore Gommon for automatic updates. I make breaking changes on minor updates not infrequently. + "matchDepNames": "Gommon", + "matchDepTypes": "nuget", + "enabled": false + }, + { + "description": "group Silk.NET packages", + "extends": ["renovate/config//groups/silkdotnet.json"], + "groupName": "Silk.NET" + }, + { + "description": "group OpenTK packages", + "extends": ["renovate/config//groups/opentk.json"], + "groupName": "OpenTK" + }, + { + "description": "group Svg.Controls packages", + "extends": ["renovate/config//groups/svgcontrols.json"], + "groupName": "Svg.Controls" + } + ] +} From 3f9d37da83fe9486ac528f74b4f86b5c3a9aef6f Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 2 May 2026 06:06:40 +0000 Subject: [PATCH 04/61] [ci skip] Update actions/checkout action to v6 (#52) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/checkout](https://git.ryujinx.app/actions/checkout) | action | major | `v4` → `v6` | --- ### Release Notes
actions/checkout (actions/checkout) ### [`v6.0.2`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v602) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v6.0.1...v6.0.2) - Fix tag handling: preserve annotations and explicit fetch-tags by [@​ericsciple](https://github.com/ericsciple) in [#​2356](https://github.com/actions/checkout/pull/2356) ### [`v6.0.1`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v601) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v6...v6.0.1) - Add worktree support for persist-credentials includeIf by [@​ericsciple](https://github.com/ericsciple) in [#​2327](https://github.com/actions/checkout/pull/2327) ### [`v6.0.0`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v600) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v6...v6) - Persist creds to a separate file by [@​ericsciple](https://github.com/ericsciple) in [#​2286](https://github.com/actions/checkout/pull/2286) - Update README to include Node.js 24 support details and requirements by [@​salmanmkc](https://github.com/salmanmkc) in [#​2248](https://github.com/actions/checkout/pull/2248) ### [`v6`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v602) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v5.0.1...v6) - Fix tag handling: preserve annotations and explicit fetch-tags by [@​ericsciple](https://github.com/ericsciple) in [#​2356](https://github.com/actions/checkout/pull/2356) ### [`v5.0.1`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v501) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v5...v5.0.1) - Port v6 cleanup to v5 by [@​ericsciple](https://github.com/ericsciple) in [#​2301](https://github.com/actions/checkout/pull/2301) ### [`v5.0.0`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v500) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v5...v5) - Update actions checkout to use node 24 by [@​salmanmkc](https://github.com/salmanmkc) in [#​2226](https://github.com/actions/checkout/pull/2226) ### [`v5`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v501) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.3.1...v5) - Port v6 cleanup to v5 by [@​ericsciple](https://github.com/ericsciple) in [#​2301](https://github.com/actions/checkout/pull/2301) ### [`v4.3.1`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v431) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.3.0...v4.3.1) - Port v6 cleanup to v4 by [@​ericsciple](https://github.com/ericsciple) in [#​2305](https://github.com/actions/checkout/pull/2305) ### [`v4.3.0`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v430) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.2.2...v4.3.0) - docs: update README.md by [@​motss](https://github.com/motss) in [#​1971](https://github.com/actions/checkout/pull/1971) - Add internal repos for checking out multiple repositories by [@​mouismail](https://github.com/mouismail) in [#​1977](https://github.com/actions/checkout/pull/1977) - Documentation update - add recommended permissions to Readme by [@​benwells](https://github.com/benwells) in [#​2043](https://github.com/actions/checkout/pull/2043) - Adjust positioning of user email note and permissions heading by [@​joshmgross](https://github.com/joshmgross) in [#​2044](https://github.com/actions/checkout/pull/2044) - Update README.md by [@​nebuk89](https://github.com/nebuk89) in [#​2194](https://github.com/actions/checkout/pull/2194) - Update CODEOWNERS for actions by [@​TingluoHuang](https://github.com/TingluoHuang) in [#​2224](https://github.com/actions/checkout/pull/2224) - Update package dependencies by [@​salmanmkc](https://github.com/salmanmkc) in [#​2236](https://github.com/actions/checkout/pull/2236) ### [`v4.2.2`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v422) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.2.1...v4.2.2) - `url-helper.ts` now leverages well-known environment variables by [@​jww3](https://github.com/jww3) in [#​1941](https://github.com/actions/checkout/pull/1941) - Expand unit test coverage for `isGhes` by [@​jww3](https://github.com/jww3) in [#​1946](https://github.com/actions/checkout/pull/1946) ### [`v4.2.1`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v421) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.2.0...v4.2.1) - Check out other refs/\* by commit if provided, fall back to ref by [@​orhantoy](https://github.com/orhantoy) in [#​1924](https://github.com/actions/checkout/pull/1924) ### [`v4.2.0`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v420) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.1.7...v4.2.0) - Add Ref and Commit outputs by [@​lucacome](https://github.com/lucacome) in [#​1180](https://github.com/actions/checkout/pull/1180) - Dependency updates by [@​dependabot-](https://github.com/dependabot-) [#​1777](https://github.com/actions/checkout/pull/1777), [#​1872](https://github.com/actions/checkout/pull/1872) ### [`v4.1.7`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v417) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.1.6...v4.1.7) - Bump the minor-npm-dependencies group across 1 directory with 4 updates by [@​dependabot](https://github.com/dependabot) in [#​1739](https://github.com/actions/checkout/pull/1739) - Bump actions/checkout from 3 to 4 by [@​dependabot](https://github.com/dependabot) in [#​1697](https://github.com/actions/checkout/pull/1697) - Check out other refs/\* by commit by [@​orhantoy](https://github.com/orhantoy) in [#​1774](https://github.com/actions/checkout/pull/1774) - Pin actions/checkout's own workflows to a known, good, stable version. by [@​jww3](https://github.com/jww3) in [#​1776](https://github.com/actions/checkout/pull/1776) ### [`v4.1.6`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v416) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.1.5...v4.1.6) - Check platform to set archive extension appropriately by [@​cory-miller](https://github.com/cory-miller) in [#​1732](https://github.com/actions/checkout/pull/1732) ### [`v4.1.5`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v415) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.1.4...v4.1.5) - Update NPM dependencies by [@​cory-miller](https://github.com/cory-miller) in [#​1703](https://github.com/actions/checkout/pull/1703) - Bump github/codeql-action from 2 to 3 by [@​dependabot](https://github.com/dependabot) in [#​1694](https://github.com/actions/checkout/pull/1694) - Bump actions/setup-node from 1 to 4 by [@​dependabot](https://github.com/dependabot) in [#​1696](https://github.com/actions/checkout/pull/1696) - Bump actions/upload-artifact from 2 to 4 by [@​dependabot](https://github.com/dependabot) in [#​1695](https://github.com/actions/checkout/pull/1695) - README: Suggest `user.email` to be `41898282+github-actions[bot]@​users.noreply.github.com` by [@​cory-miller](https://github.com/cory-miller) in [#​1707](https://github.com/actions/checkout/pull/1707) ### [`v4.1.4`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v414) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.1.3...v4.1.4) - Disable `extensions.worktreeConfig` when disabling `sparse-checkout` by [@​jww3](https://github.com/jww3) in [#​1692](https://github.com/actions/checkout/pull/1692) - Add dependabot config by [@​cory-miller](https://github.com/cory-miller) in [#​1688](https://github.com/actions/checkout/pull/1688) - Bump the minor-actions-dependencies group with 2 updates by [@​dependabot](https://github.com/dependabot) in [#​1693](https://github.com/actions/checkout/pull/1693) - Bump word-wrap from 1.2.3 to 1.2.5 by [@​dependabot](https://github.com/dependabot) in [#​1643](https://github.com/actions/checkout/pull/1643) ### [`v4.1.3`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v413) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.1.2...v4.1.3) - Check git version before attempting to disable `sparse-checkout` by [@​jww3](https://github.com/jww3) in [#​1656](https://github.com/actions/checkout/pull/1656) - Add SSH user parameter by [@​cory-miller](https://github.com/cory-miller) in [#​1685](https://github.com/actions/checkout/pull/1685) - Update `actions/checkout` version in `update-main-version.yml` by [@​jww3](https://github.com/jww3) in [#​1650](https://github.com/actions/checkout/pull/1650) ### [`v4.1.2`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v412) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.1.1...v4.1.2) - Fix: Disable sparse checkout whenever `sparse-checkout` option is not present [@​dscho](https://github.com/dscho) in [#​1598](https://github.com/actions/checkout/pull/1598) ### [`v4.1.1`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v411) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4.1.0...v4.1.1) - Correct link to GitHub Docs by [@​peterbe](https://github.com/peterbe) in [#​1511](https://github.com/actions/checkout/pull/1511) - Link to release page from what's new section by [@​cory-miller](https://github.com/cory-miller) in [#​1514](https://github.com/actions/checkout/pull/1514) ### [`v4.1.0`](https://git.ryujinx.app/actions/checkout/blob/HEAD/CHANGELOG.md#v410) [Compare Source](https://git.ryujinx.app/actions/checkout/compare/v4...v4.1.0) - [Add support for partial checkout filters](https://github.com/actions/checkout/pull/1396)
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/52 --- .forgejo/workflows/build.yml | 4 ++-- .forgejo/workflows/canary.yml | 2 +- .forgejo/workflows/pr_triage.yml | 2 +- .forgejo/workflows/release.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index a7db99713..9841323a7 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -35,7 +35,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-dotnet@v4 with: @@ -148,7 +148,7 @@ jobs: configuration: [ Release ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-dotnet@v4 with: diff --git a/.forgejo/workflows/canary.yml b/.forgejo/workflows/canary.yml index 4cb4cdcd5..930e6b253 100644 --- a/.forgejo/workflows/canary.yml +++ b/.forgejo/workflows/canary.yml @@ -217,7 +217,7 @@ jobs: - macos_release - release steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install GLI uses: actions/setup-gli@v1 diff --git a/.forgejo/workflows/pr_triage.yml b/.forgejo/workflows/pr_triage.yml index 1b17c31c8..2608bc8f2 100644 --- a/.forgejo/workflows/pr_triage.yml +++ b/.forgejo/workflows/pr_triage.yml @@ -10,7 +10,7 @@ jobs: steps: # Grab sources to get latest labeler.yml - name: Fetch sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: # Ensure we pin the source origin as pull_request_target run under forks. fetch-depth: 0 diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 45123c2cd..a767835bc 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -212,7 +212,7 @@ jobs: - macos_release - release steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install GLI uses: actions/setup-gli@v1 From 1b1ceeaa11b1a8363dbf33b8334a8a201ce0fdd0 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 2 May 2026 06:07:19 +0000 Subject: [PATCH 05/61] [ci skip] Update actions/github-script action to v9 (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/github-script](https://git.ryujinx.app/actions/github-script) | action | major | `v6` → `v9` | --- ### Release Notes
actions/github-script (actions/github-script) ### [`v9.0.0`](https://git.ryujinx.app/actions/github-script/compare/v9...v9) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v9...v9) ### [`v9`](https://git.ryujinx.app/actions/github-script/compare/v8...v9) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v8...v9) ### [`v8.0.0`](https://git.ryujinx.app/actions/github-script/compare/v8...v8) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v8...v8) ### [`v8`](https://git.ryujinx.app/actions/github-script/compare/v7.1.0...v8) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v7.1.0...v8) ### [`v7.1.0`](https://git.ryujinx.app/actions/github-script/compare/v7.0.1...v7.1.0) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v7.0.1...v7.1.0) ### [`v7.0.1`](https://git.ryujinx.app/actions/github-script/compare/v7...v7.0.1) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v7...v7.0.1) ### [`v7.0.0`](https://git.ryujinx.app/actions/github-script/compare/v7...v7) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v7...v7) ### [`v7`](https://git.ryujinx.app/actions/github-script/compare/v6.4.1...v7) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v6.4.1...v7) ### [`v6.4.1`](https://git.ryujinx.app/actions/github-script/compare/v6.4.0...v6.4.1) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v6.4.0...v6.4.1) ### [`v6.4.0`](https://git.ryujinx.app/actions/github-script/compare/v6.3.3...v6.4.0) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v6.3.3...v6.4.0) ### [`v6.3.3`](https://git.ryujinx.app/actions/github-script/compare/v6.3.2...v6.3.3) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v6.3.2...v6.3.3) ### [`v6.3.2`](https://git.ryujinx.app/actions/github-script/compare/v6.3.1...v6.3.2) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v6.3.1...v6.3.2) ### [`v6.3.1`](https://git.ryujinx.app/actions/github-script/compare/v6.3.0...v6.3.1) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v6.3.0...v6.3.1) ### [`v6.3.0`](https://git.ryujinx.app/actions/github-script/compare/v6.2.0...v6.3.0) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v6.2.0...v6.3.0) ### [`v6.2.0`](https://git.ryujinx.app/actions/github-script/compare/v6.1.1...v6.2.0) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v6.1.1...v6.2.0) ### [`v6.1.1`](https://git.ryujinx.app/actions/github-script/compare/v6.1.0...v6.1.1) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v6.1.0...v6.1.1) ### [`v6.1.0`](https://git.ryujinx.app/actions/github-script/compare/v6...v6.1.0) [Compare Source](https://git.ryujinx.app/actions/github-script/compare/v6...v6.1.0)
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/53 --- .forgejo/workflows/unused/nightly_pr_comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/unused/nightly_pr_comment.yml b/.forgejo/workflows/unused/nightly_pr_comment.yml index 24d23d98b..a2e6afbb9 100644 --- a/.forgejo/workflows/unused/nightly_pr_comment.yml +++ b/.forgejo/workflows/unused/nightly_pr_comment.yml @@ -10,7 +10,7 @@ jobs: if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v9 with: script: | const {owner, repo} = context.repo; From f749cf90b6a5d9b57610c02b3c97291122fa1bf4 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 2 May 2026 06:36:28 +0000 Subject: [PATCH 06/61] [ci skip] Update actions/setup-dotnet action to v5 (#54) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/setup-dotnet](https://git.ryujinx.app/actions/setup-dotnet) | action | major | `v4` → `v5` | --- ### Release Notes
actions/setup-dotnet (actions/setup-dotnet) ### [`v5.2.0`](https://git.ryujinx.app/actions/setup-dotnet/compare/v5.1.0...v5.2.0) [Compare Source](https://git.ryujinx.app/actions/setup-dotnet/compare/v5.1.0...v5.2.0) ### [`v5.1.0`](https://git.ryujinx.app/actions/setup-dotnet/compare/v5.0.1...v5.1.0) [Compare Source](https://git.ryujinx.app/actions/setup-dotnet/compare/v5.0.1...v5.1.0) ### [`v5.0.1`](https://git.ryujinx.app/actions/setup-dotnet/compare/v5...v5.0.1) [Compare Source](https://git.ryujinx.app/actions/setup-dotnet/compare/v5...v5.0.1) ### [`v5.0.0`](https://git.ryujinx.app/actions/setup-dotnet/compare/v5...v5) [Compare Source](https://git.ryujinx.app/actions/setup-dotnet/compare/v5...v5) ### [`v5`](https://git.ryujinx.app/actions/setup-dotnet/compare/v4.3.1...v5) [Compare Source](https://git.ryujinx.app/actions/setup-dotnet/compare/v4.3.1...v5) ### [`v4.3.1`](https://git.ryujinx.app/actions/setup-dotnet/compare/v4.3.0...v4.3.1) [Compare Source](https://git.ryujinx.app/actions/setup-dotnet/compare/v4.3.0...v4.3.1) ### [`v4.3.0`](https://git.ryujinx.app/actions/setup-dotnet/compare/v4.2.0...v4.3.0) [Compare Source](https://git.ryujinx.app/actions/setup-dotnet/compare/v4.2.0...v4.3.0) ### [`v4.2.0`](https://git.ryujinx.app/actions/setup-dotnet/compare/v4.1.0...v4.2.0) [Compare Source](https://git.ryujinx.app/actions/setup-dotnet/compare/v4.1.0...v4.2.0) ### [`v4.1.0`](https://git.ryujinx.app/actions/setup-dotnet/compare/v4.0.1...v4.1.0) [Compare Source](https://git.ryujinx.app/actions/setup-dotnet/compare/v4.0.1...v4.1.0) ### [`v4.0.1`](https://git.ryujinx.app/actions/setup-dotnet/compare/v4...v4.0.1) [Compare Source](https://git.ryujinx.app/actions/setup-dotnet/compare/v4...v4.0.1)
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/54 --- .forgejo/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 9841323a7..4ea1aa948 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -37,7 +37,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: global-json-file: global.json @@ -150,7 +150,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@v5 with: global-json-file: global.json From bd11cbde0892ae6ecac8f87acf81e52d42b917d5 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 2 May 2026 06:37:21 +0000 Subject: [PATCH 07/61] [ci skip] Update actions/upload-artifact action to v5 (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/upload-artifact](https://git.ryujinx.app/actions/upload-artifact) | action | major | `v4` → `v5` | --- ### Release Notes
actions/upload-artifact (actions/upload-artifact) ### [`v5`](https://git.ryujinx.app/actions/upload-artifact/compare/v4.3.1...v5) [Compare Source](https://git.ryujinx.app/actions/upload-artifact/compare/v4.3.1...v5) ### [`v4.3.1`](https://git.ryujinx.app/actions/upload-artifact/compare/v4.3.0...v4.3.1) [Compare Source](https://git.ryujinx.app/actions/upload-artifact/compare/v4.3.0...v4.3.1) ### [`v4.3.0`](https://git.ryujinx.app/actions/upload-artifact/compare/v4.2.0...v4.3.0) [Compare Source](https://git.ryujinx.app/actions/upload-artifact/compare/v4.2.0...v4.3.0) ### [`v4.2.0`](https://git.ryujinx.app/actions/upload-artifact/compare/v4.1.0...v4.2.0) [Compare Source](https://git.ryujinx.app/actions/upload-artifact/compare/v4.1.0...v4.2.0) ### [`v4.1.0`](https://git.ryujinx.app/actions/upload-artifact/compare/v4...v4.1.0) [Compare Source](https://git.ryujinx.app/actions/upload-artifact/compare/v4...v4.1.0)
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/55 --- .forgejo/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 4ea1aa948..e7052a6f6 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -94,7 +94,7 @@ jobs: shell: bash - name: Upload Ryujinx Windows artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }} path: artifact @@ -133,7 +133,7 @@ jobs: shell: bash - name: Upload Ryujinx AppImage artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux') with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}-AppImage @@ -197,7 +197,7 @@ jobs: shell: bash - name: Upload Ryujinx artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-macos_universal path: "publish/*.tar.gz" From 44d77f8e59d626b27d3a31b103297d7fdca13f8d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 2 May 2026 15:45:20 +0000 Subject: [PATCH 08/61] Update dependency Microsoft.Build.Utilities.Core to 17.12.50 [SECURITY] (#50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [Microsoft.Build.Utilities.Core](http://go.microsoft.com/fwlink/?LinkId=624683) ([source](https://github.com/dotnet/msbuild)) | `17.12.6` → `17.12.50` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.Build.Utilities.Core/17.12.50?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.Build.Utilities.Core/17.12.6/17.12.50?slim=true) | --- ### Microsoft Security Advisory CVE-2025-55247 | .NET Denial of Service Vulnerability BIT-dotnet-2025-55247 / BIT-dotnet-sdk-2025-55247 / [CVE-2025-55247](https://nvd.nist.gov/vuln/detail/CVE-2025-55247) / [GHSA-w3q9-fxm7-j8fq](https://github.com/advisories/GHSA-w3q9-fxm7-j8fq)
More information #### Details ##### Microsoft Security Advisory CVE-2025-55247 | .NET Denial of Service Vulnerability ##### Executive summary Microsoft is releasing this security advisory to provide information about a vulnerability in .NET 8.0.xxx, .NET 9.0.xxx and .NET 10.0.xxx. This advisory also provides guidance on what developers can do to update their environments to remove this vulnerability. A vulnerability exists in .NET where predictable paths for MSBuild's temporary directories on Linux let another user create the directories ahead of MSBuild, leading to DoS of builds. This only affects .NET on Linux operating systems. ##### Announcement Announcement for this issue can be found at https://github.com/dotnet/announcements/issues/370 ##### Mitigation factors Projects which do not utilize the [DownloadFile](https://learn.microsoft.com/visualstudio/msbuild/downloadfile-task) build task are not susceptible to this vulnerability. ##### Affected software * Any installation of .NET 10.0.100-rc.1.25451.107 SDK or earlier. * Any installation of .NET 9.0.110 SDK, .NET 9.0.305 SDK or earlier. * Any installation of .NET 8.0.120 SDK, .NET 8.0.317 SDK, .NET 8.0.414 SDK or earlier. ##### Affected Packages The vulnerability affects any Microsoft .NET Core project if it uses any of affected packages versions listed below Package name |Affected version | Patched version ------------ |---------------- | ------------------------- [Microsoft.Build.Tasks.Core](https://www.nuget.org/packages/Microsoft.Build.Tasks.Core) | 17.15.0-preview-25277-114
>=17.14.0, <= 17.14.8
>= 17.12.0, <= 17.12.36
>= 17.11.0, <= 17.11.31
>= 17.10.0, <= 17.10.29
>= 17.8.0, <= 17.8.29
| 18.0.0-preview-25476-107
17.14.28
17.12.50
17.11.48
17.10.46
17.8.43
Package name|Affected version | Patched version ------------ |---------------|---------------- [Microsoft.Build](https://www.nuget.org/packages/Microsoft.Build) | 17.15.0-preview-25277-114
>=17.14.0, <= 17.14.8
>= 17.12.0, <= 17.12.36
>= 17.11.0, <= 17.11.31
>= 17.10.0, <= 17.10.29
>= 17.8.0, <= 17.8.29
| 18.0.0-preview-25476-107
17.14.28
17.12.50
17.11.48
17.10.46
17.8.43
Package name |Affected version | Patched version ------------ |---------------|---------------- [Microsoft.Build.Utilities.core](https://www.nuget.org/packages/Microsoft.Build.Utilities.Core/17.15.0-preview-25277-114)| 17.15.0-preview-25277-114
>=17.14.0, <= 17.14.8
>= 17.12.0, <= 17.12.36
>= 17.11.0, <= 17.11.31
>= 17.10.0, <= 17.10.29
>= 17.8.0, <= 17.8.29
| 18.0.0-preview-25476-107
17.14.28
17.12.50
17.11.48
17.10.46
17.8.43
##### Advisory FAQ ##### How do I know if I am affected? If you have a .NET SDK with a version listed, or an affected package listed in [affected software](#affected-packages) or [affected packages](#affected-software), you're exposed to the vulnerability. ##### How do I fix the issue? 1. To fix the issue please install the latest version of .NET 10.0 SDK, .NET 9.0 SDK or .NET 8.0 SDK. If you have installed one or more .NET SDKs through Visual Studio, Visual Studio will prompt you to update Visual Studio, which will also update your .NET SDKs. 2. If your application references the vulnerable package, update the package reference to the patched version. * You can list the versions you have installed by running the `dotnet --info` command. You will see output like the following; ``` .NET SDK: Version: 9.0.100 Commit: 59db016f11 Workload version: 9.0.100-manifests.3068a692 MSBuild version: 17.12.7+5b8665660 Runtime Environment: OS Name: Mac OS X OS Version: 15.2 OS Platform: Darwin RID: osx-arm64 Base Path: /usr/local/share/dotnet/sdk/9.0.100/ .NET workloads installed: There are no installed workloads to display. Configured to use loose manifests when installing new manifests. Host: Version: 9.0.0 Architecture: arm64 Commit: 9d5a6a9aa4 .NET SDKs installed: 9.0.100 [/usr/local/share/dotnet/sdk] .NET runtimes installed: Microsoft.AspNetCore.App 9.0.0 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 9.0.0 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Other architectures found: x64 [/usr/local/share/dotnet] registered at [/etc/dotnet/install_location_x64] Environment variables: Not set global.json file: Not found Learn more: https://aka.ms/dotnet/info Download .NET: https://aka.ms/dotnet/download ``` * If you're using .NET 10.0, you should download and install the appropriate SDK: `.NET 10.0.100-rc.2.25502.107` for Visual Studio 2026 v18 Preview 3. Download from https://dotnet.microsoft.com/download/dotnet-core/10.0. * If you're using .NET 9.0, you should download and install the appropriate SDK: `.NET 9.0.306` for Visual Studio 2022 v17.14 or `.NET 9.0.111` for v17.12. Download from https://dotnet.microsoft.com/download/dotnet-core/9.0. * If you're using .NET 8.0, you should download and install the appropriate SDK: `.NET 8.0.415` for Visual Studio 2022 v17.11, `.NET 8.0.318` for v17.10, or `.NET 8.0.121` for v17.8. Download from https://dotnet.microsoft.com/download/dotnet-core/8.0. Once you have installed the updated SDK, restart your apps for the update to take effect. ##### Other Information ##### Reporting Security Issues If you have found a potential security issue in .NET 8.0, .NET 9.0 or .NET 10.0, please email details to secure@microsoft.com. Reports may qualify for the Microsoft .NET Core & .NET 5 Bounty. Details of the Microsoft .NET Bounty Program including terms and conditions are at . ##### Support You can ask questions about this issue on GitHub in the .NET GitHub organization. The main repos are located at https://github.com/dotnet/runtime. The Announcements repo (https://github.com/dotnet/Announcements) will contain this bulletin as an issue and will include a link to a discussion issue. You can ask questions in the linked discussion issue. ##### Disclaimer The information provided in this advisory is provided "as is" without warranty of any kind. Microsoft disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. In no event shall Microsoft Corporation or its suppliers be liable for any damages whatsoever including direct, indirect, incidental, consequential, loss of business profits or special damages, even if Microsoft Corporation or its suppliers have been advised of the possibility of such damages. Some states do not allow the exclusion or limitation of liability for consequential or incidental damages so the foregoing limitation may not apply. ##### External Links [CVE-2025-55247]( https://www.cve.org/CVERecord?id=CVE-2025-55247) ##### Revisions V1.0 (October 14, 2025): Advisory published. #### Severity - CVSS Score: 7.3 / 10 (High) - Vector String: `CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H` #### References - [https://github.com/dotnet/msbuild/security/advisories/GHSA-w3q9-fxm7-j8fq](https://github.com/dotnet/msbuild/security/advisories/GHSA-w3q9-fxm7-j8fq) - [https://nvd.nist.gov/vuln/detail/CVE-2025-55247](https://nvd.nist.gov/vuln/detail/CVE-2025-55247) - [https://github.com/dotnet/msbuild](https://github.com/dotnet/msbuild) - [https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-55247](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-55247) This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-w3q9-fxm7-j8fq) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)).
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/50 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 014235f95..c40220ed2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,7 +12,7 @@ - + From c9c4ed67b9d70f6444a0f6ba5ea5e0031a6723ae Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 2 May 2026 16:04:58 +0000 Subject: [PATCH 09/61] updated a whole bunch of dependencies :) (#48) hi greem ily :cheart: Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/48 --- Directory.Packages.props | 44 +++++++++++++++++++++----------------- src/Ryujinx/Ryujinx.csproj | 2 +- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c40220ed2..1dee59de8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,8 +9,8 @@ - - + + @@ -19,35 +19,39 @@ - + - - + + - - + + - - - - - - + + + + + + + + - - + + + + - + - + - + @@ -55,7 +59,7 @@ - - + + diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 926af7f50..f80c1aa97 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -58,7 +58,7 @@ - + From ddfb56c424a0fc01b4082454f3a400027f586cc4 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 2 May 2026 19:05:11 +0000 Subject: [PATCH 10/61] Update dependency Newtonsoft.Json to 13.0.4 (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [Newtonsoft.Json](https://www.newtonsoft.com/json) ([source](https://github.com/JamesNK/Newtonsoft.Json)) | `13.0.3` → `13.0.4` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Newtonsoft.Json/13.0.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Newtonsoft.Json/13.0.3/13.0.4?slim=true) | --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/74 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1dee59de8..f8185217e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,7 +13,7 @@ - + From 2b929c55379b28a0ec56e4d405277dfcf197e322 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 2 May 2026 19:05:24 +0000 Subject: [PATCH 11/61] Update dependency Microsoft.IdentityModel.JsonWebTokens to 8.18.0 (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [Microsoft.IdentityModel.JsonWebTokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) | `8.17.0` → `8.18.0` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.IdentityModel.JsonWebTokens/8.18.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.IdentityModel.JsonWebTokens/8.17.0/8.18.0?slim=true) | --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/75 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f8185217e..4c02a3b8d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,7 +27,7 @@ - + From c1c47d308d5267c3d085d356c287cba8a86be760 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 3 May 2026 16:27:51 +0000 Subject: [PATCH 12/61] revert 96f8d519e6 (#76) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/76 --- src/Ryujinx.Common/Helpers/ConsoleHelper.cs | 13 +++++++++ src/Ryujinx/Program.cs | 30 +++++---------------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/Ryujinx.Common/Helpers/ConsoleHelper.cs b/src/Ryujinx.Common/Helpers/ConsoleHelper.cs index 776ff13d7..2b3c11b1e 100644 --- a/src/Ryujinx.Common/Helpers/ConsoleHelper.cs +++ b/src/Ryujinx.Common/Helpers/ConsoleHelper.cs @@ -16,6 +16,15 @@ namespace Ryujinx.Common.Helper [return: MarshalAs(UnmanagedType.Bool)] private static partial bool ShowWindow(nint hWnd, int nCmdShow); + [SupportedOSPlatform("windows")] + [LibraryImport("user32")] + private static partial nint GetForegroundWindow(); + + [SupportedOSPlatform("windows")] + [LibraryImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SetForegroundWindow(nint hWnd); + public static bool SetConsoleWindowStateSupported => OperatingSystem.IsWindows(); public static void SetConsoleWindowState(bool show) @@ -44,6 +53,10 @@ namespace Ryujinx.Common.Helper return; } + SetForegroundWindow(hWnd); + + hWnd = GetForegroundWindow(); + ShowWindow(hWnd, show ? SW_SHOW : SW_HIDE); } } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index ce2a0eb11..cb219b216 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -24,11 +24,9 @@ using Ryujinx.Headless; using Ryujinx.SDL3.Common; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Security.Principal; -using System.Text; using System.Threading.Tasks; namespace Ryujinx.Ava @@ -54,22 +52,6 @@ namespace Ryujinx.Ava if (OperatingSystem.IsWindows()) { -#if !DEBUG - // this fixes the "hide console" option by forcing the emulator to launch in an old-school cmd - if (!Console.Title.Contains("conhost.exe")) - { - StringBuilder sb = new(); - - foreach (string arg in args) - { - sb.Append(arg.Contains(' ') ? $" \"{arg}\"" : $" {arg}"); - } - - Process.Start("conhost.exe", $"{Environment.ProcessPath} {sb}"); - return 0; - } -#endif - 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); @@ -103,7 +85,7 @@ namespace Ryujinx.Ava 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, + // 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) @@ -260,7 +242,7 @@ namespace Ryujinx.Ava ConfigurationPath = appDataConfigurationPath; } } - + if (ConfigurationPath == null) { // No configuration, we load the default values and save it to disk @@ -331,28 +313,28 @@ namespace Ryujinx.Ava _ => ConfigurationState.Instance.HideCursor, }; - // Check if memoryManagerMode was overridden. + // 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. + // 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. + // 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. + //Check if language was overridden. if (CommandLineState.OverrideSystemLanguage is not null) if (Enum.TryParse(CommandLineState.OverrideSystemLanguage, true, out Language result)) { From 3a3e5e5c5a27fe6e66ffb45ea19d5a315e3d500b Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 3 May 2026 19:09:47 +0000 Subject: [PATCH 13/61] added skia native assets for windows, macOS and switched to no-depend for linux (#77) im not really sure why these were missing, but theyre here now :woopernod: Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/77 --- Directory.Packages.props | 4 +++- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 4 +++- src/Ryujinx/Ryujinx.csproj | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4c02a3b8d..7c94ffe24 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -57,7 +57,9 @@ - + + + diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index 7e4c8a9e1..f7641c68d 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -27,7 +27,9 @@ - + + + diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index f80c1aa97..d517746c3 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -49,6 +49,9 @@ + + + From 1e06c86d4747f73203473fbc1ec5dc2b176931b5 Mon Sep 17 00:00:00 2001 From: ryuadmin Date: Mon, 4 May 2026 05:38:44 +0000 Subject: [PATCH 14/61] [ci skip] revert 3a3e5e5c5a27fe6e66ffb45ea19d5a315e3d500b broke macOS CI for some reason --- Directory.Packages.props | 4 +--- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 4 +--- src/Ryujinx/Ryujinx.csproj | 3 --- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7c94ffe24..4c02a3b8d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -57,9 +57,7 @@ - - - + diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index f7641c68d..7e4c8a9e1 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -27,9 +27,7 @@ - - - + diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index d517746c3..f80c1aa97 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -49,9 +49,6 @@ - - - From a3e10a1e5ab90eede43fbf598d2f87e1ad04c8a8 Mon Sep 17 00:00:00 2001 From: ryuadmin Date: Mon, 4 May 2026 06:18:29 +0000 Subject: [PATCH 15/61] [ci skip] Use gradient CSS classes in README. Recent change in our fork: https://github.com/Ryubing/forgejo/commit/50e0549dac66901181729ebdacc96bbff2cb8c41 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce18233c0..0364dc7fa 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ -# Ryujinx +

Ryujinx

[![Latest release](https://git.ryujinx.app/projects/Ryubing/badges/release.svg?label=stable&color=32cd32)](https://update.ryujinx.app/latest/stable) [![Latest canary release](https://git.ryujinx.app/Ryubing/Canary/badges/release.svg?label=canary&color=FF4500)](https://update.ryujinx.app/latest/canary) @@ -21,7 +21,7 @@ Ryujinx is an open-source Nintendo Switch emulator, originally created by gdkchan, written in C#. This emulator aims at providing excellent accuracy and performance, a user-friendly interface and consistent builds. It was written from scratch and development on the project began in September 2017. - Ryujinx is available on a self-managed modified Forgejo instance under the MIT license. + Ryujinx is available on a self-managed modified Forgejo instance under the MIT license.

From 87ce5162be796ae862e348bee2ca61dc8818d587 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 4 May 2026 12:42:52 +0000 Subject: [PATCH 16/61] skia-natives (again) (#78) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/78 --- Directory.Packages.props | 4 +++- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 4 +++- src/Ryujinx/Ryujinx.csproj | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4c02a3b8d..7c94ffe24 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -57,7 +57,9 @@ - + + + diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index 7e4c8a9e1..f7641c68d 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -27,7 +27,9 @@ - + + + diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index f80c1aa97..3bfddbff6 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -49,6 +49,9 @@ + + + From 88421959a62a37148076861539cec4c7f27af4ba Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 4 May 2026 19:58:48 -0500 Subject: [PATCH 17/61] Rework nightly_pr_comment for Forgejo Actions --- .forgejo/workflows/nightly_pr_comment.yml | 58 ++++++++++++++++++ .../workflows/unused/nightly_pr_comment.yml | 61 ------------------- 2 files changed, 58 insertions(+), 61 deletions(-) create mode 100644 .forgejo/workflows/nightly_pr_comment.yml delete mode 100644 .forgejo/workflows/unused/nightly_pr_comment.yml diff --git a/.forgejo/workflows/nightly_pr_comment.yml b/.forgejo/workflows/nightly_pr_comment.yml new file mode 100644 index 000000000..25f6abacc --- /dev/null +++ b/.forgejo/workflows/nightly_pr_comment.yml @@ -0,0 +1,58 @@ +name: Comment PR artifacts links + +on: + workflow_run: + workflows: ['Build PR'] + types: [completed] + +jobs: + pr_comment: + if: forgejo.event.workflow_run.event == 'pull_request' && forgejo.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v6 + with: + script: | + const forgejo = getOctokit(process.env.FORGEJO_TOKEN, { + baseUrl: 'https://git.ryujinx.app/api/v1' + }) + + const {owner, repo} = context.repo; + const run_id = ${{forgejo.event.workflow_run.id}}; + const pull_head_sha = '${{forgejo.event.workflow_run.head_sha}}'; + + const issue_number = await (async () => { + const pulls = await forgejo.rest.pulls.list({owner, repo}); + for await (const {data} of forgejo.paginate.iterator(pulls)) { + for (const pull of data) { + if (pull.head.sha === pull_head_sha) { + return pull.number; + } + } + } + })(); + if (issue_number) { + core.info(`Using pull request ${issue_number}`); + } else { + return core.error(`No matching pull request found`); + } + + const {data: {artifacts}} = await forgejo.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id}); + if (!artifacts.length) { + return core.error(`No artifacts found`); + } + let body = `Download the artifacts for this pull request:\n`; + for (const art of artifacts) { + const url = `https://git.ryujinx.app/api/v1/repos/${owner}/${repo}/actions/artifacts/${art.id}/zip`; + body += `\n* [${art.name}](${url})`; + } + + const {data: comments} = await forgejo.rest.issues.listComments({repo, owner, issue_number}); + const existing_comment = comments.find((c) => c.user.login === 'forgejo-actions'); + if (existing_comment) { + core.info(`Updating comment ${existing_comment.id}`); + await forgejo.rest.issues.updateComment({repo, owner, comment_id: existing_comment.id, body}); + } else { + core.info(`Creating a comment`); + await forgejo.rest.issues.createComment({repo, owner, issue_number, body}); + } diff --git a/.forgejo/workflows/unused/nightly_pr_comment.yml b/.forgejo/workflows/unused/nightly_pr_comment.yml deleted file mode 100644 index a2e6afbb9..000000000 --- a/.forgejo/workflows/unused/nightly_pr_comment.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Comment PR artifacts links - -on: - workflow_run: - workflows: ['Build PR'] - types: [completed] - -jobs: - pr_comment: - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v9 - with: - script: | - const {owner, repo} = context.repo; - const run_id = ${{github.event.workflow_run.id}}; - const pull_head_sha = '${{github.event.workflow_run.head_sha}}'; - - const issue_number = await (async () => { - const pulls = await github.rest.pulls.list({owner, repo}); - for await (const {data} of github.paginate.iterator(pulls)) { - for (const pull of data) { - if (pull.head.sha === pull_head_sha) { - return pull.number; - } - } - } - })(); - if (issue_number) { - core.info(`Using pull request ${issue_number}`); - } else { - return core.error(`No matching pull request found`); - } - - const {data: {artifacts}} = await github.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id}); - if (!artifacts.length) { - return core.error(`No artifacts found`); - } - let body = `Download the artifacts for this pull request:\n`; - let hidden_debug_artifacts = `\n\n

Only for Developers\n`; - for (const art of artifacts) { - const url = `https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip`; - if (art.name.includes('Debug')) { - hidden_debug_artifacts += `\n* [${art.name}](${url})`; - } else { - body += `\n* [${art.name}](${url})`; - } - } - hidden_debug_artifacts += `\n
`; - body += hidden_debug_artifacts; - - const {data: comments} = await github.rest.issues.listComments({repo, owner, issue_number}); - const existing_comment = comments.find((c) => c.user.login === 'github-actions[bot]'); - if (existing_comment) { - core.info(`Updating comment ${existing_comment.id}`); - await github.rest.issues.updateComment({repo, owner, comment_id: existing_comment.id, body}); - } else { - core.info(`Creating a comment`); - await github.rest.issues.createComment({repo, owner, issue_number, body}); - } From 518dd6548458e0abfdfeacd9f82e31f105c33cc9 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 4 May 2026 20:45:45 -0500 Subject: [PATCH 18/61] fix: Collapse PR comment into PR build workflow Not sure why this was ever separate, and Forgejo doesn't seem to run 'workflow_run` post-execution workflows. --- .forgejo/workflows/build.yml | 54 +++++++++++++++++++++ .forgejo/workflows/nightly_pr_comment.yml | 58 ----------------------- 2 files changed, 54 insertions(+), 58 deletions(-) delete mode 100644 .forgejo/workflows/nightly_pr_comment.yml diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index e7052a6f6..3e2ae332f 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -202,3 +202,57 @@ jobs: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-macos_universal path: "publish/*.tar.gz" if: forgejo.event_name == 'pull_request' + + post_comment: + name: Post comment linking uploaded artifacts + runs-on: ubuntu-latest + needs: + - build + - build_macos + steps: + - uses: actions/github-script@v6 + with: + script: | + const forgejo = getOctokit(process.env.FORGEJO_TOKEN, { + baseUrl: 'https://git.ryujinx.app/api/v1' + }) + + const {owner, repo} = context.repo; + const run_id = ${{forgejo.event.workflow_run.id}}; + const pull_head_sha = '${{forgejo.event.workflow_run.head_sha}}'; + + const issue_number = await (async () => { + const pulls = await forgejo.rest.pulls.list({owner, repo}); + for await (const {data} of forgejo.paginate.iterator(pulls)) { + for (const pull of data) { + if (pull.head.sha === pull_head_sha) { + return pull.number; + } + } + } + })(); + if (issue_number) { + core.info(`Using pull request ${issue_number}`); + } else { + return core.error(`No matching pull request found`); + } + + const {data: {artifacts}} = await forgejo.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id}); + if (!artifacts.length) { + return core.error(`No artifacts found`); + } + let body = `Download the artifacts for this pull request:\n`; + for (const art of artifacts) { + const url = `https://git.ryujinx.app/api/v1/repos/${owner}/${repo}/actions/artifacts/${art.id}/zip`; + body += `\n* [${art.name}](${url})`; + } + + const {data: comments} = await forgejo.rest.issues.listComments({repo, owner, issue_number}); + const existing_comment = comments.find((c) => c.user.login === 'forgejo-actions'); + if (existing_comment) { + core.info(`Updating comment ${existing_comment.id}`); + await forgejo.rest.issues.updateComment({repo, owner, comment_id: existing_comment.id, body}); + } else { + core.info(`Creating a comment`); + await forgejo.rest.issues.createComment({repo, owner, issue_number, body}); + } diff --git a/.forgejo/workflows/nightly_pr_comment.yml b/.forgejo/workflows/nightly_pr_comment.yml deleted file mode 100644 index 25f6abacc..000000000 --- a/.forgejo/workflows/nightly_pr_comment.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Comment PR artifacts links - -on: - workflow_run: - workflows: ['Build PR'] - types: [completed] - -jobs: - pr_comment: - if: forgejo.event.workflow_run.event == 'pull_request' && forgejo.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v6 - with: - script: | - const forgejo = getOctokit(process.env.FORGEJO_TOKEN, { - baseUrl: 'https://git.ryujinx.app/api/v1' - }) - - const {owner, repo} = context.repo; - const run_id = ${{forgejo.event.workflow_run.id}}; - const pull_head_sha = '${{forgejo.event.workflow_run.head_sha}}'; - - const issue_number = await (async () => { - const pulls = await forgejo.rest.pulls.list({owner, repo}); - for await (const {data} of forgejo.paginate.iterator(pulls)) { - for (const pull of data) { - if (pull.head.sha === pull_head_sha) { - return pull.number; - } - } - } - })(); - if (issue_number) { - core.info(`Using pull request ${issue_number}`); - } else { - return core.error(`No matching pull request found`); - } - - const {data: {artifacts}} = await forgejo.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id}); - if (!artifacts.length) { - return core.error(`No artifacts found`); - } - let body = `Download the artifacts for this pull request:\n`; - for (const art of artifacts) { - const url = `https://git.ryujinx.app/api/v1/repos/${owner}/${repo}/actions/artifacts/${art.id}/zip`; - body += `\n* [${art.name}](${url})`; - } - - const {data: comments} = await forgejo.rest.issues.listComments({repo, owner, issue_number}); - const existing_comment = comments.find((c) => c.user.login === 'forgejo-actions'); - if (existing_comment) { - core.info(`Updating comment ${existing_comment.id}`); - await forgejo.rest.issues.updateComment({repo, owner, comment_id: existing_comment.id, body}); - } else { - core.info(`Creating a comment`); - await forgejo.rest.issues.createComment({repo, owner, issue_number, body}); - } From e656de5fffb51c915f827c1d6da266db4a820f2e Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 4 May 2026 20:46:40 -0500 Subject: [PATCH 19/61] fix: use ubuntu-latest in release.yml post-ci steps. --- .forgejo/workflows/release.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index a767835bc..341430adb 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -143,9 +143,7 @@ jobs: macos_release: name: Release MacOS universal - runs-on: docker - container: - image: ghcr.io/catthehacker/ubuntu:act-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -207,7 +205,7 @@ jobs: post_ci: name: Post-CI Steps - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest needs: - macos_release - release From 0d66cfa281dae8722b568b76c7c96da665ee2d73 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 4 May 2026 21:18:33 -0500 Subject: [PATCH 20/61] chore: Update actions/github-script to v9 (not sure how this got lost) also add explicit semicolon for getOctokit --- .forgejo/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 3e2ae332f..6f18bb975 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -210,12 +210,12 @@ jobs: - build - build_macos steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v9 with: script: | const forgejo = getOctokit(process.env.FORGEJO_TOKEN, { baseUrl: 'https://git.ryujinx.app/api/v1' - }) + }); const {owner, repo} = context.repo; const run_id = ${{forgejo.event.workflow_run.id}}; From 4e86159bcee7164aed5290ef2c153808a6307033 Mon Sep 17 00:00:00 2001 From: greem Date: Tue, 5 May 2026 03:15:33 +0000 Subject: [PATCH 21/61] [ci skip] fix: Invalid workflow templates in github-script source --- .forgejo/workflows/build.yml | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 6f18bb975..a93e02cf4 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -218,24 +218,11 @@ jobs: }); const {owner, repo} = context.repo; - const run_id = ${{forgejo.event.workflow_run.id}}; - const pull_head_sha = '${{forgejo.event.workflow_run.head_sha}}'; + const run_id = ${{ forgejo.run_id }}; + const pull_head_sha = '${{ forgejo.event.workflow_run.head_sha }}'; - const issue_number = await (async () => { - const pulls = await forgejo.rest.pulls.list({owner, repo}); - for await (const {data} of forgejo.paginate.iterator(pulls)) { - for (const pull of data) { - if (pull.head.sha === pull_head_sha) { - return pull.number; - } - } - } - })(); - if (issue_number) { - core.info(`Using pull request ${issue_number}`); - } else { - return core.error(`No matching pull request found`); - } + const issue_number = ${{ forgejo.event.pull_request.number }}; + core.info(`Using pull request ${issue_number}`); const {data: {artifacts}} = await forgejo.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id}); if (!artifacts.length) { From 4d0cd61b6aa9de5e906f0f07ab2418a5f83551d8 Mon Sep 17 00:00:00 2001 From: Babib3l Date: Tue, 5 May 2026 03:48:23 +0000 Subject: [PATCH 22/61] Fix Windows console hide path targeting the foreground window (#32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR addresses [Ryubing/Issues#345](https://github.com/Ryubing/Issues/issues/345) by fixing the Windows console hide/show path so it only acts on Ryujinx’s own console window instead of whatever window happens to be focused during startup. Previously, when Show Console was disabled, the helper could race with focus changes and end up affecting another app or shell window while leaving the console visible; this change removes that foreground-window dependency and keeps the startup behavior scoped to the Ryujinx console. Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/32 --- assets/Locales/Root.json | 25 +++++++++++ src/Ryujinx.Common/Helpers/ConsoleHelper.cs | 43 ++++++++++--------- src/Ryujinx.Common/Logging/Logger.cs | 21 ++++++--- .../UI/ViewModels/MainWindowViewModel.cs | 9 ++++ 4 files changed, 72 insertions(+), 26 deletions(-) diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index a11aad25a..0b1115cdc 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -21425,6 +21425,31 @@ "zh_TW": "需要重新啟動 Ryujinx" } }, + { + "ID": "SettingsShowConsoleRestartMessage", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "The console will be available the next time Ryujinx starts.", + "es_ES": "La consola estará disponible la próxima vez que se inicie Ryujinx.", + "fr_FR": "La console sera disponible au prochain démarrage de Ryujinx.", + "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": "SettingsGpuBackendRestartMessage", "Translations": { diff --git a/src/Ryujinx.Common/Helpers/ConsoleHelper.cs b/src/Ryujinx.Common/Helpers/ConsoleHelper.cs index 2b3c11b1e..75954110c 100644 --- a/src/Ryujinx.Common/Helpers/ConsoleHelper.cs +++ b/src/Ryujinx.Common/Helpers/ConsoleHelper.cs @@ -12,20 +12,12 @@ namespace Ryujinx.Common.Helper private static partial nint GetConsoleWindow(); [SupportedOSPlatform("windows")] - [LibraryImport("user32")] + [LibraryImport("kernel32", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool ShowWindow(nint hWnd, int nCmdShow); - - [SupportedOSPlatform("windows")] - [LibraryImport("user32")] - private static partial nint GetForegroundWindow(); - - [SupportedOSPlatform("windows")] - [LibraryImport("user32")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool SetForegroundWindow(nint hWnd); + private static partial bool FreeConsole(); public static bool SetConsoleWindowStateSupported => OperatingSystem.IsWindows(); + public static bool HasConsoleWindow => OperatingSystem.IsWindows() && GetConsoleWindow() != nint.Zero; public static void SetConsoleWindowState(bool show) { @@ -42,22 +34,31 @@ namespace Ryujinx.Common.Helper [SupportedOSPlatform("windows")] private static void SetConsoleWindowStateWindows(bool show) { - const int SW_HIDE = 0; - const int SW_SHOW = 5; - - nint hWnd = GetConsoleWindow(); - - if (hWnd == nint.Zero) + if (show) { - Logger.Warning?.Print(LogClass.Application, "Attempted to show/hide console window but console window does not exist"); + if (GetConsoleWindow() != nint.Zero) + { + Logger.SetConsoleTargetEnabled(true); + } return; } - SetForegroundWindow(hWnd); + Logger.SetConsoleTargetEnabled(false); + DetachConsole(); + } - hWnd = GetForegroundWindow(); + [SupportedOSPlatform("windows")] + private static void DetachConsole() + { + if (GetConsoleWindow() == nint.Zero) + { + return; + } - ShowWindow(hWnd, show ? SW_SHOW : SW_HIDE); + if (!FreeConsole()) + { + Logger.Warning?.Print(LogClass.Application, "Attempted to detach console window but the operation failed"); + } } } } diff --git a/src/Ryujinx.Common/Logging/Logger.cs b/src/Ryujinx.Common/Logging/Logger.cs index 64a76a3e4..b5450c94b 100644 --- a/src/Ryujinx.Common/Logging/Logger.cs +++ b/src/Ryujinx.Common/Logging/Logger.cs @@ -136,11 +136,7 @@ namespace Ryujinx.Common.Logging _time = Stopwatch.StartNew(); - // Logger should log to console by default - AddTarget(new AsyncLogTargetWrapper( - new ConsoleLogTarget("console"), - 1000, - AsyncLogTargetOverflowAction.Discard)); + SetConsoleTargetEnabled(true); Notice = new Log(LogLevel.Notice); @@ -173,6 +169,21 @@ namespace Ryujinx.Common.Logging Updated += target.Log; } + public static void SetConsoleTargetEnabled(bool enabled) + { + if (enabled) + { + AddTarget(new AsyncLogTargetWrapper( + new ConsoleLogTarget("console"), + 1000, + AsyncLogTargetOverflowAction.Discard)); + } + else + { + RemoveTarget("console"); + } + } + public static void RemoveTarget(string target) { ILogTarget logTarget = GetTarget(target); diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 9e139c186..57fd825b3 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -656,10 +656,19 @@ namespace Ryujinx.Ava.UI.ViewModels get => ConfigurationState.Instance.UI.ShowConsole; set { + bool restartRequired = value && !ConsoleHelper.HasConsoleWindow; + ConfigurationState.Instance.UI.ShowConsole.Value = value; ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + if (restartRequired) + { + NotificationHelper.ShowInformation( + LocaleManager.Instance[LocaleKeys.SettingsAppRequiredRestartMessage], + LocaleManager.Instance[LocaleKeys.SettingsShowConsoleRestartMessage]); + } + OnPropertyChanged(); } } From b222f671f39ca24683e20b32cbf43aa071f1a734 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 4 May 2026 23:32:35 -0500 Subject: [PATCH 23/61] fix: use run_number instead of run_id --- .forgejo/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index a93e02cf4..224685909 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -218,7 +218,7 @@ jobs: }); const {owner, repo} = context.repo; - const run_id = ${{ forgejo.run_id }}; + const run_id = ${{ forgejo.run_number }}; const pull_head_sha = '${{ forgejo.event.workflow_run.head_sha }}'; const issue_number = ${{ forgejo.event.pull_request.number }}; From e1dcaef709d7cf9e5a1389deb5821412ba975db0 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 4 May 2026 23:32:53 -0500 Subject: [PATCH 24/61] fix: don't try to access .length if artifacts is undefined --- .forgejo/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 224685909..1cd3697fb 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -225,7 +225,7 @@ jobs: core.info(`Using pull request ${issue_number}`); const {data: {artifacts}} = await forgejo.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id}); - if (!artifacts.length) { + if (artifacts == undefined || !artifacts.length) { return core.error(`No artifacts found`); } let body = `Download the artifacts for this pull request:\n`; From d2b2d650619b4d4d87d4d80748fbd06e342316a4 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 4 May 2026 23:33:28 -0500 Subject: [PATCH 25/61] chore: Remove unused variable. --- .forgejo/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 1cd3697fb..80344ff84 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -219,7 +219,6 @@ jobs: const {owner, repo} = context.repo; const run_id = ${{ forgejo.run_number }}; - const pull_head_sha = '${{ forgejo.event.workflow_run.head_sha }}'; const issue_number = ${{ forgejo.event.pull_request.number }}; core.info(`Using pull request ${issue_number}`); From 1d3d4197b7860f08b325d74aa75d06d132be3337 Mon Sep 17 00:00:00 2001 From: ryuadmin Date: Tue, 5 May 2026 06:22:51 +0000 Subject: [PATCH 26/61] fix: *hopefully* fix build comments --- .forgejo/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 80344ff84..13f949594 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -218,7 +218,7 @@ jobs: }); const {owner, repo} = context.repo; - const run_id = ${{ forgejo.run_number }}; + const run_id = ${{ env.FORGEJO_RUN_ID }}; const issue_number = ${{ forgejo.event.pull_request.number }}; core.info(`Using pull request ${issue_number}`); From b0179e6433f08504a4ab7c6a72b87d37a86c27f5 Mon Sep 17 00:00:00 2001 From: ryuadmin Date: Tue, 5 May 2026 07:50:58 +0000 Subject: [PATCH 27/61] [ci skip] Improve logging for PR build comment step. --- .forgejo/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 13f949594..98eeb759a 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -221,11 +221,11 @@ jobs: const run_id = ${{ env.FORGEJO_RUN_ID }}; const issue_number = ${{ forgejo.event.pull_request.number }}; - core.info(`Using pull request ${issue_number}`); + core.info(`Using run ID ${run_id} from pull request ${issue_number}`); const {data: {artifacts}} = await forgejo.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id}); if (artifacts == undefined || !artifacts.length) { - return core.error(`No artifacts found`); + return core.error(`No artifacts found for run ID`); } let body = `Download the artifacts for this pull request:\n`; for (const art of artifacts) { From 722eb93554085fe6ecf5a0a6b76775d0f8dbde0a Mon Sep 17 00:00:00 2001 From: greem Date: Tue, 5 May 2026 08:07:38 +0000 Subject: [PATCH 28/61] Use a dedicated access token instead of the runner-generated one. --- .forgejo/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 98eeb759a..401396914 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -211,9 +211,11 @@ jobs: - build_macos steps: - uses: actions/github-script@v9 + env: + COMMENTER_TOKEN: ${{ secrets.COMMENTER_TOKEN }} with: script: | - const forgejo = getOctokit(process.env.FORGEJO_TOKEN, { + const forgejo = getOctokit(process.env.COMMENTER_TOKEN, { baseUrl: 'https://git.ryujinx.app/api/v1' }); From 49dd56953ce7e72aeda5f7e841f97570b05cb62b Mon Sep 17 00:00:00 2001 From: greem Date: Tue, 5 May 2026 08:52:08 +0000 Subject: [PATCH 29/61] annoying error --- .forgejo/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 401396914..3500066d5 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -214,6 +214,7 @@ jobs: env: COMMENTER_TOKEN: ${{ secrets.COMMENTER_TOKEN }} with: + token: 'n/a' script: | const forgejo = getOctokit(process.env.COMMENTER_TOKEN, { baseUrl: 'https://git.ryujinx.app/api/v1' From c4788154fdbd9d60392d80753cb8469212765ad9 Mon Sep 17 00:00:00 2001 From: greem Date: Tue, 5 May 2026 09:15:54 +0000 Subject: [PATCH 30/61] even more annoying skill issue! --- .forgejo/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 3500066d5..03388765a 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -214,7 +214,7 @@ jobs: env: COMMENTER_TOKEN: ${{ secrets.COMMENTER_TOKEN }} with: - token: 'n/a' + github-token: 'n/a' script: | const forgejo = getOctokit(process.env.COMMENTER_TOKEN, { baseUrl: 'https://git.ryujinx.app/api/v1' From 2d2661298c7ef484cf9dfc421e1409efdf137aa1 Mon Sep 17 00:00:00 2001 From: greem Date: Tue, 5 May 2026 09:29:41 +0000 Subject: [PATCH 31/61] Update .forgejo/workflows/build.yml --- .forgejo/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 03388765a..04cf4ea32 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -1,7 +1,7 @@ name: Build PR on: - pull_request: + pull_request_target: branches: [ master ] paths: - '**' From bf083a716c3bc264ebca15e11236d1709c521c12 Mon Sep 17 00:00:00 2001 From: greem Date: Tue, 5 May 2026 10:12:06 +0000 Subject: [PATCH 32/61] use new workflow type in conditions --- .forgejo/workflows/build.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 04cf4ea32..e7b961602 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -63,7 +63,7 @@ jobs: - name: Change config filename run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash - if: forgejo.event_name == 'pull_request' + if: forgejo.event_name == 'pull_request_target' - name: 'Cache: ~/.nuget/packages' uses: actions/cache@v5 @@ -85,7 +85,7 @@ jobs: - name: Publish Ryujinx run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.result }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained - if: forgejo.event_name == 'pull_request' + if: forgejo.event_name == 'pull_request_target' - name: Packing Windows builds if: contains(matrix.platform.name, 'win') @@ -98,10 +98,10 @@ jobs: with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }} path: artifact - if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'win') + if: forgejo.event_name == 'pull_request_target' && contains(matrix.platform.name, 'win') - name: Build AppImage - if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux') + if: forgejo.event_name == 'pull_request_target' && contains(matrix.platform.name, 'linux') run: | chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh @@ -134,7 +134,7 @@ jobs: - name: Upload Ryujinx AppImage artifact uses: actions/upload-artifact@v5 - if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux') + if: forgejo.event_name == 'pull_request_target' && contains(matrix.platform.name, 'linux') with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}-AppImage path: publish_appimage @@ -182,7 +182,7 @@ jobs: - name: Change config filename run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash - if: forgejo.event_name == 'pull_request' + if: forgejo.event_name == 'pull_request_target' - name: 'Cache: ~/.nuget/packages' uses: actions/cache@v5 @@ -201,7 +201,7 @@ jobs: with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-macos_universal path: "publish/*.tar.gz" - if: forgejo.event_name == 'pull_request' + if: forgejo.event_name == 'pull_request_target' post_comment: name: Post comment linking uploaded artifacts From ad34237fc63dadef6b885724ec99f3c5072b7317 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 10 May 2026 15:56:44 -0500 Subject: [PATCH 33/61] Revert "use new workflow type in conditions" This reverts commit bf083a716c3bc264ebca15e11236d1709c521c12. --- .forgejo/workflows/build.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index e7b961602..04cf4ea32 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -63,7 +63,7 @@ jobs: - name: Change config filename run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash - if: forgejo.event_name == 'pull_request_target' + if: forgejo.event_name == 'pull_request' - name: 'Cache: ~/.nuget/packages' uses: actions/cache@v5 @@ -85,7 +85,7 @@ jobs: - name: Publish Ryujinx run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.result }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained - if: forgejo.event_name == 'pull_request_target' + if: forgejo.event_name == 'pull_request' - name: Packing Windows builds if: contains(matrix.platform.name, 'win') @@ -98,10 +98,10 @@ jobs: with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }} path: artifact - if: forgejo.event_name == 'pull_request_target' && contains(matrix.platform.name, 'win') + if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'win') - name: Build AppImage - if: forgejo.event_name == 'pull_request_target' && contains(matrix.platform.name, 'linux') + if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux') run: | chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh @@ -134,7 +134,7 @@ jobs: - name: Upload Ryujinx AppImage artifact uses: actions/upload-artifact@v5 - if: forgejo.event_name == 'pull_request_target' && contains(matrix.platform.name, 'linux') + if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux') with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}-AppImage path: publish_appimage @@ -182,7 +182,7 @@ jobs: - name: Change config filename run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash - if: forgejo.event_name == 'pull_request_target' + if: forgejo.event_name == 'pull_request' - name: 'Cache: ~/.nuget/packages' uses: actions/cache@v5 @@ -201,7 +201,7 @@ jobs: with: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-macos_universal path: "publish/*.tar.gz" - if: forgejo.event_name == 'pull_request_target' + if: forgejo.event_name == 'pull_request' post_comment: name: Post comment linking uploaded artifacts From 708186d8d256521f0ff0d3bb9f1fa3db4f662b94 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Sun, 10 May 2026 15:56:48 -0500 Subject: [PATCH 34/61] Revert "Update .forgejo/workflows/build.yml" This reverts commit 2d2661298c7ef484cf9dfc421e1409efdf137aa1. --- .forgejo/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 04cf4ea32..03388765a 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -1,7 +1,7 @@ name: Build PR on: - pull_request_target: + pull_request: branches: [ master ] paths: - '**' From 5d8cb3e378e6eafb15f651bf462dcbfcc79f5228 Mon Sep 17 00:00:00 2001 From: greem Date: Sun, 10 May 2026 21:09:26 +0000 Subject: [PATCH 35/61] This stupid bullshit doesn't work, I'm done --- .forgejo/workflows/build.yml | 43 ------------------------------------ 1 file changed, 43 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 03388765a..e7052a6f6 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -202,46 +202,3 @@ jobs: name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-macos_universal path: "publish/*.tar.gz" if: forgejo.event_name == 'pull_request' - - post_comment: - name: Post comment linking uploaded artifacts - runs-on: ubuntu-latest - needs: - - build - - build_macos - steps: - - uses: actions/github-script@v9 - env: - COMMENTER_TOKEN: ${{ secrets.COMMENTER_TOKEN }} - with: - github-token: 'n/a' - script: | - const forgejo = getOctokit(process.env.COMMENTER_TOKEN, { - baseUrl: 'https://git.ryujinx.app/api/v1' - }); - - const {owner, repo} = context.repo; - const run_id = ${{ env.FORGEJO_RUN_ID }}; - - const issue_number = ${{ forgejo.event.pull_request.number }}; - core.info(`Using run ID ${run_id} from pull request ${issue_number}`); - - const {data: {artifacts}} = await forgejo.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id}); - if (artifacts == undefined || !artifacts.length) { - return core.error(`No artifacts found for run ID`); - } - let body = `Download the artifacts for this pull request:\n`; - for (const art of artifacts) { - const url = `https://git.ryujinx.app/api/v1/repos/${owner}/${repo}/actions/artifacts/${art.id}/zip`; - body += `\n* [${art.name}](${url})`; - } - - const {data: comments} = await forgejo.rest.issues.listComments({repo, owner, issue_number}); - const existing_comment = comments.find((c) => c.user.login === 'forgejo-actions'); - if (existing_comment) { - core.info(`Updating comment ${existing_comment.id}`); - await forgejo.rest.issues.updateComment({repo, owner, comment_id: existing_comment.id, body}); - } else { - core.info(`Creating a comment`); - await forgejo.rest.issues.createComment({repo, owner, issue_number, body}); - } From 1f9bfab9239402204933dfbb462fbf2c710b3dc0 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 10 May 2026 23:18:23 +0000 Subject: [PATCH 36/61] Updated OpenGL calls to no longer be deprecated (#83) - updated SharpCompress 0.47.4 -> 0.48.0 (security) - set ProcessResult to be nullable (since it is) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/83 --- Directory.Packages.props | 4 ++-- .../Image/TextureView.cs | 18 +++++++++--------- .../PersistentBuffers.cs | 4 ++-- src/Ryujinx.Graphics.OpenGL/Pipeline.cs | 10 +++++----- src/Ryujinx.Graphics.OpenGL/Program.cs | 2 +- .../Queries/BufferedQuery.cs | 2 +- .../Loaders/Processes/ProcessLoader.cs | 2 ++ 7 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7c94ffe24..4466f2777 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + @@ -64,4 +64,4 @@ - + \ No newline at end of file diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs index 12ec23c8b..9b1df80dc 100644 --- a/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs +++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureView.cs @@ -551,7 +551,7 @@ namespace Ryujinx.Graphics.OpenGL.Image level, x, width, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -579,7 +579,7 @@ namespace Ryujinx.Graphics.OpenGL.Image layer, width, 1, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -609,7 +609,7 @@ namespace Ryujinx.Graphics.OpenGL.Image y, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -643,7 +643,7 @@ namespace Ryujinx.Graphics.OpenGL.Image width, height, 1, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -675,7 +675,7 @@ namespace Ryujinx.Graphics.OpenGL.Image y, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -744,7 +744,7 @@ namespace Ryujinx.Graphics.OpenGL.Image level, 0, width, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -773,7 +773,7 @@ namespace Ryujinx.Graphics.OpenGL.Image 0, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -807,7 +807,7 @@ namespace Ryujinx.Graphics.OpenGL.Image width, height, depth, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize, data); } @@ -843,7 +843,7 @@ namespace Ryujinx.Graphics.OpenGL.Image 0, width, height, - format.PixelFormat, + (InternalFormat) format.PixelFormat, mipSize / 6, data + faceOffset); } diff --git a/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs b/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs index d5c02f4df..b77ac5f17 100644 --- a/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs +++ b/src/Ryujinx.Graphics.OpenGL/PersistentBuffers.cs @@ -27,7 +27,7 @@ namespace Ryujinx.Graphics.OpenGL public void Map(BufferHandle handle, int size) { GL.BindBuffer(BufferTarget.CopyWriteBuffer, handle); - nint ptr = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, size, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit); + nint ptr = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, size, MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapPersistentBit); _maps[handle] = ptr; } @@ -75,7 +75,7 @@ namespace Ryujinx.Graphics.OpenGL GL.BindBuffer(BufferTarget.CopyWriteBuffer, _copyBufferHandle); GL.BufferStorage(BufferTarget.CopyWriteBuffer, requiredSize, nint.Zero, BufferStorageFlags.MapReadBit | BufferStorageFlags.MapPersistentBit); - _bufferMap = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, requiredSize, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit); + _bufferMap = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, requiredSize, MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapPersistentBit); } } diff --git a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs index e58e6f2b9..5b1e63e3b 100644 --- a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs +++ b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs @@ -924,8 +924,8 @@ namespace Ryujinx.Graphics.OpenGL GL.Disable(EnableCap.CullFace); return; } - - GL.CullFace(face.Convert()); + + GL.CullFace((TriangleFace) face.Convert()); GL.Enable(EnableCap.CullFace); } @@ -1085,12 +1085,12 @@ namespace Ryujinx.Graphics.OpenGL { if (frontMode == backMode) { - GL.PolygonMode(MaterialFace.FrontAndBack, frontMode.Convert()); + GL.PolygonMode((TriangleFace) MaterialFace.FrontAndBack, frontMode.Convert()); } else { - GL.PolygonMode(MaterialFace.Front, frontMode.Convert()); - GL.PolygonMode(MaterialFace.Back, backMode.Convert()); + GL.PolygonMode((TriangleFace) MaterialFace.Front, frontMode.Convert()); + GL.PolygonMode((TriangleFace) MaterialFace.Back, backMode.Convert()); } } diff --git a/src/Ryujinx.Graphics.OpenGL/Program.cs b/src/Ryujinx.Graphics.OpenGL/Program.cs index 608a03451..cb9933c10 100644 --- a/src/Ryujinx.Graphics.OpenGL/Program.cs +++ b/src/Ryujinx.Graphics.OpenGL/Program.cs @@ -59,7 +59,7 @@ namespace Ryujinx.Graphics.OpenGL GL.CompileShader(shaderHandle); break; case TargetLanguage.Spirv: - GL.ShaderBinary(1, ref shaderHandle, (BinaryFormat)All.ShaderBinaryFormatSpirVArb, shader.BinaryCode, shader.BinaryCode.Length); + GL.ShaderBinary(1, ref shaderHandle, ShaderBinaryFormat.ShaderBinaryFormatSpirV, shader.BinaryCode, shader.BinaryCode.Length); GL.SpecializeShader(shaderHandle, "main", 0, (int[])null, (int[])null); break; } diff --git a/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs b/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs index f39829923..c9dbcdcf2 100644 --- a/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs +++ b/src/Ryujinx.Graphics.OpenGL/Queries/BufferedQuery.cs @@ -32,7 +32,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries GL.BufferStorage(BufferTarget.QueryBuffer, sizeof(long), (nint)(&defaultValue), BufferStorageFlags.MapReadBit | BufferStorageFlags.MapWriteBit | BufferStorageFlags.MapPersistentBit); } - _bufferMap = GL.MapBufferRange(BufferTarget.QueryBuffer, nint.Zero, sizeof(long), BufferAccessMask.MapReadBit | BufferAccessMask.MapWriteBit | BufferAccessMask.MapPersistentBit); + _bufferMap = GL.MapBufferRange(BufferTarget.QueryBuffer, nint.Zero, sizeof(long), MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapWriteBit | MapBufferAccessMask.MapPersistentBit); } public void Reset() diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index e0edd2df5..900703f6e 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -28,6 +28,7 @@ namespace Ryujinx.HLE.Loaders.Processes private ulong _latestPid; +#nullable enable public ProcessResult? ActiveApplication { get @@ -44,6 +45,7 @@ namespace Ryujinx.HLE.Loaders.Processes return value; } } +#nullable disable public ProcessLoader(Switch device) { From bf7f978f9da0d368aae449d798550f2593b412d1 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 10 May 2026 23:18:53 +0000 Subject: [PATCH 37/61] [HLE] Implemented ILibraryAppletSelfAccessor:1 (#79) Needed for Tomodachi Life: Living the Dream (?) based on [this](https://www.reddit.com/r/Ryubing/comments/1t4lfc9/comment/ok4e7tu/) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/79 --- .../ILibraryAppletSelfAccessor.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs index fc02ea172..44c4d133a 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs @@ -44,6 +44,22 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib return ResultCode.Success; } + + [CommandCmif(1)] + // PushOutData(object) + public ResultCode PushOutData(ServiceCtx context) + { + IStorage appletData = GetObject(context, 0); + + if (appletData == null || appletData.Data.Length == 0) // is this necessary? + { + return ResultCode.NullObject; + } + + _appletStandalone.InputData.Enqueue(appletData.Data); + + return ResultCode.Success; + } [CommandCmif(11)] // GetLibraryAppletInfo() -> nn::am::service::LibraryAppletInfo From 5511ff5686d712e53cf4d6b16d31c8da4dda92c8 Mon Sep 17 00:00:00 2001 From: Frosch Date: Sun, 10 May 2026 23:19:59 +0000 Subject: [PATCH 38/61] fix: gamepads have the same name (#27) When connecting multiple controllers of the same model, the first device's name ends with (0), the second with (1), the third with (1), the fourth with (1), and so on. To ensure these names are truly unique, GetUniqueGamepadName is now called recursively. Before: ![image](/attachments/c27ab407-0945-48d8-92a8-6f1fe7fb2727) After: ![image](/attachments/da7b1427-958c-45d5-8351-6f977d971e1e) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/27 --- src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index e5f085e0f..51229af72 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -574,7 +574,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (Devices.Any(controller => controller.Name == name)) { controllerNumber++; - name = GetGamepadName(gamepad, controllerNumber); + name = GetUniqueGamepadName(gamepad, ref controllerNumber); } return name; From e9c31bea3b3483a9ceee884be8b8077bf91c657c Mon Sep 17 00:00:00 2001 From: Shyanne Date: Sun, 10 May 2026 23:29:15 +0000 Subject: [PATCH 39/61] [DEBUG] Implemented NetLog logging type (#5) Implemented a new debug log type called NetLog and added more verbose logging to the LDN service. I'd like some feedback on what all should be logged under this category -- I'm likely going to be adding logs for sockets as well, but I want to know specifically if the logging should be more or less verbose and what would be the most helpful things to log. ![image](/attachments/70d5d467-2b57-436b-944f-7bf7a1f609af) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/5 --- assets/Locales/Root.json | 50 +++++++++ src/Ryujinx.Common/Logging/LogLevel.cs | 1 + src/Ryujinx.Common/Logging/Logger.cs | 2 + .../IUserLocalCommunicationService.cs | 101 +++++++++++++++--- .../LdnRyu/LdnMasterProxyClient.cs | 8 +- .../Ldn/UserServiceCreator/Station.cs | 3 + src/Ryujinx/Headless/HeadlessRyujinx.cs | 1 + src/Ryujinx/Headless/Options.cs | 6 ++ .../Configuration/ConfigurationFileFormat.cs | 7 +- .../ConfigurationState.Migration.cs | 3 +- .../Configuration/ConfigurationState.Model.cs | 6 ++ .../Configuration/ConfigurationState.cs | 2 + .../Systems/Configuration/LoggerModule.cs | 2 + .../UI/ViewModels/SettingsViewModel.cs | 3 + .../Views/Settings/SettingsLoggingView.axaml | 4 + 15 files changed, 179 insertions(+), 20 deletions(-) diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index 0b1115cdc..eee476519 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -6100,6 +6100,31 @@ "zh_TW": "檔案系統全域存取日誌模式:" } }, + { + "ID": "SettingsTabLoggingEnableNetLogs", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Enable Net Logs", + "es_ES": "Habilitar registros de red.", + "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": "SettingsTabLoggingDeveloperOptions", "Translations": { @@ -17075,6 +17100,31 @@ "zh_TW": "啟用檔案系統存取日誌輸出到控制台中。可能的模式為 0 到 3。" } }, + { + "ID": "NetLogTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Prints network log messages in the console.", + "es_ES": "Imprimir registros de red en la consola.", + "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": "DeveloperOptionTooltip", "Translations": { diff --git a/src/Ryujinx.Common/Logging/LogLevel.cs b/src/Ryujinx.Common/Logging/LogLevel.cs index 282b07111..e8778191a 100644 --- a/src/Ryujinx.Common/Logging/LogLevel.cs +++ b/src/Ryujinx.Common/Logging/LogLevel.cs @@ -12,6 +12,7 @@ namespace Ryujinx.Common.Logging Error, Guest, AccessLog, + NetLog, Notice, Trace, } diff --git a/src/Ryujinx.Common/Logging/Logger.cs b/src/Ryujinx.Common/Logging/Logger.cs index b5450c94b..788d2ab3d 100644 --- a/src/Ryujinx.Common/Logging/Logger.cs +++ b/src/Ryujinx.Common/Logging/Logger.cs @@ -119,6 +119,7 @@ namespace Ryujinx.Common.Logging public static Log? Error { get; private set; } public static Log? Guest { get; private set; } public static Log? AccessLog { get; private set; } + public static Log? NetLog { get; private set; } public static Log? Stub { get; private set; } public static Log? Trace { get; private set; } public static Log Notice { get; } // Always enabled @@ -247,6 +248,7 @@ namespace Ryujinx.Common.Logging case LogLevel.Error : Error = enabled ? new Log(LogLevel.Error) : null; break; case LogLevel.Guest : Guest = enabled ? new Log(LogLevel.Guest) : null; break; case LogLevel.AccessLog : AccessLog = enabled ? new Log(LogLevel.AccessLog) : null; break; + case LogLevel.NetLog : NetLog = enabled ? new Log(LogLevel.NetLog) : null; break; case LogLevel.Stub : Stub = enabled ? new Log(LogLevel.Stub) : null; break; case LogLevel.Trace : Trace = enabled ? new Log(LogLevel.Trace) : null; break; case LogLevel.Notice : break; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 71d1623f3..2f764e99f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -5,7 +5,6 @@ using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.Common.Utilities; using Ryujinx.Cpu; -using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Ldn.Types; @@ -15,7 +14,6 @@ using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; -using System.ComponentModel; using System.IO; using System.Net; using System.Net.NetworkInformation; @@ -68,10 +66,11 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (localCommunicationId == localCommunicationIdChecked) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"CheckLocalCommumicationIdPermission: Checked!"); return true; } } - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"CheckLocalCommumicationIdPermission: Check failed!"); return false; } @@ -82,7 +81,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { context.ResponseData.Write((int)NetworkState.Error); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetState: _nifmResultCode = {_nifmResultCode.ToString()}"); return ResultCode.Success; } @@ -114,12 +113,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfo: _nifmResultCode = {_nifmResultCode.ToString()}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetState: resultCode = {resultCode.ToString()}"); return resultCode; } @@ -135,18 +136,22 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_state == NetworkState.StationConnected) { networkInfo = _station.NetworkInfo; + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: _station"); } else if (_state == NetworkState.AccessPointCreated) { networkInfo = _accessPoint.NetworkInfo; + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: _accessPoint"); } else { networkInfo = new NetworkInfo(); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: Invalid state!"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfoImpl: networkInfo = {networkInfo}"); return ResultCode.InvalidState; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfoImpl: networkInfo = {networkInfo}"); return ResultCode.Success; } @@ -198,7 +203,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); + Logger.NetLog?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address)); context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask)); @@ -206,7 +211,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP."); + Logger.NetLog?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP."); context.ResponseData.Write(config.ProxyIp); context.ResponseData.Write(config.ProxySubnetMask); @@ -227,7 +232,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator // NOTE: Returns ResultCode.InvalidArgument if _disconnectReason is null, doesn't occur in our case. context.ResponseData.Write((short)_disconnectReason); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetDisconnectReason: {_disconnectReason}"); return ResultCode.Success; } @@ -247,12 +252,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: resultCode = {resultCode}"); return resultCode; } @@ -263,7 +270,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator }; context.ResponseData.WriteStruct(securityParameter); - + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: securityParameter = {securityParameter}"); return ResultCode.Success; } @@ -273,12 +281,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: resultCode = {resultCode}"); return resultCode; } @@ -292,6 +302,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator }; context.ResponseData.WriteStruct(networkConfig); + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: networkConfig = {networkConfig}"); return ResultCode.Success; } @@ -322,12 +334,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkInfoLatestUpdate: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo); if (resultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkInfoLatestUpdate: resultCode = {resultCode}"); return resultCode; } @@ -378,6 +392,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -400,6 +415,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (scanFilter.Ssid.Length <= 31) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}"); return resultCode; } } @@ -408,11 +424,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (scanFilterFlag > ScanFilterFlag.All) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}"); return resultCode; } if (_state - 3 >= NetworkState.AccessPoint) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ScanImpl: Invalid state!"); resultCode = ResultCode.InvalidState; } else @@ -437,7 +455,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } } - + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}"); return resultCode; } @@ -462,6 +481,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanInternal: availableGames = {availableGames}"); return ResultCode.Success; } @@ -502,7 +522,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator throw new ArgumentException($"{GetType().FullName}: Protocol value is not 1 or 3!! Protocol value: {protocolValue}"); } - Logger.Stub?.PrintStub(LogClass.ServiceLdn, $"Protocol value: {protocolValue}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetProtocol: protocolValue = {protocolValue}"); + Logger.Stub?.PrintStub(LogClass.ServiceLdn, new { protocolValue}); return ResultCode.Success; } @@ -512,11 +533,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenAccessPoint: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (_state != NetworkState.Initialized) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "OpenAccessPoint: Invalid state!"); return ResultCode.InvalidState; } @@ -538,6 +561,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CloseAccessPoint: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -547,6 +571,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseAccessPoint: Invalid state!"); return ResultCode.InvalidState; } @@ -596,11 +621,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId); if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CreateNetworkImpl: Invalid object!"); return ResultCode.InvalidObject; } if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -629,16 +656,22 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator AddressList addressList = MemoryMarshal.Cast(addressListBytes)[0]; _accessPoint.CreateNetworkPrivate(securityConfig, securityParameter, userConfig, networkConfig, addressList); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: Created private network! " + + $"| securityConfig = {securityConfig} | securityParameter = {securityParameter} " + + $"| userConfig = {userConfig} | networkConfig = {networkConfig} | addressList = {addressList}"); } else { _accessPoint.CreateNetwork(securityConfig, userConfig, networkConfig); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: Created network! " + + $"| securityConfig = {securityConfig} | userConfig = {userConfig} | networkConfig = {networkConfig}"); } return ResultCode.Success; } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CreateNetworkImpl: Invalid state!"); return ResultCode.InvalidState; } } @@ -660,6 +693,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DestroyNetworkImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -676,9 +710,11 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator CloseAccessPoint(); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DestroyNetworkImpl: Invalid state!"); return ResultCode.InvalidState; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DestroyNetworkImpl: Invalid argument!"); return ResultCode.InvalidArgument; } @@ -695,14 +731,17 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"RejectImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (_state != NetworkState.AccessPointCreated) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "RejectImpl: Invalid state!"); return ResultCode.InvalidState; // Must be network host to reject nodes. } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"RejectImpl: disconnectReason = {disconnectReason} | nodeId = {nodeId}"); return NetworkClient.Reject(disconnectReason, nodeId); } @@ -714,11 +753,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetAdvertiseData: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (bufferSize is 0 or > LdnConst.AdvertiseDataSizeMax) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetAdvertiseData: Invalid argument!"); return ResultCode.InvalidArgument; } @@ -727,11 +768,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator byte[] advertiseData = new byte[bufferSize]; context.Memory.Read(bufferPosition, advertiseData); - + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetAdvertiseData: advertiseData = {advertiseData}"); return _accessPoint.SetAdvertiseData(advertiseData); } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetAdvertiseData: Invalid state!"); return ResultCode.InvalidState; } } @@ -744,20 +786,24 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetStationAcceptPolicy: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (acceptPolicy > AcceptPolicy.WhiteList) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetStationAcceptPolicy: Invalid argument!"); return ResultCode.InvalidArgument; } if (_state is NetworkState.AccessPoint or NetworkState.AccessPointCreated) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetStationAcceptPolicy: acceptPolicy = {acceptPolicy}"); return _accessPoint.SetStationAcceptPolicy(acceptPolicy); } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetStationAcceptPolicy: Invalid state!"); return ResultCode.InvalidState; } } @@ -768,6 +814,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"AddAcceptFilterEntry: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -782,6 +829,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ClearAcceptFilter: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -796,11 +844,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenStation: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } if (_state != NetworkState.Initialized) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "OpenStation: Invalid state!"); return ResultCode.InvalidState; } @@ -813,6 +863,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator // NOTE: Calls nifm service and returns related result codes. // Since we use our own implementation we can return ResultCode.Success. + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenStation: _station = {_station}"); return ResultCode.Success; } @@ -823,6 +875,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CloseStation: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -832,11 +885,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } else { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseStation: Invalid state!"); return ResultCode.InvalidState; } SetState(NetworkState.Initialized); - + + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseStation: Closed."); return ResultCode.Success; } @@ -901,11 +956,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId); if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ConnectImpl: Invalid object!"); return ResultCode.InvalidObject; } if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -925,6 +982,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_state != NetworkState.Station) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ConnectImpl: Invalid state!"); resultCode = ResultCode.InvalidState; } else @@ -932,10 +990,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (isPrivate) { resultCode = _station.ConnectPrivate(securityConfig, securityParameter, userConfig, localCommunicationVersion, optionUnknown, networkConfig); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: Private connection established! " + + $"| securityConfig = {securityConfig} | securityParameter = {securityParameter} | userConfig = {userConfig} " + + $"| localCommunicationVersion = {localCommunicationVersion} | optionUnknown = {optionUnknown} | networkConfig = {networkConfig}"); } else { resultCode = _station.Connect(securityConfig, userConfig, localCommunicationVersion, optionUnknown, networkInfo); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: Connection established! " + + $"| securityConfig = {securityConfig} | userConfig = {userConfig} " + + $"| localCommunicationVersion = {localCommunicationVersion} | optionUnknown = {optionUnknown} | networkConfig = {networkConfig}"); } } } @@ -943,6 +1007,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: resultCode = {resultCode}"); + return resultCode; } @@ -957,6 +1023,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DisconnectImpl: _nifmResultCode = {_nifmResultCode}"); return _nifmResultCode; } @@ -970,14 +1037,17 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _disconnectReason = disconnectReason; + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DisconnectImpl: _disconnectReason = {_disconnectReason}"); return ResultCode.Success; } CloseStation(); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DisconnectImpl: Invalid state!"); return ResultCode.InvalidState; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DisconnectImpl: Invalid argument!"); return ResultCode.InvalidArgument; } @@ -994,6 +1064,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { if (_nifmResultCode != ResultCode.Success) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"Finalize: _disconnectReason = {_disconnectReason}"); return _nifmResultCode; } @@ -1010,11 +1081,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _stateChangeEventHandle = 0; } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"Finalize: resultCode = {resultCode}"); return resultCode; } private ResultCode FinalizeImpl(bool isCausedBySystem) { + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "FinalizeImpl"); DisconnectReason disconnectReason; switch (_state) @@ -1138,7 +1211,6 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator NetworkClient.SetGameVersion(context.Device.Processes.ActiveApplication.ApplicationControlProperties.DisplayVersion); resultCode = ResultCode.Success; - _nifmResultCode = resultCode; SetState(NetworkState.Initialized); @@ -1152,6 +1224,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator } } + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"InitializeImpl: resultCode = {resultCode}"); return resultCode; } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs index f93b1c4cc..37fa722b7 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs @@ -132,7 +132,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu protected override void OnConnected() { - Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}"); UpdatePassphraseIfNeeded(); @@ -141,7 +141,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu protected override void OnDisconnected() { - Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}"); _passphrase = null; @@ -174,7 +174,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu protected override void OnError(SocketError error) { - Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}"); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}"); _error.Set(); } @@ -428,7 +428,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}."); + Logger.NetLog?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}."); _hostedProxy.Start(); (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(); diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs index fa43f789e..b589c56a4 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; @@ -36,10 +37,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (Connected) { _parent.SetState(NetworkState.StationConnected); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"NetworkChanged: {NetworkInfo}"); } else { _parent.SetDisconnectReason(e.DisconnectReasonOrDefault(DisconnectReason.DestroyedByUser)); + Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"NetworkChanged: Disconnected (DestroyedByUser)"); } } else diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.cs b/src/Ryujinx/Headless/HeadlessRyujinx.cs index bba505dbb..a6ff5da57 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.cs @@ -254,6 +254,7 @@ namespace Ryujinx.Headless Logger.SetEnable(LogLevel.Trace, option.LoggingEnableTrace); Logger.SetEnable(LogLevel.Guest, !option.LoggingDisableGuest); Logger.SetEnable(LogLevel.AccessLog, option.LoggingEnableFsAccessLog); + Logger.SetEnable(LogLevel.NetLog, option.LoggingEnableFsAccessLog); if (!option.DisableFileLog) { diff --git a/src/Ryujinx/Headless/Options.cs b/src/Ryujinx/Headless/Options.cs index 382294cf7..a2dd6e4d3 100644 --- a/src/Ryujinx/Headless/Options.cs +++ b/src/Ryujinx/Headless/Options.cs @@ -108,6 +108,9 @@ namespace Ryujinx.Headless if (NeedsOverride(nameof(LoggingEnableFsAccessLog))) LoggingEnableFsAccessLog = configurationState.Logger.EnableFsAccessLog; + + if (NeedsOverride(nameof(LoggingEnableNetLog))) + LoggingEnableNetLog = configurationState.Logger.EnableNetLog; if (NeedsOverride(nameof(LoggingGraphicsDebugLevel))) LoggingGraphicsDebugLevel = configurationState.Logger.GraphicsDebugLevel; @@ -370,6 +373,9 @@ namespace Ryujinx.Headless [Option("enable-fs-access-logs", Required = false, Default = false, HelpText = "Enables printing FS access log messages.")] public bool LoggingEnableFsAccessLog { get; set; } + + [Option("enable-net-logs", Required = false, Default = false, HelpText = "Enables printing net log messages.")] + public bool LoggingEnableNetLog { get; set; } [Option("graphics-debug-level", Required = false, Default = GraphicsDebugLevel.None, HelpText = "Change Graphics API debug log level.")] public GraphicsDebugLevel LoggingGraphicsDebugLevel { get; set; } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs index f0fafb4e0..0b451eacb 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs @@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 71; + public const int CurrentVersion = 72; /// /// Version of the configuration file format @@ -113,6 +113,11 @@ namespace Ryujinx.Ava.Systems.Configuration /// Enables printing FS access log messages /// public bool LoggingEnableFsAccessLog { get; set; } + + /// + /// Enables printing network log messages + /// + public bool LoggingEnableNetLog { get; set; } /// /// Enables log messages from Avalonia diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index 163b7e98f..90a045a67 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -1,4 +1,4 @@ -using Avalonia.Media; + using Avalonia.Media; using Gommon; using Ryujinx.Ava.Systems.Configuration.System; using Ryujinx.Ava.Systems.Configuration.UI; @@ -68,6 +68,7 @@ namespace Ryujinx.Ava.Systems.Configuration Logger.EnableTrace.Value = cff.LoggingEnableTrace; Logger.EnableGuest.Value = cff.LoggingEnableGuest; Logger.EnableFsAccessLog.Value = cff.LoggingEnableFsAccessLog; + Logger.EnableNetLog.Value = cff.LoggingEnableNetLog; Logger.FilteredClasses.Value = cff.LoggingFilteredClasses; Logger.GraphicsDebugLevel.Value = cff.LoggingGraphicsDebugLevel; diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs index 2b4c8f991..7775125d4 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs @@ -257,6 +257,11 @@ namespace Ryujinx.Ava.Systems.Configuration /// Enables printing FS access log messages /// public ReactiveObject EnableFsAccessLog { get; private set; } + + /// + /// Enables printing network log messages + /// + public ReactiveObject EnableNetLog { get; private set; } /// /// Enables log messages from Avalonia @@ -289,6 +294,7 @@ namespace Ryujinx.Ava.Systems.Configuration EnableTrace = new ReactiveObject(); EnableGuest = new ReactiveObject(); EnableFsAccessLog = new ReactiveObject(); + EnableNetLog = new ReactiveObject(); EnableAvaloniaLog = new ReactiveObject(); FilteredClasses = new ReactiveObject(); EnableFileLog = new ReactiveObject(); diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs index 0e2f6aaec..e4874963d 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs @@ -47,6 +47,7 @@ namespace Ryujinx.Ava.Systems.Configuration LoggingEnableTrace = Logger.EnableTrace, LoggingEnableGuest = Logger.EnableGuest, LoggingEnableFsAccessLog = Logger.EnableFsAccessLog, + LoggingEnableNetLog = Logger.EnableNetLog, LoggingEnableAvalonia = Logger.EnableAvaloniaLog, LoggingFilteredClasses = Logger.FilteredClasses, LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel, @@ -176,6 +177,7 @@ namespace Ryujinx.Ava.Systems.Configuration Logger.EnableTrace.Value = false; Logger.EnableGuest.Value = true; Logger.EnableFsAccessLog.Value = false; + Logger.EnableNetLog.Value = false; Logger.EnableAvaloniaLog.Value = false; Logger.FilteredClasses.Value = []; Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None; diff --git a/src/Ryujinx/Systems/Configuration/LoggerModule.cs b/src/Ryujinx/Systems/Configuration/LoggerModule.cs index e3d08ab8c..29c38b3d2 100644 --- a/src/Ryujinx/Systems/Configuration/LoggerModule.cs +++ b/src/Ryujinx/Systems/Configuration/LoggerModule.cs @@ -26,6 +26,8 @@ namespace Ryujinx.Ava.Systems.Configuration (_, e) => Logger.SetEnable(LogLevel.Guest, e.NewValue); ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += (_, e) => Logger.SetEnable(LogLevel.AccessLog, e.NewValue); + ConfigurationState.Instance.Logger.EnableNetLog.Event += + (_, e) => Logger.SetEnable(LogLevel.NetLog, e.NewValue); ConfigurationState.Instance.Logger.FilteredClasses.Event += (_, e) => { diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index abb284960..6904d4ebc 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -273,6 +273,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableTrace { get; set; } public bool EnableGuest { get; set; } public bool EnableFsAccessLog { get; set; } + public bool EnableNetLog { get; set; } public bool EnableAvaloniaLog { get; set; } public bool EnableDebug { get; set; } public bool IsOpenAlEnabled { get; set; } @@ -725,6 +726,7 @@ namespace Ryujinx.Ava.UI.ViewModels EnableGuest = config.Logger.EnableGuest; EnableDebug = config.Logger.EnableDebug; EnableFsAccessLog = config.Logger.EnableFsAccessLog; + EnableNetLog = config.Logger.EnableNetLog; EnableAvaloniaLog = config.Logger.EnableAvaloniaLog; FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; @@ -848,6 +850,7 @@ namespace Ryujinx.Ava.UI.ViewModels config.Logger.EnableGuest.Value = EnableGuest; config.Logger.EnableDebug.Value = EnableDebug; config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog; + config.Logger.EnableNetLog.Value = EnableNetLog; config.Logger.EnableAvaloniaLog.Value = EnableAvaloniaLog; config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; diff --git a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml index fa3b4e866..325c4f599 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml @@ -70,6 +70,10 @@ ToolTip.Tip="{ext:Locale FileAccessLogTooltip}"> + + + From 8065dec74410d00efce1dbf8813260c7258c6f67 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 10 May 2026 23:31:45 +0000 Subject: [PATCH 40/61] [HLE] Renamed INotificationServicesForSystem and implemented a few commands and stubs (#6) Should allow Ring Fit and other games that rely heavily on the notification system to get in-game (?), needs testing. if you're wondering what happened to the first branch -- no you're not. (duplicated history somehow??) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/6 --- src/Ryujinx.Common/Logging/LogClass.cs | 1 + .../Notification/INotificationServices.cs | 24 ++++++++++++++ .../INotificationServicesForApplication.cs | 25 +++++++++++++++ .../INotificationServicesForSystem.cs | 8 ----- .../INotificationSystemEventAccessor.cs | 32 +++++++++++++++++++ 5 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 src/Ryujinx.HLE/HOS/Services/Notification/INotificationServices.cs delete mode 100644 src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForSystem.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs index 7c6810599..0e02cfcc2 100644 --- a/src/Ryujinx.Common/Logging/LogClass.cs +++ b/src/Ryujinx.Common/Logging/LogClass.cs @@ -51,6 +51,7 @@ namespace Ryujinx.Common.Logging ServiceNgct, ServiceNifm, ServiceNim, + ServiceNotification, ServiceNs, ServiceNsd, ServiceNtc, diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServices.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServices.cs new file mode 100644 index 000000000..e44983fce --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServices.cs @@ -0,0 +1,24 @@ +namespace Ryujinx.HLE.HOS.Services.Notification +{ + [Service("notif:s")] // 9.0.0+ + class INotificationServices : IpcService + { + public INotificationServices(ServiceCtx context) { } + + [CommandCmif(1000)] // 9.0.0+ + // GetNotificationCount() -> nn::notification::server::INotificationSystemEventAccessor + public ResultCode GetNotificationCount(ServiceCtx context) + { + MakeObject(context, new INotificationSystemEventAccessor(context)); + return ResultCode.Success; + } + + [CommandCmif(1040)] // 9.0.0+ + // GetNotificationSendingNotifier() -> nn::notification::server::INotificationSystemEventAccessor + public ResultCode GetNotificationSendingNotifier(ServiceCtx context) + { + MakeObject(context, new INotificationSystemEventAccessor(context)); + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs index 29f8bfa85..498fc52fb 100644 --- a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs +++ b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForApplication.cs @@ -1,8 +1,33 @@ +using Ryujinx.Common.Logging; + namespace Ryujinx.HLE.HOS.Services.Notification { [Service("notif:a")] // 9.0.0+ class INotificationServicesForApplication : IpcService { public INotificationServicesForApplication(ServiceCtx context) { } + + // Leaving this here since I can never find it: https://switchbrew.org/wiki/Glue_services + + [CommandCmif(520)] // 9.0.0+ + // ListAlarmSettings(nn::arp::ApplicationCertificate) -> s32 AlarmSettingsCount + public ResultCode ListAlarmSettings(ServiceCtx context) + { + // TO-DO: Currently just returns 0. Should read in an ApplicationCertificate. + int alarmSettingsCount = 0; + context.ResponseData.Write(alarmSettingsCount); + return ResultCode.Success; + } + + [CommandCmif(1000)] // 9.0.0+ + // Initialize(PID-descriptor, u64 pid_reserved) + public ResultCode Intialize(ServiceCtx context) + { + ulong pid = context.Request.HandleDesc.PId; + context.RequestData.ReadUInt64(); // pid placeholder, zero + + Logger.Stub?.PrintStub(LogClass.ServiceNotification, new { pid }); + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForSystem.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForSystem.cs deleted file mode 100644 index c5946be84..000000000 --- a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationServicesForSystem.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ryujinx.HLE.HOS.Services.Notification -{ - [Service("notif:s")] // 9.0.0+ - class INotificationServicesForSystem : IpcService - { - public INotificationServicesForSystem(ServiceCtx context) { } - } -} diff --git a/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs new file mode 100644 index 000000000..8cca7cc6e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Notification/INotificationSystemEventAccessor.cs @@ -0,0 +1,32 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Horizon.Common; +using System; + +namespace Ryujinx.HLE.HOS.Services.Notification +{ + class INotificationSystemEventAccessor : IpcService + { + + private readonly KEvent _getNotificationSendingNotifierEvent; + private int _getNotificationSendingNotifierEventHandle; + public INotificationSystemEventAccessor(ServiceCtx context) { } + + [CommandCmif(0)] // 9.0.0+ + // GetNotificationSendingNotifier() -> nn::notification::server::INotificationSystemEventAccessor + public ResultCode GetSystemEvent(ServiceCtx context) + { + if (_getNotificationSendingNotifierEventHandle == 0) + { + if (context.Process.HandleTable.GenerateHandle(_getNotificationSendingNotifierEvent.ReadableEvent, out _getNotificationSendingNotifierEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_getNotificationSendingNotifierEventHandle); + return ResultCode.Success; + } + } +} From e8cc252d9a79ec7cb1950d9f6e13cd5091d2bded Mon Sep 17 00:00:00 2001 From: yell0wsuit Date: Sun, 10 May 2026 23:39:19 +0000 Subject: [PATCH 41/61] [HLE] Fix StoreData layout and implement IDatabaseService.Append (#43) - Fixes `StoreData` layout/update handling so `UpdateLatest` returns the stored Mii data correctly. - Implements `IDatabaseService.Append` (https://switchbrew.org/wiki/Shared_Database_services#IDatabaseService) Also adds regression tests for `UpdateLatest` and `Append`. (Might) fix Mario Kart 8 Deluxe crashing on first boot due to failed Mii verification check (due to custom Mii from emulator), and potentially Tomodachi Life: Living the Dream for the "Import Mii from system" option. Co-authored-by: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/43 --- .../HOS/Services/Mii/DatabaseImpl.cs | 18 ++- .../HOS/Services/Mii/MiiDatabaseManager.cs | 26 ++++ .../Mii/StaticService/DatabaseServiceImpl.cs | 15 ++- .../Mii/StaticService/IDatabaseService.cs | 11 ++ src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs | 122 ++++++++++++++++++ 5 files changed, 184 insertions(+), 8 deletions(-) create mode 100644 src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs b/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs index 4bb736992..cd2115132 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/DatabaseImpl.cs @@ -81,8 +81,10 @@ namespace Ryujinx.HLE.HOS.Services.Mii return ResultCode.Success; } - public ResultCode UpdateLatest(DatabaseSessionMetadata metadata, IStoredData oldMiiData, SourceFlag flag, IStoredData newMiiData) where T : unmanaged + public ResultCode UpdateLatest(DatabaseSessionMetadata metadata, T oldMiiData, SourceFlag flag, out T newMiiData) where T : unmanaged, IStoredData { + newMiiData = default; + if (!flag.HasFlag(SourceFlag.Database)) { return ResultCode.NotFound; @@ -106,7 +108,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii newMiiData.SetFromStoreData(storeData); - if (oldMiiData == newMiiData) + if (oldMiiData.Equals(newMiiData)) { return ResultCode.NotUpdated; } @@ -286,6 +288,18 @@ namespace Ryujinx.HLE.HOS.Services.Mii return result; } + public ResultCode Append(DatabaseSessionMetadata metadata, CharInfo charInfo) + { + ResultCode result = _miiDatabase.Append(metadata, _utilityImpl, charInfo); + + if (result == ResultCode.Success) + { + result = _miiDatabase.SaveDatabase(); + } + + return result; + } + public ResultCode ConvertCharInfoToCoreData(CharInfo charInfo, out CoreData coreData) { coreData = new CoreData(); diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs b/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs index 356d42a85..f5a173d8e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/MiiDatabaseManager.cs @@ -449,6 +449,32 @@ namespace Ryujinx.HLE.HOS.Services.Mii return ResultCode.Success; } + public ResultCode Append(DatabaseSessionMetadata metadata, UtilityImpl utilityImpl, CharInfo charInfo) + { + if (!charInfo.IsValid()) + { + return ResultCode.InvalidCharInfo; + } + + if (charInfo.Type == 1) + { + return ResultCode.InvalidOperationOnSpecialMii; + } + + CoreData coreData = new(); + coreData.SetFromCharInfo(charInfo); + + StoreData storeData; + + do + { + storeData = StoreData.BuildFromCoreData(utilityImpl, coreData); + } + while (_database.GetIndexByCreatorId(out _, storeData.CreateId)); + + return AddOrReplace(metadata, storeData); + } + public ResultCode Delete(DatabaseSessionMetadata metadata, CreateId createId) { if (!_database.GetIndexByCreatorId(out int index, createId)) diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs index fc12e2533..c12517430 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/DatabaseServiceImpl.cs @@ -54,9 +54,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService protected override ResultCode UpdateLatest(CharInfo oldCharInfo, SourceFlag flag, out CharInfo newCharInfo) { - newCharInfo = default; - - return _database.UpdateLatest(_metadata, oldCharInfo, flag, newCharInfo); + return _database.UpdateLatest(_metadata, oldCharInfo, flag, out newCharInfo); } protected override ResultCode BuildRandom(Age age, Gender gender, Race race, out CharInfo charInfo) @@ -113,14 +111,14 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService protected override ResultCode UpdateLatest1(StoreData oldStoreData, SourceFlag flag, out StoreData newStoreData) { - newStoreData = default; - if (!_isSystem) { + newStoreData = default; + return ResultCode.PermissionDenied; } - return _database.UpdateLatest(_metadata, oldStoreData, flag, newStoreData); + return _database.UpdateLatest(_metadata, oldStoreData, flag, out newStoreData); } protected override ResultCode FindIndex(CreateId createId, bool isSpecial, out int index) @@ -262,5 +260,10 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService { return _database.ConvertCharInfoToCoreData(charInfo, out coreData); } + + protected override ResultCode Append(CharInfo charInfo) + { + return _database.Append(_metadata, charInfo); + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs index 1a1c20d6e..3f9fad4fb 100644 --- a/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Mii/StaticService/IDatabaseService.cs @@ -340,6 +340,15 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService return result; } + [CommandCmif(26)] // 10.2.0+ + // Append(nn::mii::CharInfo char_info) + public ResultCode Append(ServiceCtx context) + { + CharInfo charInfo = context.RequestData.ReadStruct(); + + return Append(charInfo); + } + private Span CreateByteSpanFromBuffer(ServiceCtx context, IpcBuffDesc ipcBuff, bool isOutput) { byte[] rawData; @@ -421,5 +430,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService protected abstract ResultCode ConvertCoreDataToCharInfo(CoreData coreData, out CharInfo charInfo); protected abstract ResultCode ConvertCharInfoToCoreData(CharInfo charInfo, out CoreData coreData); + + protected abstract ResultCode Append(CharInfo charInfo); } } diff --git a/src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs b/src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs new file mode 100644 index 000000000..b1a20bb93 --- /dev/null +++ b/src/Ryujinx.Tests/HLE/MiiDatabaseTests.cs @@ -0,0 +1,122 @@ +using System.Reflection; + +using NUnit.Framework; + +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Services.Mii; +using Ryujinx.HLE.HOS.Services.Mii.StaticService; +using Ryujinx.HLE.HOS.Services.Mii.Types; + +namespace Ryujinx.Tests.HLE +{ + public class MiiDatabaseTests + { + [Test] + public void UpdateLatestReturnsStoredCharInfo() + { + DatabaseImpl database = new(); + StoreData storedData = StoreData.BuildDefault(new UtilityImpl(new TickSource(19200000)), 0); + MiiDatabaseManager databaseManager = GetDatabaseManager(database); + + NintendoFigurineDatabase figurineDatabase = new(); + figurineDatabase.Format(); + figurineDatabase.Add(storedData); + SetFigurineDatabase(databaseManager, figurineDatabase); + + TestDatabaseService service = new(database); + + CharInfo oldCharInfo = new(); + oldCharInfo.SetFromStoreData(storedData); + oldCharInfo.Height--; + + ResultCode result = service.UpdateLatestForTest(oldCharInfo, SourceFlag.Database, out CharInfo newCharInfo); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(newCharInfo.CreateId, Is.EqualTo(oldCharInfo.CreateId)); + Assert.That(newCharInfo.Height, Is.EqualTo(storedData.CoreData.Height)); + Assert.That(newCharInfo.IsValid(), Is.True); + }); + + } + + [Test] + public void AppendAddsRegularCharInfoToDatabase() + { + DatabaseImpl database = new(); + UtilityImpl utilityImpl = new(new TickSource(19200000)); + SetUtilityImpl(database, utilityImpl); + MiiDatabaseManager databaseManager = GetDatabaseManager(database); + SetFigurineDatabase(databaseManager, CreateFormattedDatabase()); + + StoreData defaultStoreData = StoreData.BuildDefault(utilityImpl, 0); + Assert.Multiple(() => + { + Assert.That(defaultStoreData.CoreData.IsValid(), Is.True); + Assert.That(defaultStoreData.IsValidDataCrc(), Is.True); + Assert.That(defaultStoreData.IsValidDeviceCrc(), Is.True); + Assert.That(defaultStoreData.IsValid(), Is.True); + }); + + CharInfo charInfo = new(); + charInfo.SetFromStoreData(defaultStoreData); + + DatabaseSessionMetadata metadata = database.CreateSessionMetadata(new SpecialMiiKeyCode()); + + ResultCode result = databaseManager.Append(metadata, utilityImpl, charInfo); + + int count = databaseManager.GetCount(metadata); + databaseManager.Get(metadata, 0, out StoreData storedData); + + CoreData expectedCoreData = new(); + expectedCoreData.SetFromCharInfo(charInfo); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(count, Is.EqualTo(1)); + Assert.That(storedData.IsValid(), Is.True); + Assert.That(storedData.CreateId, Is.Not.EqualTo(charInfo.CreateId)); + Assert.That(storedData.CoreData, Is.EqualTo(expectedCoreData)); + }); + } + + private sealed class TestDatabaseService(DatabaseImpl database) : DatabaseServiceImpl(database, true, new SpecialMiiKeyCode()) + { + public ResultCode UpdateLatestForTest(CharInfo oldCharInfo, SourceFlag flag, out CharInfo newCharInfo) + { + return UpdateLatest(oldCharInfo, flag, out newCharInfo); + } + } + + private static MiiDatabaseManager GetDatabaseManager(DatabaseImpl database) + { + return (MiiDatabaseManager)typeof(DatabaseImpl) + .GetField("_miiDatabase", BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(database); + } + + private static void SetFigurineDatabase(MiiDatabaseManager databaseManager, NintendoFigurineDatabase figurineDatabase) + { + typeof(MiiDatabaseManager) + .GetField("_database", BindingFlags.Instance | BindingFlags.NonPublic) + .SetValue(databaseManager, figurineDatabase); + } + + private static NintendoFigurineDatabase CreateFormattedDatabase() + { + NintendoFigurineDatabase figurineDatabase = new(); + figurineDatabase.Format(); + + return figurineDatabase; + } + + private static void SetUtilityImpl(DatabaseImpl database, UtilityImpl utilityImpl) + { + typeof(DatabaseImpl) + .GetField("_utilityImpl", BindingFlags.Instance | BindingFlags.NonPublic) + .SetValue(database, utilityImpl); + } + } +} From bab160d650050efc3b7d703874b587f27fc0fd7b Mon Sep 17 00:00:00 2001 From: Babib3l Date: Sun, 10 May 2026 23:47:39 +0000 Subject: [PATCH 42/61] Fix Windows fullscreen gap when toggling from maximized (#80) This PR aims to Fix the Windows fullscreen gap when toggling from maximized state by using canonical Win32 fullscreen approach. When entering fullscreen, the window style is saved and replaced with WS_POPUP | WS_VISIBLE to remove all window chrome (title bar, borders), then SetWindowPos sizes the window to cover the full screen with SWP_FRAMECHANGED to force Win32 to recalculate the frame. On exit, the original window style is restored. This eliminates the few-pixel gap at the top that occurred because Avalonia's WindowState = FullScreen transition from a maximized state retained the title bar non-client area, leaving visible space at the top of the screen (Fullscreen resolution would be 1920 x 1072p on a 1080p monitor, it now correctly renders at 1920 x 1080p) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/80 --- src/Ryujinx/UI/Helpers/Win32NativeInterop.cs | 26 +++++++++ .../UI/ViewModels/MainWindowViewModel.cs | 58 ++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs index 0ff7f5fde..adfa899ed 100644 --- a/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs +++ b/src/Ryujinx/UI/Helpers/Win32NativeInterop.cs @@ -8,6 +8,12 @@ namespace Ryujinx.Ava.UI.Helpers internal partial class Win32NativeInterop { internal const int GWLP_WNDPROC = -4; + internal const int GWL_STYLE = -16; + internal const int GWL_EXSTYLE = -20; + + internal const uint WS_OVERLAPPEDWINDOW = 0x00CF0000; + internal const uint WS_POPUP = 0x80000000; + internal const uint WS_VISIBLE = 0x10000000; [Flags] public enum ClassStyles : uint @@ -107,9 +113,29 @@ namespace Ryujinx.Ava.UI.Helpers nint hInstance, nint lpParam); + [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetWindowLongPtrW")] + public static partial nint GetWindowLongPtrW(nint hWnd, int nIndex); + [LibraryImport("user32.dll", SetLastError = true)] public static partial nint SetWindowLongPtrW(nint hWnd, int nIndex, nint value); + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool SetWindowPos( + nint hWnd, + nint hWndInsertAfter, + int x, + int y, + int cx, + int cy, + uint uFlags); + + internal const uint SWP_NOZORDER = 0x0004; + internal const uint SWP_NOACTIVATE = 0x0010; + internal const uint SWP_FRAMECHANGED = 0x0020; + internal const uint SWP_NOMOVE = 0x0002; + internal const uint SWP_NOSIZE = 0x0001; + [LibraryImport("user32.dll", SetLastError = true)] public static partial ushort GetAsyncKeyState(int nVirtKey); diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 57fd825b3..e488495d6 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -5,6 +5,7 @@ using Avalonia.Input; using Avalonia.Media; using Avalonia.Platform.Storage; using Avalonia.Threading; +using System.Runtime.Versioning; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DynamicData; @@ -2014,7 +2015,7 @@ namespace Ryujinx.Ava.UI.ViewModels LastFullscreenToggle = Environment.TickCount64; - if (WindowState is not WindowState.Normal) + if (WindowState is WindowState.FullScreen) { WindowState = WindowState.Normal; Window.TitleBar.ExtendsContentIntoTitleBar = !ConfigurationState.Instance.ShowOldUI; @@ -2023,21 +2024,74 @@ namespace Ryujinx.Ava.UI.ViewModels { ShowMenuAndStatusBar = true; } + + if (OperatingSystem.IsWindows()) + { + RestoreWindowFromFullscreen(); + } } else { - WindowState = WindowState.FullScreen; Window.TitleBar.ExtendsContentIntoTitleBar = true; if (IsGameRunning) { ShowMenuAndStatusBar = false; } + + if (OperatingSystem.IsWindows()) + { + MakeWindowFullscreen(); + } + else + { + WindowState = WindowState.FullScreen; + } } IsFullScreen = WindowState is WindowState.FullScreen; } + private nint _savedWindowStyle; + + [SupportedOSPlatform("windows")] + private void MakeWindowFullscreen() + { + nint hwnd = Window.TryGetPlatformHandle()?.Handle ?? nint.Zero; + if (hwnd == nint.Zero) return; + + // Save current style and placement + _savedWindowStyle = Win32NativeInterop.GetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE); + + // Remove window chrome: WS_OVERLAPPEDWINDOW -> WS_POPUP | WS_VISIBLE + Win32NativeInterop.SetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE, + unchecked((nint)(Win32NativeInterop.WS_POPUP | Win32NativeInterop.WS_VISIBLE))); + + Avalonia.Platform.Screen? screen = Window.Screens.ScreenFromVisual(Window); + int w = screen?.Bounds.Width ?? 0; + int h = screen?.Bounds.Height ?? 0; + + Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, w, h, + Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE | Win32NativeInterop.SWP_FRAMECHANGED); + + WindowState = WindowState.FullScreen; + } + + [SupportedOSPlatform("windows")] + private void RestoreWindowFromFullscreen() + { + nint hwnd = Window.TryGetPlatformHandle()?.Handle ?? nint.Zero; + if (hwnd == nint.Zero) return; + + // Restore original window style + Win32NativeInterop.SetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE, _savedWindowStyle); + + Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, 0, 0, + Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE | + Win32NativeInterop.SWP_FRAMECHANGED | Win32NativeInterop.SWP_NOMOVE | Win32NativeInterop.SWP_NOSIZE); + + } + public static void SaveConfig() { ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); From f8167eb6252438c63f82b2bdb04f1215dc47071b Mon Sep 17 00:00:00 2001 From: yell0wsuit Date: Sun, 10 May 2026 23:57:05 +0000 Subject: [PATCH 43/61] [HLE] Match hardware screenshot buffer size behavior for captures (#44) ## Description ~~Fixes a fatal CLR crash when `caps` screenshot saving receives an input buffer larger than `0x384000`.~~ ~~Resolves a crash in Tomodachi Life: Living the Dream where saving the pictures to the system's album crashes with 0x80131506.~~ Follow up to #18. This PR adjusts the validation and copy behavior to better match real hardware, and adds logging to make invalid screenshot buffer cases easier to diagnose. Real hardware accepts screenshot buffers with a size greater than or equal to `0x384000`, but only `0x384000` bytes are needed for the 1280x720 RGBA image. This changes screenshot saving to: - reject buffers smaller than `0x384000` - accept buffers equal to or larger than `0x384000` - copy only the first `0x384000` bytes into the 1280x720 bitmap ## Testing Tested with a real Switch NRO using `capssuSaveScreenShotEx0`, `capssuSaveScreenShotEx1`, and `capssuSaveScreenShotEx2`. Observed hardware behavior: ```text 0x384000 => OK 0x384000 - 1 => NullInputBuffer 0x384000 + 1 => OK 0x3C0000 => OK // Tomo life picture size ``` Co-authored-by: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com> Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/44 --- .../HOS/Services/Caps/CaptureManager.cs | 158 ++++++++------- .../Caps/IScreenShotApplicationService.cs | 54 ++++- src/Ryujinx.Tests/HLE/CaptureManagerTests.cs | 187 ++++++++++++++++++ 3 files changed, 326 insertions(+), 73 deletions(-) create mode 100644 src/Ryujinx.Tests/HLE/CaptureManagerTests.cs diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs b/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs index f47c663ed..651f92986 100644 --- a/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs @@ -1,7 +1,9 @@ +using Ryujinx.Common.Logging; using Ryujinx.Common.Memory; using Ryujinx.HLE.HOS.Services.Caps.Types; using SkiaSharp; using System; +using System.Globalization; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -9,16 +11,20 @@ using System.Security.Cryptography; namespace Ryujinx.HLE.HOS.Services.Caps { - class CaptureManager + internal class CaptureManager { - private readonly string _sdCardPath; + public CaptureManager(Switch device) + { + _ = device; + } + private readonly string _sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath(); private uint _shimLibraryVersion; - public CaptureManager(Switch device) - { - _sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath(); - } + private const int ScreenshotWidth = 1280; + private const int ScreenshotHeight = 720; + private const int ScreenshotBytesPerPixel = 4; + private const int ScreenshotDataSize = ScreenshotWidth * ScreenshotHeight * ScreenshotBytesPerPixel; // 0x384000 public ResultCode SetShimLibraryVersion(ServiceCtx context) { @@ -53,84 +59,94 @@ namespace Ryujinx.HLE.HOS.Services.Caps return resultCode; } - public ResultCode SaveScreenShot(byte[] screenshotData, ulong appletResourceUserId, ulong titleId, out ApplicationAlbumEntry applicationAlbumEntry) + public ResultCode SaveScreenShot( + byte[] screenshotData, + ulong appletResourceUserId, + ulong titleId, + out ApplicationAlbumEntry applicationAlbumEntry) { + Logger.Stub?.PrintStub(LogClass.ServiceCaps, new + { + appletResourceUserId, + titleId, + screenshotDataLength = screenshotData?.Length ?? 0, + }); + applicationAlbumEntry = default; - if (screenshotData.Length == 0) + if (screenshotData == null || screenshotData.Length == 0) { return ResultCode.NullInputBuffer; } - /* - // NOTE: On our current implementation, appletResourceUserId starts at 0, disable it for now. - if (appletResourceUserId == 0) + if (screenshotData.Length < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotData.Length:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + + DateTime currentDateTime = DateTime.Now; + + applicationAlbumEntry = new ApplicationAlbumEntry() + { + Size = (ulong)Unsafe.SizeOf(), + TitleId = titleId, + AlbumFileDateTime = new AlbumFileDateTime() + { + Year = (ushort)currentDateTime.Year, + Month = (byte)currentDateTime.Month, + Day = (byte)currentDateTime.Day, + Hour = (byte)currentDateTime.Hour, + Minute = (byte)currentDateTime.Minute, + Second = (byte)currentDateTime.Second, + UniqueId = 0, + }, + AlbumStorage = AlbumStorage.Sd, + ContentType = ContentType.Screenshot, + Padding = new Array5(), + Unknown0x1f = 1, + }; + + // NOTE: The hex hash is a HMAC-SHA256 (first 32 bytes) using a hardcoded secret key over the titleId, we can simulate it by hashing the titleId instead. + string hash = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId)))[..0x20]; + + string folderPath = Path.Combine( + _sdCardPath, + "Nintendo", + "Album", + currentDateTime.Year.ToString("0000", CultureInfo.InvariantCulture), + currentDateTime.Month.ToString("00", CultureInfo.InvariantCulture), + currentDateTime.Day.ToString("00", CultureInfo.InvariantCulture)); + + string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); + + _ = Directory.CreateDirectory(folderPath); + + while (File.Exists(filePath)) + { + applicationAlbumEntry.AlbumFileDateTime.UniqueId++; + filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); + } + + using SKBitmap bitmap = new(new SKImageInfo(ScreenshotWidth, ScreenshotHeight, SKColorType.Rgba8888)); + + IntPtr pixels = bitmap.GetPixels(); + + if (pixels == IntPtr.Zero) { return ResultCode.InvalidArgument; } - */ - /* - // Doesn't occur in our case. - if (applicationAlbumEntry == null) - { - return ResultCode.NullOutputBuffer; - } - */ + Marshal.Copy(screenshotData, 0, pixels, ScreenshotDataSize); - if (screenshotData.Length >= 0x384000) - { - DateTime currentDateTime = DateTime.Now; + using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80); + using FileStream file = File.OpenWrite(filePath); + data.SaveTo(file); - applicationAlbumEntry = new ApplicationAlbumEntry() - { - Size = (ulong)Unsafe.SizeOf(), - TitleId = titleId, - AlbumFileDateTime = new AlbumFileDateTime() - { - Year = (ushort)currentDateTime.Year, - Month = (byte)currentDateTime.Month, - Day = (byte)currentDateTime.Day, - Hour = (byte)currentDateTime.Hour, - Minute = (byte)currentDateTime.Minute, - Second = (byte)currentDateTime.Second, - UniqueId = 0, - }, - AlbumStorage = AlbumStorage.Sd, - ContentType = ContentType.Screenshot, - Padding = new Array5(), - Unknown0x1f = 1, - }; - - // NOTE: The hex hash is a HMAC-SHA256 (first 32 bytes) using a hardcoded secret key over the titleId, we can simulate it by hashing the titleId instead. - string hash = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId)))[..0x20]; - string folderPath = Path.Combine(_sdCardPath, "Nintendo", "Album", currentDateTime.Year.ToString("00"), currentDateTime.Month.ToString("00"), currentDateTime.Day.ToString("00")); - string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); - - // TODO: Handle that using the FS service implementation and return the right error code instead of throwing exceptions. - Directory.CreateDirectory(folderPath); - - while (File.Exists(filePath)) - { - applicationAlbumEntry.AlbumFileDateTime.UniqueId++; - - filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash); - } - - // NOTE: The saved JPEG file doesn't have the limitation in the extra EXIF data. - using SKBitmap bitmap = new(new SKImageInfo(1280, 720, SKColorType.Rgba8888, SKAlphaType.Premul)); - int dataLen = screenshotData.Length > bitmap.ByteCount ? bitmap.ByteCount : screenshotData.Length; - - Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), dataLen); - - using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80); - using FileStream file = File.OpenWrite(filePath); - data.SaveTo(file); - - return ResultCode.Success; - } - - return ResultCode.NullInputBuffer; + return ResultCode.Success; } private string GenerateFilePath(string folderPath, ApplicationAlbumEntry applicationAlbumEntry, DateTime currentDateTime, string hash) diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs b/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs index 0723b57cc..2ccb7c598 100644 --- a/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Caps/IScreenShotApplicationService.cs @@ -1,13 +1,19 @@ using Ryujinx.Common; +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Caps.Types; namespace Ryujinx.HLE.HOS.Services.Caps { [Service("caps:su")] // 6.0.0+ - class IScreenShotApplicationService : IpcService + internal class IScreenShotApplicationService : IpcService { - public IScreenShotApplicationService(ServiceCtx context) { } + private const ulong ScreenshotDataSize = 0x384000; + private const ulong ApplicationDataSize = 0x404; + public IScreenShotApplicationService(ServiceCtx context) + { + _ = context; + } [CommandCmif(32)] // 7.0.0+ // SetShimLibraryVersion(pid, u64, nn::applet::AppletResourceUserId) public ResultCode SetShimLibraryVersion(ServiceCtx context) @@ -33,6 +39,15 @@ namespace Ryujinx.HLE.HOS.Services.Caps ulong screenshotDataPosition = context.Request.SendBuff[0].Position; ulong screenshotDataSize = context.Request.SendBuff[0].Size; + if (screenshotDataSize < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray(); ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry); @@ -60,6 +75,24 @@ namespace Ryujinx.HLE.HOS.Services.Caps ulong screenshotDataPosition = context.Request.SendBuff[1].Position; ulong screenshotDataSize = context.Request.SendBuff[1].Size; + if (applicationDataSize != ApplicationDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid ApplicationData size 0x{applicationDataSize:X}; expected 0x{ApplicationDataSize:X}."); + + return ResultCode.InvalidArgument; + } + + if (screenshotDataSize < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + // TODO: Parse the application data: At 0x00 it's UserData (Size of 0x400), at 0x404 it's a uint UserDataSize (Always empty for now). _ = context.Memory.GetSpan(applicationDataPosition, (int)applicationDataSize).ToArray(); @@ -88,6 +121,23 @@ namespace Ryujinx.HLE.HOS.Services.Caps ulong screenshotDataPosition = context.Request.SendBuff[1].Position; ulong screenshotDataSize = context.Request.SendBuff[1].Size; + if (userIdListSize != 0x88) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid UserIdList size 0x{userIdListSize:X}; expected 0x88."); + return ResultCode.InvalidArgument; + } + + if (screenshotDataSize < ScreenshotDataSize) + { + Logger.Warning?.PrintMsg( + LogClass.ServiceCaps, + $"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}."); + + return ResultCode.NullInputBuffer; + } + // TODO: Parse the UserIdList. _ = context.Memory.GetSpan(userIdListPosition, (int)userIdListSize).ToArray(); diff --git a/src/Ryujinx.Tests/HLE/CaptureManagerTests.cs b/src/Ryujinx.Tests/HLE/CaptureManagerTests.cs new file mode 100644 index 000000000..8dd2b070f --- /dev/null +++ b/src/Ryujinx.Tests/HLE/CaptureManagerTests.cs @@ -0,0 +1,187 @@ +using NUnit.Framework; +using Ryujinx.HLE.HOS.Services.Caps; +using Ryujinx.HLE.HOS.Services.Caps.Types; +using SkiaSharp; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Tests.HLE +{ + public class CaptureManagerTests + { + private const int ScreenshotWidth = 1280; + private const int ScreenshotHeight = 720; + private const int BytesPerPixel = 4; + + private const int ScreenshotDataSize = ScreenshotWidth * ScreenshotHeight * BytesPerPixel; // 0x384000 + private const int PaddedScreenshotDataSize = ScreenshotWidth * 768 * BytesPerPixel; // 0x3C0000 + + [Test] + public void SaveScreenShotRejectsBufferSmallerThan720p() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = new byte[ScreenshotDataSize - 1]; + + ResultCode result = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out _); + + Assert.That(result, Is.EqualTo(ResultCode.NullInputBuffer)); + Assert.That(Directory.Exists(Path.Combine(tempSdCard.Path, "Nintendo", "Album")), Is.False); + } + + [Test] + public void SaveScreenShotAcceptsExact720pBuffer() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = CreateTestPattern(ScreenshotDataSize); + + ResultCode result = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out ApplicationAlbumEntry applicationAlbumEntry); + + string filePath = GetSingleAlbumFile(tempSdCard.Path); + + using SKBitmap bitmap = SKBitmap.Decode(filePath); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(bitmap.Width, Is.EqualTo(ScreenshotWidth)); + Assert.That(bitmap.Height, Is.EqualTo(ScreenshotHeight)); + Assert.That(applicationAlbumEntry.TitleId, Is.EqualTo(0x0100000000001000)); + Assert.That(applicationAlbumEntry.AlbumStorage, Is.EqualTo(AlbumStorage.Sd)); + Assert.That(applicationAlbumEntry.ContentType, Is.EqualTo(ContentType.Screenshot)); + Assert.That(applicationAlbumEntry.Unknown0x1f, Is.EqualTo(1)); + }); + } + + [Test] + public void SaveScreenShotAcceptsBufferLargerThan720p() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = CreateTestPattern(PaddedScreenshotDataSize); + + ResultCode result = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out ApplicationAlbumEntry applicationAlbumEntry); + + string filePath = GetSingleAlbumFile(tempSdCard.Path); + + using SKBitmap bitmap = SKBitmap.Decode(filePath); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo(ResultCode.Success)); + Assert.That(bitmap.Width, Is.EqualTo(ScreenshotWidth)); + Assert.That(bitmap.Height, Is.EqualTo(ScreenshotHeight)); + Assert.That(applicationAlbumEntry.TitleId, Is.EqualTo(0x0100000000001000)); + }); + } + + [Test] + public void SaveScreenShotCreatesUniqueFileNamesForRepeatedSaves() + { + using TempSdCard tempSdCard = new(); + + CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path); + byte[] screenshotData = CreateTestPattern(ScreenshotDataSize); + + ResultCode firstResult = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out _); + + ResultCode secondResult = captureManager.SaveScreenShot( + screenshotData, + appletResourceUserId: 0, + titleId: 0x0100000000001000, + out _); + + string[] files = Directory.GetFiles( + Path.Combine(tempSdCard.Path, "Nintendo", "Album"), + "*.jpg", + SearchOption.AllDirectories); + + Assert.Multiple(() => + { + Assert.That(firstResult, Is.EqualTo(ResultCode.Success)); + Assert.That(secondResult, Is.EqualTo(ResultCode.Success)); + Assert.That(files, Has.Length.EqualTo(2)); + }); + } + + private static CaptureManager CreateCaptureManager(string sdCardPath) + { + CaptureManager captureManager = (CaptureManager)RuntimeHelpers.GetUninitializedObject(typeof(CaptureManager)); + + typeof(CaptureManager) + .GetField("_sdCardPath", BindingFlags.Instance | BindingFlags.NonPublic) + .SetValue(captureManager, sdCardPath); + + return captureManager; + } + + private static string GetSingleAlbumFile(string sdCardPath) + { + string albumPath = Path.Combine(sdCardPath, "Nintendo", "Album"); + + string[] files = Directory.GetFiles(albumPath, "*.jpg", SearchOption.AllDirectories); + + Assert.That(files, Has.Length.EqualTo(1)); + + return files.Single(); + } + + private static byte[] CreateTestPattern(int size) + { + byte[] data = new byte[size]; + + int pixelCount = size / BytesPerPixel; + + for (int i = 0; i < pixelCount; i++) + { + int x = i % ScreenshotWidth; + int y = i / ScreenshotWidth; + + data[(i * 4) + 0] = (byte)(x & 0xff); + data[(i * 4) + 1] = (byte)(y & 0xff); + data[(i * 4) + 2] = 0x80; + data[(i * 4) + 3] = 0xff; + } + + return data; + } + + private sealed class TempSdCard : IDisposable + { + public string Path { get; } = System.IO.Path.Combine( + TestContext.CurrentContext.WorkDirectory, + "sdcard-" + Guid.NewGuid()); + + public void Dispose() + { + if (Directory.Exists(Path)) + { + Directory.Delete(Path, recursive: true); + } + } + } + } +} From 89ea41ef84aa9d88261274b453c6ddbe9aa019cc Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 12 May 2026 02:38:09 +0000 Subject: [PATCH 44/61] Check if the Device is rendering before waiting on it (#86) Fixes an issue where there were missed references and an ``OperationCancelled`` exception when exiting an application. Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/86 --- src/Ryujinx.Graphics.Vulkan/Auto.cs | 4 +- src/Ryujinx/Systems/AppHost.cs | 75 +++++++++++++++-------------- 2 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/Auto.cs b/src/Ryujinx.Graphics.Vulkan/Auto.cs index 9c8c7ff4b..d97d69ad3 100644 --- a/src/Ryujinx.Graphics.Vulkan/Auto.cs +++ b/src/Ryujinx.Graphics.Vulkan/Auto.cs @@ -176,9 +176,7 @@ namespace Ryujinx.Graphics.Vulkan } } } - - // This can somehow become -1. - // Logger.Info?.PrintMsg(LogClass.Gpu, $"_referenceCount: {_referenceCount}"); + Debug.Assert(_referenceCount >= 0); } diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 6675972be..358b585f4 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -622,15 +622,15 @@ namespace Ryujinx.Ava.Systems // If the GPU has no work and is cancelled, we need to handle that as well. WaitHandle.WaitAny(new[] { _gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle }); - _gpuCancellationTokenSource.Dispose(); - - // Waiting for work to be finished before we dispose. + if (_renderingStarted) { + // Waiting for work to be finished before we dispose. Device.Gpu.WaitUntilGpuReady(); } _gpuDoneEvent.Dispose(); + _gpuCancellationTokenSource.Dispose(); DisposeGpu(); AppExit?.Invoke(this, EventArgs.Empty); @@ -1095,51 +1095,56 @@ namespace Ryujinx.Ava.Systems Device.Gpu.Renderer.RunLoop(() => { - Device.Gpu.SetGpuThread(); - Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); - - _renderer.Window.ChangeVSyncMode(Device.VSyncMode); - - while (_isActive) + try { - _ticks += _chrono.ElapsedTicks; + Device.Gpu.SetGpuThread(); + Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); - _chrono.Restart(); + _renderer.Window.ChangeVSyncMode(Device.VSyncMode); - if (Device.WaitFifo()) + while (_isActive) { - Device.Statistics.RecordFifoStart(); - Device.ProcessFrame(); - Device.Statistics.RecordFifoEnd(); - } + _ticks += _chrono.ElapsedTicks; - while (Device.ConsumeFrameAvailable()) - { - if (!_renderingStarted) + _chrono.Restart(); + + if (Device.WaitFifo()) { - _renderingStarted = true; - _viewModel.SwitchToRenderer(false); - InitStatus(); + Device.Statistics.RecordFifoStart(); + Device.ProcessFrame(); + Device.Statistics.RecordFifoEnd(); } - Device.PresentFrame(() => (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers()); - } + while (Device.ConsumeFrameAvailable()) + { + if (!_renderingStarted) + { + _renderingStarted = true; + _viewModel.SwitchToRenderer(false); + InitStatus(); + } - if (_ticks >= _ticksPerFrame) - { - UpdateStatus(); + Device.PresentFrame(() => + (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers()); + } + + if (_ticks >= _ticksPerFrame) + { + UpdateStatus(); + } } } - - // Make sure all commands in the run loop are fully executed before leaving the loop. - if (Device.Gpu.Renderer is ThreadedRenderer threaded) + finally { - Logger.Info?.PrintMsg(LogClass.Gpu, "Flushing threaded commands..."); - threaded.FlushThreadedCommands(); - Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!"); + // Make sure all commands in the run loop are fully executed before leaving the loop. + if (Device.Gpu.Renderer is ThreadedRenderer threaded) + { + Logger.Info?.PrintMsg(LogClass.Gpu, "Flushing threaded commands..."); + threaded.FlushThreadedCommands(); + Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!"); + } + _gpuDoneEvent.Set(); } - - _gpuDoneEvent.Set(); }); (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(true); From 49891ba7af2f1c9469347ae7156def073e5fea6b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 12 May 2026 02:41:10 +0000 Subject: [PATCH 45/61] Update avalonia monorepo to 11.3.15 (#85) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [Avalonia](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia/11.3.14/11.3.15?slim=true) | | [Avalonia.Desktop](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Desktop/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Desktop/11.3.14/11.3.15?slim=true) | | [Avalonia.Diagnostics](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Diagnostics/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Diagnostics/11.3.14/11.3.15?slim=true) | | [Avalonia.Markup.Xaml.Loader](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Markup.Xaml.Loader/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Markup.Xaml.Loader/11.3.14/11.3.15?slim=true) | --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/85 --- Directory.Packages.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4466f2777..3d39156b5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,11 +3,11 @@ true - + - - - + + + From e7aa6775af2fefc3a5ff1e9b974b5956edf5ddb0 Mon Sep 17 00:00:00 2001 From: cookieso Date: Wed, 13 May 2026 23:57:30 +0000 Subject: [PATCH 46/61] Input: Implement HD Rumble for compatible Nin devices (#40) This PR addresses [this issue](https://github.com/Ryubing/Issues/issues/231) and implements HD Rumble for compatible Nin devices. ## New Features - Add the option for Gamepads to implement HD Rumble - Add the HD rumble capability to SDL3Gamepad and SDL3JoyCon when they meet certain requirements Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/40 --- src/Ryujinx.Input.SDL3/NpadHdRumble.cs | 151 ++++++++++++++++++++++++ src/Ryujinx.Input.SDL3/SDL3Gamepad.cs | 13 ++ src/Ryujinx.Input.SDL3/SDL3JoyCon.cs | 13 ++ src/Ryujinx.Input/HLE/NpadController.cs | 17 ++- src/Ryujinx.Input/IGamepad.cs | 11 ++ 5 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 src/Ryujinx.Input.SDL3/NpadHdRumble.cs diff --git a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs new file mode 100644 index 000000000..e367f6a9c --- /dev/null +++ b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs @@ -0,0 +1,151 @@ +using Ryujinx.HLE.HOS.Services.Hid; +using SDL; +using static SDL.SDL3; +using System; + +namespace Ryujinx.Input.SDL3 +{ + /// + /// Manages a HID handle of a gamepad to encode and write HD rumble commands for Nin controllers. + /// + public unsafe class NpadHdRumble : IDisposable + { + private readonly SDL_hid_device* _hidHandle; + + private int _globalCount; + + private NpadHdRumble(SDL_hid_device* hidHandle) + { + _hidHandle = hidHandle; + } + + public static NpadHdRumble Create(SDL_Gamepad* gamepadHandle) + { + ushort vendor = SDL_GetGamepadVendor(gamepadHandle); + if (vendor != 0x057e) + { + return null; + } + + ushort product = SDL_GetGamepadProduct(gamepadHandle); + if (product != 0x2006 && product != 0x2007 && product != 0x2009 && product != 0x200e) + { + return null; + } + + return new NpadHdRumble(SDL_hid_open(vendor, product, 0)); + } + + // Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble + private void WriteHdRumble( + int encLeftLowFreq, int encLeftLowAmp, + int encLeftHighFreq, int encLeftHighAmp, + int encRightLowFreq, int encRightLowAmp, + int encRightHighFreq, int encRightHighAmp) + { + byte[] buf = new byte[10]; + + buf[0] = 0x10; + buf[1] = (byte)((++_globalCount) & 0xF); + + buf[2] = (byte)(encLeftHighFreq & 0xFF); + buf[3] = (byte)(encLeftHighAmp + ((encLeftHighFreq >> 8) & 0xFF)); + buf[4] = (byte)(encLeftLowFreq + ((encLeftLowAmp >> 8) & 0xFF)); + buf[5] = (byte)(encLeftLowAmp & 0xFF); + + buf[6] = (byte)(encRightHighFreq & 0xFF); + buf[7] = (byte)(encRightHighAmp + ((encRightHighFreq >> 8) & 0xFF)); + buf[8] = (byte)(encRightLowFreq + ((encRightLowAmp >> 8) & 0xFF)); + buf[9] = (byte)(encRightLowAmp & 0xFF); + + if (_globalCount > 0xF) + { + _globalCount = 0x0; + } + + fixed (byte* ptr = buf) + { + SDL_hid_write(_hidHandle, ptr, (nuint)buf.Length); + } + } + + + private static int EncodeLowFreq(float lowFreq) + { + float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f); + return (int)Math.Round(32 * Math.Log2(lf * 0.1f)) - 0x40; + } + + private static int EncodeHighFreq(float highFreq) + { + float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f); + return ((int)Math.Round(32 * Math.Log2(hf * 0.1f)) - 0x60) * 4; + } + + private static int EncodeLowAmp(float rawAmp) + { + int encodedAmp = 0; + + if (rawAmp is > 0 and < 0.012f) + { + encodedAmp = 1; + } + else if (rawAmp is >= 0.012f and < 0.112f) + { + encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f)); + } + else if (rawAmp is >= 0.112f and < 0.225f) + { + encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f)); + } + else if (rawAmp is >= 0.225f and <= 1f) + { + encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f)); + } + + return (int)Math.Floor(encodedAmp / 2.0) + 64; + } + + private static int EncodeHighAmp(float rawAmp) + { + int encodedAmp = 0; + + if (rawAmp is > 0 and < 0.012f) + { + encodedAmp = 1; + } + else if (rawAmp is >= 0.012f and < 0.112f) + { + encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f)); + } + else if (rawAmp is >= 0.112f and < 0.225f) + { + encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f)); + } + else if (rawAmp is >= 0.225f and <= 1f) + { + encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f)); + } + + return encodedAmp * 2; + } + + public bool HdRumble(VibrationValue left, VibrationValue right) + { + WriteHdRumble(EncodeLowFreq(left.FrequencyLow), + EncodeLowAmp(left.AmplitudeLow), + EncodeHighFreq(left.FrequencyHigh), + EncodeHighAmp(left.AmplitudeHigh), + EncodeLowFreq(right.FrequencyLow), + EncodeLowAmp(right.AmplitudeLow), + EncodeHighFreq(right.FrequencyHigh), + EncodeHighAmp(right.AmplitudeHigh)); + return true; + } + + public void Dispose() + { + SDL_hid_close(_hidHandle); + } + } +} diff --git a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs index 4985d8eea..c23b64304 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs @@ -2,6 +2,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Collections.Generic; using System.Numerics; @@ -76,11 +77,14 @@ namespace Ryujinx.Input.SDL3 private SDL_Gamepad* _gamepadHandle; + private NpadHdRumble _hdRumble; + private float _triggerThreshold; public SDL3Gamepad(SDL_Gamepad* gamepadHandle, string driverId) { _gamepadHandle = gamepadHandle; + _hdRumble = NpadHdRumble.Create(gamepadHandle); _buttonsUserMapping = new List(20); Name = SDL_GetGamepadName(_gamepadHandle); @@ -165,6 +169,10 @@ namespace Ryujinx.Input.SDL3 protected virtual void Dispose(bool disposing) { + if (disposing && _hdRumble != null) + { + _hdRumble.Dispose(); + } if (disposing && _gamepadHandle != null) { SDL_CloseGamepad(_gamepadHandle); @@ -184,6 +192,11 @@ namespace Ryujinx.Input.SDL3 _triggerThreshold = triggerThreshold; } + public bool HDRumble(VibrationValue left, VibrationValue right) + { + return _hdRumble?.HdRumble(left, right) ?? false; + } + public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { if ((Features & GamepadFeaturesFlag.Rumble) == 0) diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs index 5311a256c..5d779518d 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Collections.Generic; using System.Numerics; @@ -61,6 +62,8 @@ namespace Ryujinx.Input.SDL3 public GamepadFeaturesFlag Features { get; } private SDL_Gamepad* _gamepadHandle; + + private NpadHdRumble _hdRumble; private enum JoyConType { @@ -76,6 +79,7 @@ namespace Ryujinx.Input.SDL3 public SDL3JoyCon(SDL_Gamepad* gamepadHandle, string driverId) { _gamepadHandle = gamepadHandle; + _hdRumble = NpadHdRumble.Create(gamepadHandle); _buttonsUserMapping = new List(10); Name = SDL_GetGamepadName(_gamepadHandle); @@ -139,6 +143,10 @@ namespace Ryujinx.Input.SDL3 protected virtual void Dispose(bool disposing) { + if (disposing && _hdRumble != null) + { + _hdRumble.Dispose(); + } if (disposing && _gamepadHandle != null) { SDL_CloseGamepad(_gamepadHandle); @@ -156,6 +164,11 @@ namespace Ryujinx.Input.SDL3 { } + + public bool HDRumble(VibrationValue left, VibrationValue right) + { + return _hdRumble?.HdRumble(left, right) ?? false; + } public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index 29bc973f6..a46ff8daf 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -559,18 +559,29 @@ namespace Ryujinx.Input.HLE { VibrationValue leftVibrationValue = dualVibrationValue.Item1; VibrationValue rightVibrationValue = dualVibrationValue.Item2; - + float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble)); float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble)); + + leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; + leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; + rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; + rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; - _gamepad?.Rumble(low, high, uint.MaxValue); + if (_gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false) + { + _gamepad?.Rumble(low, high, uint.MaxValue); + } Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " + $"L.low.amp={leftVibrationValue.AmplitudeLow}, " + $"L.high.amp={leftVibrationValue.AmplitudeHigh}, " + + $"L.low.freq={leftVibrationValue.FrequencyLow}, " + + $"L.high.freq={leftVibrationValue.FrequencyHigh}, " + $"R.low.amp={rightVibrationValue.AmplitudeLow}, " + $"R.high.amp={rightVibrationValue.AmplitudeHigh} " + - $"--> ({low}, {high})"); + $"R.low.freq={rightVibrationValue.FrequencyLow}, " + + $"R.high.freq={rightVibrationValue.FrequencyHigh}"); } } } diff --git a/src/Ryujinx.Input/IGamepad.cs b/src/Ryujinx.Input/IGamepad.cs index 945ccfa8b..d45ac0444 100644 --- a/src/Ryujinx.Input/IGamepad.cs +++ b/src/Ryujinx.Input/IGamepad.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Numerics; using System.Runtime.CompilerServices; @@ -74,6 +75,16 @@ namespace Ryujinx.Input public void ClearLed() => SetLed(0); + /// + /// Starts an HD vibration effect on the gamepad if available. + /// + /// The vibration data for the left side + /// The vibration data for the right side + bool HDRumble(VibrationValue left, VibrationValue right) + { + return false; + } + /// /// Starts a rumble effect on the gamepad. /// From ec07e5180769403c5a3b8f9214ed56d9d07b90c1 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 14 May 2026 04:05:58 +0000 Subject: [PATCH 47/61] Update compatibility.csv (#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Emio – The Smiling Man: Famicom Detective Club (DEMO) - GROOVE COASTER WAI WAI PARTY!!!! - Metroid Prime 4: Beyond - Mute Crimson DX - Pokémon Champions - Pokémon FireRed Version - Pokémon LeafGreen Version - Tomodachi Life: Living the Dream - Tomodachi Life: Living the Dream – Welcome Edtion Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/94 --- docs/compatibility.csv | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/compatibility.csv b/docs/compatibility.csv index fa804c909..a0a7e730a 100644 --- a/docs/compatibility.csv +++ b/docs/compatibility.csv @@ -1061,6 +1061,7 @@ 0100BCA016636000,"eBaseball Powerful Pro Yakyuu 2022",gpu;services-horizon;crash,nothing,2024-05-26 23:07:19 01001F20100B8000,"Eclipse: Edge of Light",,playable,2020-08-11 23:06:29 0100E0A0110F4000,"eCrossminton",,playable,2020-07-11 18:24:27 +010054601D54C000,"Emio – The Smiling Man: Famicom Detective Club (DEMO)",demo,playable,2026-05-13 18:32:12 0100ABE00DB4E000,"Edna & Harvey: Harvey's New Eyes",nvdec,playable,2021-01-26 14:36:08 01004F000B716000,"Edna & Harvey: The Breakout – Anniversary Edition",crash;nvdec,ingame,2022-08-01 16:59:56 01002550129F0000,"Effie",,playable,2022-10-27 14:36:39 @@ -1204,7 +1205,7 @@ 01003B200E440000,"Five Nights at Freddy's: Sister Location",,playable,2023-10-06 09:00:58 010038200E088000,"Flan",crash;regression,ingame,2021-11-17 07:39:28 01000A0004C50000,"FLASHBACK™",nvdec,playable,2020-05-14 13:57:29 -0100C53004C52000,"Flat Heroes",gpu,ingame,2022-07-26 19:37:37 +0100C53004C52000,"Flat Heroes",,playable,2026-02-27 17:00:00 0100B54012798000,"Flatland: Prologue",,playable,2020-12-11 20:41:12 0100307004B4C000,"Flinthook",online,playable,2021-03-25 20:42:29 010095A004040000,"Flip Wars",services;ldn-untested,ingame,2022-05-02 15:39:18 @@ -1394,6 +1395,7 @@ 0100c3c012718000,"Grand Theft Auto: III – The Definitive Edition",gpu;UE4,ingame,2022-10-31 20:13:52 0100182014022000,"Grand Theft Auto: Vice City – The Definitive Edition",gpu;UE4,ingame,2022-10-31 20:13:52 010065a014024000,"Grand Theft Auto: San Andreas – The Definitive Edition",gpu;UE4,ingame,2022-10-31 20:13:52 +0100EB500D92E000,"GROOVE COASTER WAI WAI PARTY!!!!",gpu,ingame,2026-05-13 18:32:12 0100822012D76000,"HAAK",gpu,ingame,2023-02-19 14:31:05 01007E100EFA8000,"Habroxia",,playable,2020-06-16 23:04:42 0100535012974000,"Hades",vulkan,playable,2022-10-05 10:45:21 @@ -1832,6 +1834,7 @@ 010055200E87E000,"Metamorphosis",UE4;audout;gpu;nvdec,ingame,2021-06-16 16:18:11 0100D4900E82C000,"Metro 2033 Redux",gpu,ingame,2022-11-09 10:53:13 0100F0400E850000,"Metro: Last Light Redux",slow;nvdec;vulkan-backend-bug,ingame,2023-11-01 11:53:52 +010019A01E2F2000,"Metroid Prime 4: Beyond",,ingame,2026-05-13 18:32:12 010012101468C000,"Metroid Prime™ Remastered",gpu;Needs Update;vulkan-backend-bug;opengl-backend-bug,ingame,2024-05-07 22:48:15 010093801237C000,"Metroid™ Dread",,playable,2023-11-13 04:02:36 0100A1200F20C000,"Midnight Evil",,playable,2022-10-18 22:55:19 @@ -1945,6 +1948,7 @@ 0100C3E00ACAA000,"Mutant Football League: Dynasty Edition",online-broken,playable,2022-08-05 17:01:51 01004BE004A86000,"Mutant Mudds Collection",,playable,2022-08-05 17:11:38 0100E6B00DEA4000,"Mutant Year Zero: Road to Eden - Deluxe Edition",nvdec;UE4,playable,2022-09-10 13:31:10 +010037501F864000,"Mute Crimson DX",,ingame,2026-05-13 18:32:12 0100161009E5C000,"MX Nitro: Unleashed",,playable,2022-09-27 22:34:33 0100218011E7E000,"MX vs ATV All Out",nvdec;UE4;vulkan-backend-bug,playable,2022-10-25 19:51:46 0100D940063A0000,"MXGP3 - The Official Motocross Videogame",UE4;gpu;nvdec,ingame,2020-12-16 14:00:20 @@ -2268,6 +2272,7 @@ 010086F0064CE000,"Poi: Explorer Edition",nvdec,playable,2021-01-21 19:32:00 0100EB6012FD2000,"Poison Control",,playable,2021-05-16 14:01:54 010072400E04A000,"Pokémon Café ReMix",,playable,2021-08-17 20:00:04 +01005B7008C52800,"Pokémon Champions",Needs Update;services;online-broke,menus,2026-05-13 18:32:12 010008c01e742000,"Pokémon Friends",crash;services,menus,2025-07-24 13:32:00 01003D200BAA2000,"Pokémon Mystery Dungeon™: Rescue Team DX",mac-bug,playable,2024-01-21 00:16:32 01008DB008C2C000,"Pokémon Shield + Pokémon Shield Expansion Pass",deadlock;crash;online-broken;ldn-works;LAN,ingame,2024-08-12 07:20:22 @@ -2275,6 +2280,8 @@ 01009AD008C4C000,"Pokémon: Let's Go, Pikachu! demo",slow;demo,playable,2023-11-26 11:23:20 0100000011D90000,"Pokémon™ Brilliant Diamond",gpu;ldn-works,ingame,2024-08-28 13:26:35 010018E011D92000,"Pokémon™ Shining Pearl",gpu;ldn-works,ingame,2024-08-28 13:26:35 +100554023408000,"Pokémon FireRed Version",crashes,nothing,2026-05-13 18:32:12 +010034D02340E000,"Pokémon LeafGreen Version",crashes,nothing,2026-05-13 18:32:12 010015F008C54000,"Pokémon™ HOME",Needs Update;crash;services,menus,2020-12-06 06:01:51 01001F5010DFA000,"Pokémon™ Legends: Arceus",gpu;Needs Update;ldn-works,ingame,2024-09-19 10:02:02 0100F43008C44000,"Pokémon™ Legends: Z-A",gpu;crash;ldn-works,ingame,2025-11-16 00:30:00 @@ -2866,7 +2873,7 @@ 0100277011F1A000,"Super Mario Bros.™ 35",online-broken,menus,2022-08-07 16:27:25 010015100B514000,"Super Mario Bros.™ Wonder",amd-vendor-bug,playable,2024-09-06 13:21:21 01009B90006DC000,"Super Mario Maker™ 2",online-broken;ldn-broken,playable,2024-08-25 11:05:19 -0100000000010000,"Super Mario Odyssey™",nvdec;intel-vendor-bug;mac-bug,playable,2024-08-25 01:32:34 +0100000000010000,"Super Mario Odyssey™",nvdec;intel-vendor-bug;mac-bug;amd-vendor-bug,playable,2026-05-13 18:32:12 010036B0034E4000,"Super Mario Party™",gpu;Needs Update;ldn-works,ingame,2024-06-21 05:10:16 0100965017338000,"Super Mario Party Jamboree",mac-bug;gpu,ingame,2025-02-17 02:09:20 0100BC0018138000,"Super Mario RPG™",gpu;audio;nvdec,ingame,2024-06-19 17:43:42 @@ -3163,6 +3170,8 @@ 0100E2E00CB14000,"Tokyo School Life",,playable,2022-09-16 20:25:54 010024601BB16000,"Tomb Raider I-III Remastered Starring Lara Croft",gpu;opengl,ingame,2024-09-27 12:32:04 0100D7F01E49C000,"Tomba! Special Edition",services-horizon,nothing,2024-09-15 21:59:54 +010051F0207B2000,"Tomodachi Life: Living the Dream",amd-vendor-bug;gpu;intel-vendor-bug;ldn-broken,ingame,2026-05-13 18:32:12 +0100CA502552A000,"Tomodachi Life: Living the Dream – Welcome Edtion",amd-vendor-bug;demo,playable,2026-05-13 18:32:12 0100D400100F8000,"Tonight We Riot",,playable,2021-02-26 15:55:09 0100CC00102B4000,"Tony Hawk's™ Pro Skater™ 1 + 2",gpu;Needs Update,ingame,2024-09-24 08:18:14 010093F00E818000,"Tools Up!",crash,ingame,2020-07-21 12:58:17 From dcac29680acc2721ba3c8016a93432307a4bbaf7 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 14 May 2026 04:06:28 +0000 Subject: [PATCH 48/61] Updated PlayReports for more titles (#93) - Echoes of Wisdom (Warps) - Super Mario Odyssey (Kingdoms) - Super Mario Bros. Wonder (World & Course) - Pokemon Scarlet/Violet (DLC & Accademy Rooms) - Super Mario 3D All Stars (Game Selection) (Berry is working on track showcase) Co-authored-by: berrydiaboli Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/93 --- src/Ryujinx.Common/TitleIDs.cs | 27 +- .../PlayReport/PlayReports.Formatters.cs | 460 +++++++++++++++++- src/Ryujinx/Systems/PlayReport/PlayReports.cs | 58 ++- 3 files changed, 513 insertions(+), 32 deletions(-) diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs index c232cfd01..890cf2793 100644 --- a/src/Ryujinx.Common/TitleIDs.cs +++ b/src/Ryujinx.Common/TitleIDs.cs @@ -59,6 +59,7 @@ namespace Ryujinx.Common //Mario Franchise "010021d00812a000", // Arcade Archives VS. SUPER MARIO BROS. + "01007fe0221d8000", // Hello, Mario! "01006d0017f7a000", // Mario & Luigi: Brothership "010003000e146000", // Mario & Sonic at the Olympic Games Tokyo 2020 "010067300059a000", // Mario + Rabbids: Kingdom Battle @@ -70,6 +71,9 @@ namespace Ryujinx.Common "0100bde00862a000", // Mario Tennis Aces "0100b99019412000", // Mario vs. Donkey Kong "010049900f546000", // Super Mario 3D All-Stars + "010049900f546001", // Super Mario 3D All-Stars | Super Mario 64 + "010049900f546002", // Super Mario 3D All-Stars | Super Mario Sunshine + "010049900f546003", // Super Mario 3D All-Stars | Super Mario Galaxy "010028600ebda000", // Super Mario 3D World + Bowser's Fury "010049900F546001", // Super Mario 64 "0100ea80032ea000", // Super Mario Bros. U Deluxe @@ -107,6 +111,11 @@ namespace Ryujinx.Common "0100187003a36000", // Pokémon: Let's Go Eevee! "010003f003a34000", // Pokémon: Let's Go Pikachu! "0100f43008c44000", // Pokémon Legends: Z-A + "0100554023408000", // Pokémon FireRed Version (EN) + "01006fa0233f8000", // Pokémon FireRed Version (JP) + "0100fd6023430000", // Pokémon LeafGreen Version (DE) + "0100f1e0233fa000", // Pokémon LeafGreen Version (JP) + "01005b7008c52000", // Pokémon Champions //Splatoon Franchise "0100f8f0000a2000", // Splatoon 2 (EU) @@ -116,13 +125,14 @@ namespace Ryujinx.Common "0100ba0018500000", // Splatoon 3: Splatfest World Premiere //NSO Membership games + "0100d870045b6000", // NES - Nintendo Switch Online + "01008d300c50c000", // SNES - Nintendo Switch Online "0100c62011050000", // GB - Nintendo Switch Online "010012f017576000", // GBA - Nintendo Switch Online "0100c9a00ece6000", // N64 - Nintendo Switch Online "0100e0601c632000", // N64 - Nintendo Switch Online 18+ - "0100d870045b6000", // NES - Nintendo Switch Online "0100b3c014bda000", // SEGA Genesis - Nintendo Switch Online - "01008d300c50c000", // SNES - Nintendo Switch Online + "0100bfc01d976000", // Virtual Boy - Nintendo Switch Online "0100ccf019c8c000", // F-ZERO 99 "0100ad9012510000", // PAC-MAN 99 "010040600c5ce000", // Tetris 99 @@ -141,12 +151,17 @@ namespace Ryujinx.Common "0100704000B3A000", // Snipperclips "01006a800016e000", // Super Smash Bros. Ultimate "0100a9400c9c2000", // Tokyo Mirage Sessions #FE Encore + "0100ca502552a000", // Tomodachi Life: Living the Dream - Welcome Edition + "010051f0207b2000", // Tomodachi Life: Living the Dream //Bayonetta Franchise "010076f0049a2000", // Bayonetta "01007960049a0000", // Bayonetta 2 "01004a4010fea000", // Bayonetta 3 "0100cf5010fec000", // Bayonetta Origins: Cereza and the Lost Demon + + // Famicom Detective Club Franchise + "010054601d54c000", // Emio - The Smiling Man: Famicom Detective Series (DEMO) //Persona Franchise "0100dcd01525a000", // Persona 3 Portable @@ -171,7 +186,9 @@ namespace Ryujinx.Common "0100453019aa8000", // Xenoblade Chronicles: X Definitive Edition //Misc Games + "01003670066de000", // 36 Fragments of Midnight "010056e00853a000", // A Hat in Time + "0100c9f00aaee000", // Ascendence "0100fd1014726000", // Baldurs Gate: Dark Alliance "01008c2019598000", // Bluey: The Video Game "010096f00ff22000", // Borderlands 2: Game of the Year Edition @@ -185,8 +202,10 @@ namespace Ryujinx.Common "010027400cdc6000", // Divinity Original 2 - Definitive Edition "01008c8012920000", // Dying Light Platinum Edition "0100d11013e6a000", // Eschatos + "01000490067ae000", // Frederic 2: Evil Strikes Back "01001cc01b2d4000", // Goat Simulator 3 "01003620068ea000", // Hand of Fate 2 + "01007ac00e012000", // HEXAGRAVITY "0100f7e00c70e000", // Hogwarts Legacy "010013c00e930000", // Hollow Knight: Silksong "010085500130a000", // Lego City: Undercover @@ -196,6 +215,7 @@ namespace Ryujinx.Common "0100853015e86000", // No Man's Sky "0100f85014ed0000", // No More Heroes "0100463014ed4000", // No More Heroes 2 + "0100f7d00a1bc000", // NO THING "0100e570094e8000", // Owlboy "01007bb017812000", // Portal "0100abd01785c000", // Portal 2 @@ -204,11 +224,14 @@ namespace Ryujinx.Common "01008e200c5c2000", // Muse Dash "01005ff002e2a000", // Rayman Legends "01007820196a6000", // Red Dead Redemption + "01007a800d520000", // REFUNCT "0100e8300a67a000", // Risk "01002f7013224000", // Rune Factory 5 "01008d100d43e000", // Saints Row IV "0100de600beee000", // Saints Row: The Third - The Full Package "01001180021fa000", // Shovel Knight: Specter of Torment + "010079f00671c000", // Sparkle 2: Evo + "010077b00e046000", // Spyro: Reignited Trilogy "0100e1D01eb2e000", // Squeakross: Home Squeak Home "0100e65002bb8000", // Stardew Valley "0100d7a01b7a2000", // Star Wars: Bounty Hunter diff --git a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs index 5aeb923da..649c3cad6 100644 --- a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs +++ b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs @@ -1,5 +1,6 @@ using Gommon; using Humanizer; +using MsgPack; using System; using System.Buffers.Binary; using System.Collections.Generic; @@ -23,24 +24,382 @@ namespace Ryujinx.Ava.Systems.PlayReport private static FormattedValue SkywardSwordHD_Rupees(SingleValue value) => "rupee".ToQuantity(value.Matched.IntValue); - private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value) - => value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; + private static FormattedValue EchoesOfWisdom_Warp(SingleValue value) + { + FormattedValue locations = value.Matched.IntValue switch + { + // Hyrule Field + 23 => "Hyrule Field: Kakariko Village", + 43 => "Hyrule Field: West of Hyrule Ranch", + 45 => "Hyrule Field: North of Hyrule Ranch", + 25 => "Hyrule Field: Hyrule Ranch", + 26 => "Hyrule Field: West of Hyrule Castle", + 48 => "Hyrule Field: Haunted Grove", + 24 => "Hyrule Field: Hyrule Castle", + 27 => "Hyrule Field: Northern Sanctuary", + 28 => "Eastern Hyrule Field: Eastern Temple", + 41 => "Eastern Hyrule Field: Dampé Studio", + 22 => "Lake Hylia: Great Fairy Shrine", + // Eternal Forest + 47 => "Eternal Forest: Entrance", + 46 => "Eternal Forest: Great Deku Tree", + 752 => "Eternal Forest: Stilled Ancient Ruins (Halfway Point)", + 753 => "Eternal Forest: Stilled Ancient Ruins (Null)", + // Suthorn + 33 => "Suthorn Prairie: Lueburry's House", + 20 => "Suthorn Prairie: Suthorn Village", + 21 => "Suthorn Forest: Suthorn Ruins", + // Faron Wetlands + 13 => "Faron Wetlands: Entrance", + 15 => "Faron Wetlands: Scrubton", + 18 => "Faron Wetlands: Blossu's House", + 17 => "Faron Wetlands: Heart Lake", + 852 => "Faron Wetlands: Stilled Faron Wetlands", + 601 => "Faron Wetlands: Faron Temple 3F", + 602 => "Faron Wetlands: Faron Temple 2F (Underwater Entrance)", + 603 => "Faron Wetlands: Faron Temple 2F (West Entrance)", + 604 => "Faron Wetlands: Faron Temple 2F (Cliff Entrance)", + 605 => "Faron Wetlands: Faron Temple 1F (Diababa)", + 606 => "Faron Wetlands: Faron Temple 1F (Gohma)", + // Jabul Waters + 11 => "Jabul Waters: River Zora Village", + 9 => "Jabul Waters: Crossflows Plaza", + 8 => "Jabul Waters: Seesyde Village", + 12 => "Jabul Waters: Sea Zora Village", + 10 => "Jabul Waters: Lord Jabu-Jabu's Den", + 201 => "Jabul Waters: Jabul Ruins 1F (Entrance)", + 202 => "Jabul Waters: Jabul Ruins 1F (Vocavor)", + // Gerudo Desert + 40 => "Gerudo Desert: Entrance", + 29 => "Gerudo Desert: Oasis", + 32 => "Gerudo Desert: Ancestor's Cave Of Rest", + 30 => "Gerudo Desert: Gerudo Town", + 31 => "Gerudo Desert: Gerudo Sanctum", + 351 => "Gerudo Desert: Stilled Gerudo Sanctum", + 303 => "Gerudo Desert: Gerudo Sanctum 1F (West Entrance)", + 304 => "Gerudo Desert: Gerudo Sanctum 1F (East Entrance)", + 301 => "Gerudo Desert: Gerudo Sanctum 2F (The Key)", + 302 => "Gerudo Desert: Gerudo Sanctum 2F (The Elephant Room)", + 305 => "Gerudo Desert: Gerudo Sanctum 2F (Mogryph)", + // Eldin Volcano + 4 => "Eldin Volcano: Eldin Volcano Trail", + 44 => "Eldin Volcano: Lava Lake", + 3 => "Eldin Volcano: Goron City", + 5 => "Eldin Volcano: Rock Roast Volcano", + 49 => "Eldin Volcano: Crater Shortcut", + 552 => "Eldin Volcano: Stilled Eldin Volcano", + 501 => "Eldin Volcano: Eldin Temple 1F", + 503 => "Eldin Volcano: Eldin Temple 2F", + 502 => "Eldin Volcano: Eldin Temple 3F", + // Hebra Mountain + 34 => "Hebra Mountain: Hebra Mountain Passage (1)", + 35 => "Hebra Mountain: Sheltered Hot Spring", + 36 => "Hebra Mountain: Condé's House", + 38 => "Hebra Mountain: Hebra Mountain Passage (2)", + 37 => "Hebra Mountain: Hebra Mountain Passage (3)", + 39 => "Hebra Mountain: Summit", + 652 => "Hebra Mountain: Stilled Holy Mount Lanayru", + 801 => "Hebra Mountain: Lanayru Temple 1F", + 802 => "Hebra Mountain: Lanayru Temple B2", + 803 => "Hebra Mountain: Lanayru Temple B4", + _ => FormattedValue.ForceReset + }; - private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value) - => value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; + return locations.Reset + ? FormattedValue.ForceReset + : $"Warped to {locations}"; + } + + private static FormattedValue SuperMario3DAllStars(SingleValue value) + { + // TODO: Is this really necessary? + FormattedValue title = value.Matched.IntValue switch + { + 1 => "Super Mario 64", + 2 => "Super Mario Sunshine", + 3 => "Super Mario Galaxy", + _ => FormattedValue.ForceReset + }; + + return title.Reset + ? FormattedValue.ForceReset + : $"Playing {title}"; + } + + private static FormattedValue SuperMario3DAllStars_MainMenu(MultiValue value) + { + int albumId = value.Matched[0].IntValue; + int songId = value.Matched[1].IntValue; + + string album = value.Matched[0].IntValue switch + { + 1 => "Super Mario 64 OST", + 2 => "Super Mario Sunshine OST", + 3 => "Super Mario Galaxy OST", + _ => "Listening to Super Mario 3D All-Stars" + }; + + string song = (albumId, songId) switch + { + // Super Mario 64 + (1, 0) => "It's a Me, Mario!", + (1, 1) => "Title Theme", + (1, 2) => "Peach's Message", + (1, 3) => "Opening", + (1, 4) => "Super Mario 64 Main Theme", + (1, 5) => "Slider", + (1, 6) => "Inside the Castle Walls", + (1, 7) => "Looping Steps", + (1, 8) => "Dire, Dire Docks", + (1, 9) => "Lethal Lava Land", + (1, 10) => "Snow Mountain", + (1, 11) => "Haunted House", + (1, 12) => "Merry-Go-Round", + (1, 13) => "Cave Dungeon", + (1, 14) => "Piranha Plant's Lullaby", + (1, 15) => "Powerful Mario", + (1, 16) => "Metallic Mario", + (1, 17) => "File Select", + (1, 18) => "Correct Solution", + (1, 19) => "Toad's Message", + (1, 20) => "Power Star", + (1, 21) => "Race Fanfare", + (1, 22) => "Star Catch Fanfare", + (1, 23) => "Game Start", + (1, 24) => "Course Clear", + (1, 25) => "Game Over", + (1, 26) => "Stage Boss", + (1, 27) => "Koopa's Message", + (1, 28) => "Koopa's Road", + (1, 29) => "Koopa's Theme", + (1, 30) => "Koopa Clear", + (1, 31) => "Ultimate Koopa", + (1, 32) => "Ultimate Koopa Clear", + (1, 33) => "Ending Demo", + (1, 34) => "Staff Roll", + (1, 35) => "Piranha Plant's Lullaby - Piano", + + // Super Mario Sunshine + (2, 0) => "Isle Delfino", + (2, 1) => "Delfino Airstrip", + (2, 2) => "Bianco Hills", + (2, 3) => "Ricco Harbor", + (2, 4) => "Gelato Beach", + (2, 5) => "Pinna Beach", + (2, 6) => "Pinna Park", + (2, 7) => "Sirena Beach", + (2, 8) => "Hotel Delfino", + (2, 9) => "Casino", + (2, 10) => "Noki Bay", + (2, 11) => "Noki Depths", + (2, 12) => "Pianta Village", + (2, 13) => "Pianta Hot Spring", + (2, 14) => "Pianta Rescue", + (2, 15) => "Pianta Village - Fluff Festival", + (2, 16) => "Underground", + (2, 17) => "Secret Course", + (2, 18) => "Secret Course - Sky and Sea", + (2, 19) => "Corona Mountain", + (2, 20) => "Mid-Boss", + (2, 21) => "Proto Piranha", + (2, 22) => "Phantamanta", + (2, 23) => "Boss Battle", + (2, 24) => "Gooper Blooper Intro", + (2, 25) => "Wiggler Intro", + (2, 26) => "Mecha-Bowser", + (2, 27) => "Bowser", + (2, 28) => "Shadow Mario", + (2, 29) => "Racing Il Piantissimo", + (2, 30) => "Event", + (2, 31) => "Timed Event", + (2, 32) => "Yoshi-Go-Round", + (2, 33) => "Title Screen", + (2, 34) => "Opening Demo", + (2, 35) => "Select Data", + (2, 36) => "Select Scenario", + (2, 37) => "Course Intro", + (2, 38) => "Course Intro - Shadow Mario", + (2, 39) => "A Shine Sprite Appears", + (2, 40) => "Shine!", + (2, 41) => "Race Fanfare", + (2, 42) => "Casino Fanfare", + (2, 43) => "Too Bad!", + (2, 44) => "Game Over", + (2, 45) => "Welcome to Isle Delfino (Movie)", + (2, 46) => "Icky Goop (Movie)", + (2, 47) => "Mario on Trial (Movie)", + (2, 48) => "How to Use FLUDD (Movie)", + (2, 49) => "Shadow Mario Appears (Movie)", + (2, 50) => "The Kidnapping of Princess Peach (Movie)", + (2, 51) => "Mecha-Bowser Rises (Movie)", + (2, 52) => "Meet Bowser Jr. (Movie)", + (2, 53) => "FLUDD Theft (Movie)", + (2, 54) => "Hot Tub Intrusion (Movie)", + (2, 55) => "Epilogue (Movie)", + (2, 56) => "Staff Credits", + (2, 57) => "Have a Relaxing Vacation!", + + // Super Mario Galaxy + (3, 0) => "Overture", + (3, 1) => "The Star Festival", + (3, 2) => "Attack of the Airships", + (3, 3) => "Catastrophe", + (3, 4) => "Peach's Castle Stolen", + (3, 5) => "Enter the Galaxy", + (3, 6) => "Egg Planet", + (3, 7) => "Rosaline in the Observatory 1", + (3, 8) => "The Honeyhive", + (3, 9) => "Space Junk Road", + (3, 10) => "Battlerock Galaxy", + (3, 11) => "Beach Bowl Galaxy", + (3, 12) => "Rosalina in the Observatory 2", + (3, 13) => "Enter Bowser Jr.!", + (3, 14) => "Waltz of the Boos", + (3, 15) => "Buoy Base Galaxy", + (3, 16) => "Gusty Garden Galaxy", + (3, 17) => "Rosaline in the Observatory 3", + (3, 18) => "King Bowser", + (3, 19) => "Melty Molten Galaxy", + (3, 20) => "The Galaxy Reactor", + (3, 21) => "Final Battle with Bowser", + (3, 22) => "A New Dawn", + (3, 23) => "Birth", + (3, 24) => "Super Mario Galaxy", + (3, 25) => "Purple Comet", + (3, 26) => "Blue Sky Athletic", + (3, 27) => "Super Mario 2007", + (3, 28) => "File Select", + (3, 29) => "Luma", + (3, 30) => "Gateway Galaxy", + (3, 31) => "Stolen Grand Star", + (3, 32) => "To the Observatory Grounds 1", + (3, 33) => "Observation Dome", + (3, 34) => "Course Select", + (3, 35) => "Dino Piranha", + (3, 36) => "A Chance to Grab a Star!", + (3, 37) => "A Tense Moment", + (3, 38) => "Big Bad Bugaboom", + (3, 39) => "King Kaliente", + (3, 40) => "The Toad Brigade", + (3, 41) => "Airship Armada", + (3, 42) => "Aquatic Race", + (3, 43) => "Space Fantasy", + (3, 44) => "Megaleg", + (3, 45) => "To The Observatory Grounds 2", + (3, 46) => "Space Athletic", + (3, 47) => "Speedy Comet", + (3, 48) => "Beach Bowl Galaxy - Undersea", + (3, 49) => "Interlude", + (3, 50) => "Bowser's Stronghold Appears", + (3, 51) => "The Fiery Stronghold", + (3, 52) => "The Big Staircase", + (3, 53) => "Bowser Appears", + (3, 54) => "Star Ball", + (3, 55) => "The Library", + (3, 56) => "Buoy Base Galaxy - Undersea", + (3, 57) => "Rainbow Mario", + (3, 58) => "Chase the Bunnies", + (3, 59) => "Help!", + (3, 60) => "Major Burrows", + (3, 61) => "Pipe Interior", + (3, 62) => "Cosmic Comet", + (3, 63) => "Drip Drop Galaxy", + (3, 64) => "Kingfin", + (3, 65) => "Boo Race", + (3, 66) => "Ice Mountain", + (3, 67) => "Ice Mario", + (3, 68) => "Lava Path", + (3, 69) => "Fire Mario", + (3, 70) => "Dusty Dune Galaxy", + (3, 71) => "Heavy Metal Mecha-Bowser", + (3, 72) => "A-wa-wa-wa!", + (3, 73) => "Deep Dark Galaxy", + (3, 74) => "Kamella", + (3, 75) => "Star Ball 2", + (3, 76) => "Sad Girl", + (3, 77) => "Flying Mario", + (3, 78) => "Star Child", + (3, 79) => "A Wish", + (3, 80) => "Family", + _ => "" + }; + + return string.IsNullOrEmpty(song) ? FormattedValue.ForceReset : $"{album} - {song}"; + } + + private static FormattedValue SuperMarioOdyssey(SingleValue value) + => value.Matched.LongValue switch + { + // TODO: Needs updated for sub-areas. + 2973331007 => "Cap Kingdom: Bonneton", + 2661781375 => "Cascade Kingdom: Fossil Falls", + 512560049 => "Sand Kingdom: Tostarena", + 3079659402 => "Wooded Kingdom: Steam Gardens", + 1941286268 => "Lake Kingdom: Lake Lamode", + 3098209122 => "Cloud Kingdom: Nimbus Arena", + 4088050842 => "Lost Kingdom: Forgotten Isle", + 53003352 => "Metro Kingdom: New Donk City", + 4265839612 => "Seaside Kingdom: Bubblaine", + 3288863344 => "Snow Kingdom: Shiveria", + 3180104973 => "Luncheon Kingdom: Mount Volbono", + 2284558980 => "Ruined Kingdom: Crumbleden", + 3024139598 => "Bowser's Kingdom: Bowser's Castle", + 1351608174 => "Moon Kingdom: Honeylune Ridge", + 1698750149 => "Dark Side: Rabbit Ridge", + 3206301958 => "Darker Side: Culmina Crater", + 3963002526 => "Mushroom Kingdom: Peach's Castle", + _ => FormattedValue.ForceReset + }; private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value) => value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; + private static FormattedValue SuperMarioWonder(SingleValue value) + { + // TODO: Needs updated for course names. + MessagePackObject messagePackObject = value.Matched.PackedValue; + MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary(); + + int worldNumber = messagePackObjectDictionary["world_no"].AsInt32(); + int courseNumber = 0; + + if (messagePackObjectDictionary.TryGetValue("course_no", out MessagePackObject courseNumberVariable)) + { + courseNumber = courseNumberVariable.AsInt32(); + } + + FormattedValue world = worldNumber switch + { + 1 => "Pipe-Rock Plateau", + 2 => "Petal Isles", + 3 => "Fluff-Puff Peaks", + 4 => "Shining Falls", + 5 => "Sunbaked Desert", + 6 => "Fungi Mines", + 7 => "Deep Magma Bog", + 9 => "Special World", + _ => FormattedValue.ForceReset + }; + + if (courseNumber == 0) + { + return FormattedValue.ForceReset; + } + + return world.Reset + ? FormattedValue.ForceReset + : $"{world}: {worldNumber}-{courseNumber}"; + } + private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value) => value.Matched.StringValue switch { // Single Player "Single" => "Single Player", // Multiplayer - "Multi-2players" => "Multiplayer 2 Players", - "Multi-3players" => "Multiplayer 3 Players", - "Multi-4players" => "Multiplayer 4 Players", + "Multi-2players" => "Multiplayer: 2 Players", + "Multi-3players" => "Multiplayer: 3 Players", + "Multi-4players" => "Multiplayer: 4 Players", // Wireless/LAN Play "Local-Single" => "Wireless/LAN Play", "Local-2players" => "Wireless/LAN Play 2 Players", @@ -62,8 +421,9 @@ namespace Ryujinx.Ava.Systems.PlayReport private static FormattedValue PokemonSV(MultiValue values) { - - string playStatus = values.Matched[0].BoxedValue is 0 ? "Playing Alone" : "Playing in a group"; + string region = PokemonSV_Region(values.Matched[1].ToString()); + string union = values.Matched[0].BoxedValue is 0 ? "" : " with friends"; + string academyName = PokemonSV_AcademyName(values.Application.Title); FormattedValue locations = values.Matched[1].ToString() switch { @@ -89,18 +449,86 @@ namespace Ryujinx.Ava.Systems.PlayReport "a_w20" => "North Area Three", "a_w21" => "North Area One", "a_w22" => "North Area Two", - "a_w23" => "The Great Crater of Paldea", + "a_w23" => "Area Zero: The Great Crater of Paldea", "a_w24" => "South Paldean Sea", "a_w25" => "West Paldean Sea", "a_w26" => "East Paldean Sea", "a_w27" => "North Paldean Sea", - //TODO DLC Locations + // Naranja / Uva Academy + "a_sch_entrance01" => $"{academyName} Academy: Entrance", + "a_sch_cafe01" => $"{academyName} Academy: Cafeteria", + "a_sch_shop01" => $"{academyName} Academy: School Store", + "a_sch_room01" => $"{academyName} Academy: Home Ec Room", + "a_sch_room02" => $"{academyName} Academy: Art Room", + "a_sch_room03" => $"{academyName} Academy: Biology Lab", + "a_sch_room04" => $"{academyName} Academy: Staff Room", + "a_sch_office01" => $"{academyName} Academy: Director's Office", + "a_sch_office03" => $"{academyName} Academy: Nurse's Office", + "a_sch_ground01" => $"{academyName} Academy: School Yard", + "a_sch_class1a" => $"{academyName} Academy: Classroom 1-A", + "a_sch_class1d" => $"{academyName} Academy: Classroom 1-D", + "a_sch_class2g" => $"{academyName} Academy: Classroom 2-G", + "a_sch_dorm01" => $"{academyName} Academy: Dorm Room (Trainer)", + "a_sch_dorm02" => $"{academyName} Academy: Dorm Room (Nemona)", + "a_sch_dorm03" => $"{academyName} Academy: Dorm Room (Arven)", + "a_sch_dorm04" => $"{academyName} Academy: Dorm Room (Penny)", + // DLC + // Kitakami + "a_su0101" => "Mossui Town", + "a_su0102" => "Loyalty Plaza", + "a_su0103" => "Kitakami Hall", + "a_su0104" => "Oni Mountain", + "a_su0105" => "Infernal Pass", + "a_su0106" => "Crystal Pool", + "a_su0107" => "Wistful Fields", + "a_su0108" => "Mossfell Confluence", + "a_su0109" => "Fellhorn Gorge", + "a_su0110" => "Paradise Barrens", + "a_su0111" => "Timeless Woods", + // Blueberry Academy: School + "a_sch_2_entrance0" => "Blueberry Academy: Entrance", + "a_sch_2_clubroom" => "Blueberry Academy: League Clubroom", + "a_sch_2_class1" => "Blueberry Academy: Classroom 1-4", + "a_sch_2_class2" => "Blueberry Academy: Classroom 3-2", + "a_sch_2_shop01" => "Blueberry Academy: School Store", + "a_sch_2_cafe01" => "Blueberry Academy: Cafeteria", + "a_sch_2_dorm01" => "Blueberry Academy: Dorm Room (Trainer)", + "a_sch_2_dorm02" => "Blueberry Academy: Dorm Room (Carmine)", + // Blueberry Academy: Terrarium + "a_su0201" => "Savanna Biome", + "a_su0202" => "Coastal Biome", + "a_su0203" => "Canyon Biome", + "a_su0204" => "Polar Biome", _ => FormattedValue.ForceReset }; - - return locations.Reset - ? FormattedValue.ForceReset - : $"{playStatus} in {locations}"; + + return locations.Reset + ? FormattedValue.ForceReset + : $"Exploring {region}{union} | {locations}"; + } + + private static string PokemonSV_Region(string location) + { + if (location.Contains("a_su02") || location.Contains("a_sch_2")) return "Unova"; + if (location.Contains("a_su01")) return "Kitakami"; + return "Paldea"; + } + + private static string PokemonSV_AcademyName(string title) + { + // TODO: Is this even necessary? + if ( + title.Contains("Scarlet") + || title.Contains("Escarlata") + || title.Contains("Écarlate") + || title.Contains("Karmesin") + || title.Contains("Scarlatto") + || title.Contains("スカーレット") + || title.Contains("스칼렛") + || title.Contains("朱") + + ) { return "Naranja"; } + return "Uva"; } private static FormattedValue SuperSmashBrosUltimate_Mode(SparseMultiValue values) @@ -641,5 +1069,7 @@ namespace Ryujinx.Ava.Systems.PlayReport _ => FormattedValue.ForceReset }; + + } } diff --git a/src/Ryujinx/Systems/PlayReport/PlayReports.cs b/src/Ryujinx/Systems/PlayReport/PlayReports.cs index 628194b19..d483515bb 100644 --- a/src/Ryujinx/Systems/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Systems/PlayReport/PlayReports.cs @@ -14,7 +14,7 @@ namespace Ryujinx.Ava.Systems.PlayReport private static readonly Lazy _analyzerLazy = new(() => new Analyzer() .AddSpec( - "01007ef00011e000", + "01007ef00011e000", // Breath of the Wild spec => spec .WithDescription("based on being in Master Mode.") .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode) @@ -22,48 +22,74 @@ namespace Ryujinx.Ava.Systems.PlayReport .AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets) ) .AddSpec( - "0100f2c0115b6000", + "0100f2c0115b6000", // Tears of the Kingdom spec => spec .WithDescription("based on where you are in Hyrule (Depths, Surface, Sky).") .AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField)) .AddSpec( - "01002da013484000", + "01002da013484000", // Skyward Sword spec => spec .WithDescription("based on how many Rupees you have.") .AddValueFormatter("rupees", SkywardSwordHD_Rupees)) + .AddSpec( - "0100000000010000", + "01008cf01baac000", // Echoes of Wisdom spec => spec - .WithDescription("based on if you're playing with Assist Mode.") - .AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode) + .WithDescription("based on where you've warped.") + .AddValueFormatter("dest_index", EchoesOfWisdom_Warp) + ) + + .AddSpec( + "010049900f546000", // Super Mario 3D All Stars + spec => spec + .WithDescription("based on what album and track you're listening to.") + .AddMultiValueFormatter(["app_id","song_id"], SuperMario3DAllStars_MainMenu) ) .AddSpec( - "010075000ecbe000", + ["010049900f546001", "010049900f546002", "010049900F546003"], // Super Mario 3D All Stars spec => spec - .WithDescription("based on if you're playing with Assist Mode.") - .AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode) + .WithDescription("based on which game you've selected to play in the collection.") + .AddValueFormatter("program_id", SuperMario3DAllStars) ) .AddSpec( - "010028600ebda000", + "0100000000010000", // Super Mario Odyssey + spec => spec + .WithDescription("based on what kingdom you're in.") + .AddValueFormatter("stage_name", SuperMarioOdyssey) + ) + .AddSpec( + "010028600ebda000", // Super Mario 3D World + Bowser's Fury spec => spec .WithDescription("based on being in either Super Mario 3D World or Bowser's Fury.") .AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury) ) + .AddSpec( + ["010049900f546000", "010049900f546001", "010049900f546002", "010049900F546003"], + spec => spec + .WithDescription("based on which game you've selected to play in the collection.") + .AddValueFormatter("program_id", SuperMario3DAllStars) + ) + .AddSpec( + "010015100b514000", // Super Mario Bros. Wonder + spec => spec + .WithDescription("based on what world and course you're in.") + .AddValueFormatter("stage_info", SuperMarioWonder) + ) .AddSpec( // Global & China IDs - ["0100152000022000", "010075100e8ec000"], + ["0100152000022000", "010075100e8ec000"], // Mario Kart 8 Deluxe spec => spec .WithDescription( "based on what modes you're selecting in the menu & whether or not you're in a race.") .AddValueFormatter("To", MarioKart8Deluxe_Mode) ) .AddSpec( - ["0100a3d008c5c000", "01008f6008c5e000"], + ["0100a3d008c5c000", "01008f6008c5e000"], // Pokemon Scarlet/Violet spec => spec .WithDescription("based on if you're playing alone or in a group and what area of Paldea you're exploring.") .AddMultiValueFormatter(["team_circle", "area_no"], PokemonSV) ) .AddSpec( - "01006a800016e000", + "01006a800016e000", // Super Smash Bros. Ultimate spec => spec .WithDescription("based on what mode you're playing, who won, and what characters were present.") .AddSparseMultiValueFormatter( @@ -83,8 +109,10 @@ namespace Ryujinx.Ava.Systems.PlayReport ) .AddSpec( [ - "0100c9a00ece6000", "01008d300c50c000", "0100d870045b6000", - "010012f017576000", "0100c62011050000", "0100b3c014bda000" + "0100B4E00444C000", "0100d870045b6000", "01008d300c50c000", "0100c62011050000", "010012f017576000", + /*Famicom*/ /*NES*/ /*SNES*/ /*GBC*/ /*GBA*/ + "0100b3c014bda000", "0100c9a00ece6000", "0100e0601c632000", "0100bfc01d976000" + /*SEGA Genesis*/ /*N64*/ /*N64 MATURE*/ /*Virtual Boy*/ ], spec => spec .WithDescription( From c0078088ddc1df9720de05f2501cf794c4cbdf30 Mon Sep 17 00:00:00 2001 From: AsperTheDog Date: Thu, 14 May 2026 08:59:10 +0000 Subject: [PATCH 49/61] Add shader non-uniform indexing support (#91) This PR marks ALL texture indexes as nonuniform to fix an issue with the paths in Tomodachi Life: Living the Dream on AMD cards. It should have a negligible impact on performance (and it should not have an impact at all on NVIDIA cards!) It's caused by what is called 'implicit non-uniform sampler array indexing'. The idea is basically that some GPUs optimize texture lookups from indexed texture arrays, by assuming that you are never going to index different textures within a single workgroup. What this causes is that visual glitch where a subgroup is tasked with rendering a block of the screen, and in the boundaries some cores are indexing the wrong texture. Co-authored-by: AsperTheDog Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/91 --- src/Ryujinx.Graphics.GAL/Capabilities.cs | 3 ++ .../Shader/DiskCache/DiskCacheHostStorage.cs | 2 +- .../Shader/GpuAccessorBase.cs | 2 + src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs | 1 + .../CodeGen/Spirv/Instructions.cs | 50 +++++++++++++++++-- .../CodeGen/Spirv/SpirvGenerator.cs | 8 +++ src/Ryujinx.Graphics.Shader/IGpuAccessor.cs | 4 ++ .../Translation/HostCapabilities.cs | 3 ++ .../Translation/TranslatorContext.cs | 1 + .../VulkanInitialization.cs | 2 + src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 3 ++ 11 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs index 1eec80e51..4271c3d18 100644 --- a/src/Ryujinx.Graphics.GAL/Capabilities.cs +++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs @@ -42,6 +42,7 @@ namespace Ryujinx.Graphics.GAL public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderFloat64; + public readonly bool SupportsShaderNonUniformIndexing; public readonly bool SupportsTextureGatherOffsets; public readonly bool SupportsTextureShadowLod; public readonly bool SupportsVertexStoreAndAtomics; @@ -110,6 +111,7 @@ namespace Ryujinx.Graphics.GAL bool supportsShaderBallot, bool supportsShaderBarrierDivergence, bool supportsShaderFloat64, + bool supportsShaderNonUniformIndexing, bool supportsTextureGatherOffsets, bool supportsTextureShadowLod, bool supportsVertexStoreAndAtomics, @@ -172,6 +174,7 @@ namespace Ryujinx.Graphics.GAL SupportsShaderBallot = supportsShaderBallot; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderFloat64 = supportsShaderFloat64; + SupportsShaderNonUniformIndexing = supportsShaderNonUniformIndexing; SupportsTextureGatherOffsets = supportsTextureGatherOffsets; SupportsTextureShadowLod = supportsTextureShadowLod; SupportsVertexStoreAndAtomics = supportsVertexStoreAndAtomics; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs index 6444b18e3..7b58e18c3 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs @@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache private const ushort FileFormatVersionMajor = 1; private const ushort FileFormatVersionMinor = 2; private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor; - private const uint CodeGenVersion = 7353; + private const uint CodeGenVersion = 7354; private const string SharedTocFileName = "shared.toc"; private const string SharedDataFileName = "shared.data"; diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs index d89eebabf..2f8c329e5 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs @@ -231,6 +231,8 @@ namespace Ryujinx.Graphics.Gpu.Shader public bool QueryHostSupportsShaderFloat64() => _context.Capabilities.SupportsShaderFloat64; + public bool QueryHostSupportsShaderNonUniformIndexing() => _context.Capabilities.SupportsShaderNonUniformIndexing; + public bool QueryHostSupportsSnormBufferTextureFormat() => _context.Capabilities.SupportsSnormBufferTextureFormat; public bool QueryHostSupportsTextureGatherOffsets() => _context.Capabilities.SupportsTextureGatherOffsets; diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs index af1494bbe..acc0dbd68 100644 --- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs +++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs @@ -184,6 +184,7 @@ namespace Ryujinx.Graphics.OpenGL supportsShaderBallot: HwCapabilities.SupportsShaderBallot, supportsShaderBarrierDivergence: !(intelWindows || intelUnix), supportsShaderFloat64: true, + supportsShaderNonUniformIndexing: false, supportsTextureGatherOffsets: true, supportsTextureShadowLod: HwCapabilities.SupportsTextureShadowLod, supportsVertexStoreAndAtomics: true, diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs index 83b037c1c..9de806d89 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs @@ -587,6 +587,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv return OperationResult.Invalid; } + private static void MarkNonUniform(CodeGenContext context, SpvInstruction inst) + { + if (context.HostCapabilities.SupportsShaderNonUniformIndexing) + { + context.Decorate(inst, Decoration.NonUniform); + } + } + private static OperationResult GenerateImageAtomic(CodeGenContext context, AstOperation operation) { AstTextureOperation texOp = (AstTextureOperation)operation; @@ -613,6 +621,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv SpvInstruction textureIndex = Src(AggregateType.S32); image = context.AccessChain(imagePointerType, image, textureIndex); + MarkNonUniform(context, image); } int coordsCount = texOp.Type.Dimensions; @@ -683,15 +692,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; SpvInstruction image = declaration.Image; + bool isIndexed = declaration.IsIndexed; - if (declaration.IsIndexed) + if (isIndexed) { SpvInstruction textureIndex = Src(AggregateType.S32); image = context.AccessChain(declaration.ImagePointerType, image, textureIndex); + MarkNonUniform(context, image); } image = context.Load(declaration.ImageType, image); + if (isIndexed) + { + MarkNonUniform(context, image); + } int coordsCount = texOp.Type.Dimensions; @@ -740,15 +755,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()]; SpvInstruction image = declaration.Image; + bool isIndexed = declaration.IsIndexed; - if (declaration.IsIndexed) + if (isIndexed) { SpvInstruction textureIndex = Src(AggregateType.S32); image = context.AccessChain(declaration.ImagePointerType, image, textureIndex); + MarkNonUniform(context, image); } image = context.Load(declaration.ImageType, image); + if (isIndexed) + { + MarkNonUniform(context, image); + } int coordsCount = texOp.Type.Dimensions; @@ -1878,35 +1899,56 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv private static SpvInstruction GenerateSampledImageLoad(CodeGenContext context, AstTextureOperation texOp, SamplerDeclaration declaration, ref int srcIndex) { SpvInstruction image = declaration.Image; + bool imageIndexed = declaration.IsIndexed; - if (declaration.IsIndexed) + if (imageIndexed) { SpvInstruction textureIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++)); image = context.AccessChain(declaration.SampledImagePointerType, image, textureIndex); + MarkNonUniform(context, image); } if (texOp.IsSeparate) { image = context.Load(declaration.ImageType, image); + if (imageIndexed) + { + MarkNonUniform(context, image); + } SamplerDeclaration samplerDeclaration = context.Samplers[texOp.GetSamplerSetAndBinding()]; SpvInstruction sampler = samplerDeclaration.Image; + bool samplerIndexed = samplerDeclaration.IsIndexed; - if (samplerDeclaration.IsIndexed) + if (samplerIndexed) { SpvInstruction samplerIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++)); sampler = context.AccessChain(samplerDeclaration.SampledImagePointerType, sampler, samplerIndex); + MarkNonUniform(context, sampler); } sampler = context.Load(samplerDeclaration.ImageType, sampler); + if (samplerIndexed) + { + MarkNonUniform(context, sampler); + } + image = context.SampledImage(declaration.SampledImageType, image, sampler); + if (imageIndexed || samplerIndexed) + { + MarkNonUniform(context, image); + } } else { image = context.Load(declaration.SampledImageType, image); + if (imageIndexed) + { + MarkNonUniform(context, image); + } } return image; diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs index e1561446b..2dd7186ba 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs @@ -60,6 +60,14 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv context.AddCapability(Capability.Float64); } + if (parameters.HostCapabilities.SupportsShaderNonUniformIndexing) + { + context.AddExtension("SPV_EXT_descriptor_indexing"); + context.AddCapability(Capability.ShaderNonUniform); + context.AddCapability(Capability.SampledImageArrayNonUniformIndexing); + context.AddCapability(Capability.StorageImageArrayNonUniformIndexing); + } + if (parameters.Definitions.TransformFeedbackEnabled && parameters.Definitions.LastInVertexPipeline) { context.AddCapability(Capability.TransformFeedback); diff --git a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs index 4e6d6edf9..77953df05 100644 --- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs +++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs @@ -336,6 +336,10 @@ namespace Ryujinx.Graphics.Shader { return true; } + bool QueryHostSupportsShaderNonUniformIndexing() + { + return false; + } /// /// Queries host GPU support for signed normalized buffer texture formats. diff --git a/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs b/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs index 11fe6599d..bfd85c158 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs @@ -9,6 +9,7 @@ namespace Ryujinx.Graphics.Shader.Translation public readonly bool SupportsShaderBallot; public readonly bool SupportsShaderBarrierDivergence; public readonly bool SupportsShaderFloat64; + public readonly bool SupportsShaderNonUniformIndexing; public readonly bool SupportsTextureShadowLod; public readonly bool SupportsViewportMask; @@ -20,6 +21,7 @@ namespace Ryujinx.Graphics.Shader.Translation bool supportsShaderBallot, bool supportsShaderBarrierDivergence, bool supportsShaderFloat64, + bool supportsShaderNonUniformIndexing, bool supportsTextureShadowLod, bool supportsViewportMask) { @@ -30,6 +32,7 @@ namespace Ryujinx.Graphics.Shader.Translation SupportsShaderBallot = supportsShaderBallot; SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence; SupportsShaderFloat64 = supportsShaderFloat64; + SupportsShaderNonUniformIndexing = supportsShaderNonUniformIndexing; SupportsTextureShadowLod = supportsTextureShadowLod; SupportsViewportMask = supportsViewportMask; } diff --git a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs index e1ca22610..c7c562947 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs @@ -364,6 +364,7 @@ namespace Ryujinx.Graphics.Shader.Translation GpuAccessor.QueryHostSupportsShaderBallot(), GpuAccessor.QueryHostSupportsShaderBarrierDivergence(), GpuAccessor.QueryHostSupportsShaderFloat64(), + GpuAccessor.QueryHostSupportsShaderNonUniformIndexing(), GpuAccessor.QueryHostSupportsTextureShadowLod(), GpuAccessor.QueryHostSupportsViewportMask()); diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs index 02c4e6873..9cd8f90d7 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs @@ -494,6 +494,8 @@ namespace Ryujinx.Graphics.Vulkan UniformBufferStandardLayout = supportedPhysicalDeviceVulkan12Features.UniformBufferStandardLayout, UniformAndStorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.UniformAndStorageBuffer8BitAccess, StorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.StorageBuffer8BitAccess, + ShaderSampledImageArrayNonUniformIndexing = supportedPhysicalDeviceVulkan12Features.ShaderSampledImageArrayNonUniformIndexing, + ShaderStorageImageArrayNonUniformIndexing = supportedPhysicalDeviceVulkan12Features.ShaderStorageImageArrayNonUniformIndexing, }; pExtendedFeatures = &featuresVk12; diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index 5f1c50b00..ccb541a34 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -775,6 +775,9 @@ namespace Ryujinx.Graphics.Vulkan supportsShaderBallot: false, supportsShaderBarrierDivergence: Vendor != Vendor.Intel, supportsShaderFloat64: Capabilities.SupportsShaderFloat64, + supportsShaderNonUniformIndexing: + featuresVk12.ShaderSampledImageArrayNonUniformIndexing && + featuresVk12.ShaderStorageImageArrayNonUniformIndexing, supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended && !IsMoltenVk, supportsTextureShadowLod: false, supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics, From c96433eb1351e771d31590b3e35335649f20cf07 Mon Sep 17 00:00:00 2001 From: shinyoyo Date: Thu, 14 May 2026 14:00:16 +0000 Subject: [PATCH 50/61] Updated Simplified Chinese translation (#95) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/95 --- assets/Locales/Root.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index eee476519..8e3d25049 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -596,7 +596,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "重启模拟", "zh_TW": "重新啟動模擬" } }, @@ -6121,7 +6121,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "开启网络日志", "zh_TW": "" } }, @@ -11371,7 +11371,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "保存", "zh_TW": "儲存" } }, @@ -17121,7 +17121,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "在控制台中显示网络日志", "zh_TW": "" } }, @@ -21496,7 +21496,7 @@ "th_TH": "", "tr_TR": "", "uk_UA": "", - "zh_CN": "", + "zh_CN": "控制台将会在下次启动 Ryujinx 时可用。", "zh_TW": "" } }, From 298b6c3959fce51d55d25f6b9f9fdfd82b5b30db Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 15 May 2026 15:13:23 +0000 Subject: [PATCH 51/61] [HLE] Stub ILibrarySelfAccessor:ExitAndReturn (10) (#96) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/96 --- .../ILibraryAppletSelfAccessor.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs index 44c4d133a..fa986de93 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs @@ -1,4 +1,5 @@ using Ryujinx.Common; +using Ryujinx.Common.Logging; using System; namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.LibraryAppletProxy @@ -60,6 +61,19 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib return ResultCode.Success; } + + [CommandCmif(10)] + // ExitProcessAndReturn -> nn::am::service::LibraryAppletInfo + public ResultCode ExitProcessAndReturn(ServiceCtx context) + { + // Exits the LibraryApplet and returns to running the title which launched this LibraryApplet (qlaunch for example). + // On success, official sw will enter an infinite loop with sleep-thread value 86400000000000. + // Since we don't currently support qlaunch, it's fine to stub it. + + Logger.Stub?.PrintStub(LogClass.Service); + return ResultCode.Success; + } + [CommandCmif(11)] // GetLibraryAppletInfo() -> nn::am::service::LibraryAppletInfo @@ -83,7 +97,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib AppletIdentifyInfo appletIdentifyInfo = new() { AppletId = AppletId.QLaunch, - TitleId = 0x0100000000001000, + // 0x4 padding + TitleId = 0x0100000000001000, // qlaunch systemAppletMenu title ID }; context.ResponseData.WriteStruct(appletIdentifyInfo); From 134453e62bb42765626559d4cde22816fb35043b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 15 May 2026 15:13:49 +0000 Subject: [PATCH 52/61] Update dependency SharpCompress to 0.48.1 (#97) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [SharpCompress](https://github.com/adamhathcock/sharpcompress) | `0.48.0` → `0.48.1` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/SharpCompress/0.48.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/SharpCompress/0.48.0/0.48.1?slim=true) | --- ### Release Notes
adamhathcock/sharpcompress (SharpCompress) ### [`v0.48.1`](https://github.com/adamhathcock/sharpcompress/releases/tag/0.48.1): - GZip writing fix [Compare Source](https://github.com/adamhathcock/sharpcompress/compare/0.48.0...0.48.1) #### What's Changed - release test cleanup by [@​adamhathcock](https://github.com/adamhathcock) in [#​1322](https://github.com/adamhathcock/sharpcompress/pull/1322) - Fix GZip write async by [@​adamhathcock](https://github.com/adamhathcock) in [#​1320](https://github.com/adamhathcock/sharpcompress/pull/1320) **Full Changelog**:
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/97 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3d39156b5..73cb36c51 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + From d307afc454e0b0b774c569c4d05f8e69ebbb24c9 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 15 May 2026 15:14:23 +0000 Subject: [PATCH 53/61] Update dependency Sep to 0.14.1 (#98) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [Sep](https://github.com/nietras/Sep) | `0.13.0` → `0.14.1` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Sep/0.14.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Sep/0.13.0/0.14.1?slim=true) | --- ### Release Notes
nietras/Sep (Sep) ### [`v0.14.1`](https://github.com/nietras/Sep/releases/tag/v0.14.1): 0.14.1 #### What's Changed - Improve SepReaderOptions.Unescape/.Trim comments by [@​nietras](https://github.com/nietras) in [#​542](https://github.com/nietras/Sep/pull/542) - Improve SepReader/WriterExtensions.Strict() comments by [@​nietras](https://github.com/nietras) in [#​543](https://github.com/nietras/Sep/pull/543) **Full Changelog**: ### [`v0.14.0`](https://github.com/nietras/Sep/releases/tag/v0.14.0): 0.14.0 #### What's Changed - Add `leaveOpen` overloads for SepReaderOptions.From\* via SepTextReaderDisposers by [@​Copilot](https://github.com/Copilot) in [#​530](https://github.com/nietras/Sep/pull/530) - Bump MSTest from 4.2.1 to 4.2.2 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​533](https://github.com/nietras/Sep/pull/533) - Bump github/codeql-action from 4.35.2 to 4.35.3 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​532](https://github.com/nietras/Sep/pull/532) - Bump step-security/harden-runner from 2.19.0 to 2.19.1 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​531](https://github.com/nietras/Sep/pull/531) - Confirm statuses:write is the correct minimum permission for super-linter (not checks:write) by [@​Copilot](https://github.com/Copilot) in [#​540](https://github.com/nietras/Sep/pull/540) **Full Changelog**:
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/98 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 73cb36c51..7c4da891f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -51,7 +51,7 @@ - + From 48888bd0144162b28a127d89c8dd131b7a2abdd3 Mon Sep 17 00:00:00 2001 From: AsperTheDog Date: Fri, 15 May 2026 15:19:12 +0000 Subject: [PATCH 54/61] Change non-unform shader extension to be more conservative (#99) The previous fix for Tomodachi Life (#91) included the extension to all shaders, independently on if it was needed or not. This PR fixes that by lazily adding the extension only when it is actually needed. This change should not be noticed by anyone, but it avoids having to modify shaders that do not perform any type of dynamic indexing, which apparently is something some modders care about. Co-authored-by: AsperTheDog Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/99 --- .../CodeGen/Spirv/CodeGenContext.cs | 3 +++ .../CodeGen/Spirv/Instructions.cs | 9 +++++++++ .../CodeGen/Spirv/SpirvGenerator.cs | 8 -------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs index 4fe214778..3f80c2ae0 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs @@ -82,6 +82,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv public bool IsMainFunction { get; private set; } public bool MayHaveReturned { get; set; } + public bool WasNonUniformAccessDeclared { get; set; } public CodeGenContext( StructuredProgramInfo info, @@ -89,6 +90,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv GeneratorPool instPool, GeneratorPool integerPool) : base(SpirvVersionPacked, instPool, integerPool) { + WasNonUniformAccessDeclared = false; + Info = info; AttributeUsage = parameters.AttributeUsage; Definitions = parameters.Definitions; diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs index 9de806d89..77a23d1f2 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs @@ -591,7 +591,16 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv { if (context.HostCapabilities.SupportsShaderNonUniformIndexing) { + if (!context.WasNonUniformAccessDeclared) + { + context.AddExtension("SPV_EXT_descriptor_indexing"); + context.AddCapability(Capability.ShaderNonUniform); + context.AddCapability(Capability.SampledImageArrayNonUniformIndexing); + context.AddCapability(Capability.StorageImageArrayNonUniformIndexing); + } + context.Decorate(inst, Decoration.NonUniform); + context.WasNonUniformAccessDeclared = true; } } diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs index 2dd7186ba..e1561446b 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs @@ -60,14 +60,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv context.AddCapability(Capability.Float64); } - if (parameters.HostCapabilities.SupportsShaderNonUniformIndexing) - { - context.AddExtension("SPV_EXT_descriptor_indexing"); - context.AddCapability(Capability.ShaderNonUniform); - context.AddCapability(Capability.SampledImageArrayNonUniformIndexing); - context.AddCapability(Capability.StorageImageArrayNonUniformIndexing); - } - if (parameters.Definitions.TransformFeedbackEnabled && parameters.Definitions.LastInVertexPipeline) { context.AddCapability(Capability.TransformFeedback); From 58bd19a2f31a1f9e7d5e67d3a78661255d305229 Mon Sep 17 00:00:00 2001 From: AsperTheDog Date: Fri, 15 May 2026 15:36:14 +0000 Subject: [PATCH 55/61] Fix Vulkan validation errors (#92) This PR fixes several validation errors caused by invalid Vulkan usage. These validation errors often end up invoking Undefined Behavior on the driver side, which can lead to artifacts or crashes which are driver specific and otherwise incredibly hard to track. I don't think it should have any impact on performance, but it would be good to test it with as many games as possible (maybe a bug in a game was fixed?). Each commit fixes an error. I added to each a description with the validation error that was fixed and a small explanation on what was causing it and how I fixed it. Co-authored-by: AsperTheDog Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/92 --- src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs | 9 +++++- src/Ryujinx.Graphics.Vulkan/TextureCopy.cs | 31 +++++++++++++++++++ src/Ryujinx.Graphics.Vulkan/TextureStorage.cs | 11 ++++--- src/Ryujinx.Graphics.Vulkan/TextureView.cs | 14 ++++++++- .../VertexBufferState.cs | 10 ++---- .../VertexBufferUpdater.cs | 24 +++++++++++--- src/Ryujinx.Graphics.Vulkan/Window.cs | 4 ++- 7 files changed, 84 insertions(+), 19 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs b/src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs index 251f74319..927845fa0 100644 --- a/src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs +++ b/src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs @@ -46,7 +46,14 @@ namespace Ryujinx.Graphics.Vulkan public static (AccessFlags Access, PipelineStageFlags Stages) GetSubpassAccessSuperset(VulkanRenderer gd) { - AccessFlags access = BufferAccess; + AccessFlags access = BufferAccess | + AccessFlags.ShaderReadBit | + AccessFlags.ShaderWriteBit | + AccessFlags.ColorAttachmentReadBit | + AccessFlags.ColorAttachmentWriteBit | + AccessFlags.DepthStencilAttachmentReadBit | + AccessFlags.DepthStencilAttachmentWriteBit; + PipelineStageFlags stages = PipelineStageFlags.AllGraphicsBit; if (gd.TransformFeedbackApi != null) diff --git a/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs b/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs index aae3b0afb..0efb4dbb0 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureCopy.cs @@ -15,6 +15,8 @@ namespace Ryujinx.Graphics.Vulkan Image dstImage, TextureCreateInfo srcInfo, TextureCreateInfo dstInfo, + TextureCreateInfo srcStorageInfo, + TextureCreateInfo dstStorageInfo, Extents2D srcRegion, Extents2D dstRegion, int srcLayer, @@ -40,6 +42,13 @@ namespace Ryujinx.Graphics.Vulkan return (xy1, xy2); } + static (Offset3D, Offset3D) ClampOffsetsToMip(Offset3D xy1, Offset3D xy2, int mipW, int mipH) + { + return ( + new Offset3D(Math.Min(xy1.X, mipW), Math.Min(xy1.Y, mipH), xy1.Z), + new Offset3D(Math.Min(xy2.X, mipW), Math.Min(xy2.Y, mipH), xy2.Z)); + } + if (srcAspectFlags == 0) { srcAspectFlags = srcInfo.Format.ConvertAspectFlags(); @@ -80,6 +89,14 @@ namespace Ryujinx.Graphics.Vulkan (srcOffsets.Element0, srcOffsets.Element1) = ExtentsToOffset3D(srcRegion, srcInfo.Width, srcInfo.Height, level); (dstOffsets.Element0, dstOffsets.Element1) = ExtentsToOffset3D(dstRegion, dstInfo.Width, dstInfo.Height, level); + int srcMipW = Math.Max(1, srcStorageInfo.Width >> (int)copySrcLevel); + int srcMipH = Math.Max(1, srcStorageInfo.Height >> (int)copySrcLevel); + int dstMipW = Math.Max(1, dstStorageInfo.Width >> (int)copyDstLevel); + int dstMipH = Math.Max(1, dstStorageInfo.Height >> (int)copyDstLevel); + + (srcOffsets.Element0, srcOffsets.Element1) = ClampOffsetsToMip(srcOffsets.Element0, srcOffsets.Element1, srcMipW, srcMipH); + (dstOffsets.Element0, dstOffsets.Element1) = ClampOffsetsToMip(dstOffsets.Element0, dstOffsets.Element1, dstMipW, dstMipH); + ImageBlit region = new() { SrcSubresource = srcSl, @@ -121,6 +138,8 @@ namespace Ryujinx.Graphics.Vulkan Image dstImage, TextureCreateInfo srcInfo, TextureCreateInfo dstInfo, + TextureCreateInfo srcStorageInfo, + TextureCreateInfo dstStorageInfo, int srcViewLayer, int dstViewLayer, int srcViewLevel, @@ -151,6 +170,8 @@ namespace Ryujinx.Graphics.Vulkan dstImage, srcInfo, dstInfo, + srcStorageInfo, + dstStorageInfo, srcViewLayer, dstViewLayer, srcViewLevel, @@ -186,6 +207,8 @@ namespace Ryujinx.Graphics.Vulkan Image dstImage, TextureCreateInfo srcInfo, TextureCreateInfo dstInfo, + TextureCreateInfo srcStorageInfo, + TextureCreateInfo dstStorageInfo, int srcViewLayer, int dstViewLayer, int srcViewLevel, @@ -314,6 +337,14 @@ namespace Ryujinx.Graphics.Vulkan int copyWidth = sizeInBlocks ? BitUtils.DivRoundUp(width, blockWidth) : width; int copyHeight = sizeInBlocks ? BitUtils.DivRoundUp(height, blockHeight) : height; + int srcMipW = Math.Max(1, srcStorageInfo.Width >> (srcViewLevel + srcLevel + level)); + int srcMipH = Math.Max(1, srcStorageInfo.Height >> (srcViewLevel + srcLevel + level)); + int dstMipW = Math.Max(1, dstStorageInfo.Width >> (dstViewLevel + dstLevel + level)); + int dstMipH = Math.Max(1, dstStorageInfo.Height >> (dstViewLevel + dstLevel + level)); + + copyWidth = Math.Min(copyWidth, Math.Min(srcMipW, dstMipW)); + copyHeight = Math.Min(copyHeight, Math.Min(srcMipH, dstMipH)); + Extent3D extent = new((uint)copyWidth, (uint)copyHeight, (uint)srcDepth); if (srcInfo.Samples > 1 && srcInfo.Samples != dstInfo.Samples) diff --git a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs index 46cd5b4be..b102efaf2 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs @@ -67,6 +67,8 @@ namespace Ryujinx.Graphics.Vulkan public VkFormat VkFormat { get; } + public ImageUsageFlags UsageFlags { get; } + public unsafe TextureStorage( VulkanRenderer gd, Device device, @@ -93,7 +95,8 @@ namespace Ryujinx.Graphics.Vulkan SampleCountFlags sampleCountFlags = ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)info.Samples); - ImageUsageFlags usage = GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported, true); + ImageUsageFlags usage = GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported); + UsageFlags = usage; ImageCreateFlags flags = ImageCreateFlags.CreateMutableFormatBit | ImageCreateFlags.CreateExtendedUsageBit; @@ -159,7 +162,7 @@ namespace Ryujinx.Graphics.Vulkan _imageAuto = new Auto(new DisposableImage(_gd.Api, device, _image)); - InitialTransition(ImageLayout.Preinitialized, ImageLayout.General); + InitialTransition(ImageLayout.Undefined, ImageLayout.General); } _slices = new TextureSliceInfo[levels * _depthOrLayers]; @@ -307,7 +310,7 @@ namespace Ryujinx.Graphics.Vulkan } } - public static ImageUsageFlags GetImageUsage(Format format, in HardwareCapabilities capabilities, bool isMsImageStorageSupported, bool extendedUsage) + public static ImageUsageFlags GetImageUsage(Format format, in HardwareCapabilities capabilities, bool isMsImageStorageSupported) { ImageUsageFlags usage = DefaultUsageFlags; @@ -320,7 +323,7 @@ namespace Ryujinx.Graphics.Vulkan usage |= ImageUsageFlags.ColorAttachmentBit; } - if ((format.IsImageCompatible && isMsImageStorageSupported) || extendedUsage) + if (format.IsImageCompatible && isMsImageStorageSupported) { usage |= ImageUsageFlags.StorageBit; } diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs index 4513c804f..b6c0b8369 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs @@ -64,7 +64,7 @@ namespace Ryujinx.Graphics.Vulkan bool isMsImageStorageSupported = gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample; VkFormat format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format, isMsImageStorageSupported); - ImageUsageFlags usage = TextureStorage.GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported, false); + ImageUsageFlags usage = TextureStorage.GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported) & storage.UsageFlags; uint levels = (uint)info.Levels; uint layers = (uint)info.GetLayers(); @@ -133,6 +133,8 @@ namespace Ryujinx.Graphics.Vulkan shaderUsage |= ImageUsageFlags.StorageBit; } + shaderUsage &= storage.UsageFlags; + _imageView = CreateImageView(componentMapping, subresourceRange, type, shaderUsage); // Framebuffer attachments and storage images requires a identity component mapping. @@ -257,6 +259,8 @@ namespace Ryujinx.Graphics.Vulkan dstImage, src.Info, dst.Info, + src.Storage.Info, + dst.Storage.Info, src.FirstLayer, dst.FirstLayer, src.FirstLevel, @@ -310,6 +314,8 @@ namespace Ryujinx.Graphics.Vulkan dstImage, src.Info, dst.Info, + src.Storage.Info, + dst.Storage.Info, src.FirstLayer, dst.FirstLayer, src.FirstLevel, @@ -385,6 +391,8 @@ namespace Ryujinx.Graphics.Vulkan dst.GetImage().Get(cbs).Value, src.Info, dst.Info, + src.Storage.Info, + dst.Storage.Info, src.FirstLayer, dst.FirstLayer, src.FirstLevel, @@ -410,6 +418,8 @@ namespace Ryujinx.Graphics.Vulkan dst.GetImage().Get(cbs).Value, src.Info, dst.Info, + src.Storage.Info, + dst.Storage.Info, srcRegion, dstRegion, src.FirstLayer, @@ -463,6 +473,8 @@ namespace Ryujinx.Graphics.Vulkan dstImage.Get(cbs).Value, src.Info, dst.Info, + src.Storage.Info, + dst.Storage.Info, srcRegion, dstRegion, src.FirstLayer, diff --git a/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs b/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs index 236ab8721..da4edaa6a 100644 --- a/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs +++ b/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs @@ -68,9 +68,7 @@ namespace Ryujinx.Graphics.Vulkan int stride = (_stride + (alignment - 1)) & -alignment; int newSize = (_size / _stride) * stride; - Buffer buffer = autoBuffer.Get(cbs, 0, newSize).Value; - - updater.BindVertexBuffer(cbs, binding, buffer, 0, (ulong)newSize, (ulong)stride); + updater.BindVertexBuffer(cbs, binding, autoBuffer, 0, newSize, (ulong)stride); _buffer = autoBuffer; @@ -93,11 +91,7 @@ namespace Ryujinx.Graphics.Vulkan if (autoBuffer != null) { - int offset = _offset; - bool mirrorable = _size <= VertexBufferMaxMirrorable; - Buffer buffer = mirrorable ? autoBuffer.GetMirrorable(cbs, ref offset, _size, out _).Value : autoBuffer.Get(cbs, offset, _size).Value; - - updater.BindVertexBuffer(cbs, binding, buffer, (ulong)offset, (ulong)_size, (ulong)_stride); + updater.BindVertexBuffer(cbs, binding, autoBuffer, _offset, _size, (ulong)_stride); } } diff --git a/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs b/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs index 94269dd76..8927d2264 100644 --- a/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs +++ b/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs @@ -15,6 +15,10 @@ namespace Ryujinx.Graphics.Vulkan private readonly NativeArray _sizes; private readonly NativeArray _strides; + private readonly Auto[] _bufferAutos; + private readonly int[] _bufferOffsetsForGet; + private readonly int[] _bufferSizesForGet; + public VertexBufferUpdater(VulkanRenderer gd) { _gd = gd; @@ -23,9 +27,13 @@ namespace Ryujinx.Graphics.Vulkan _offsets = new NativeArray(Constants.MaxVertexBuffers); _sizes = new NativeArray(Constants.MaxVertexBuffers); _strides = new NativeArray(Constants.MaxVertexBuffers); + + _bufferAutos = new Auto[Constants.MaxVertexBuffers]; + _bufferOffsetsForGet = new int[Constants.MaxVertexBuffers]; + _bufferSizesForGet = new int[Constants.MaxVertexBuffers]; } - public void BindVertexBuffer(CommandBufferScoped cbs, uint binding, VkBuffer buffer, ulong offset, ulong size, ulong stride) + public void BindVertexBuffer(CommandBufferScoped cbs, uint binding, Auto autoBuffer, int offset, int size, ulong stride) { if (_count == 0) { @@ -39,9 +47,11 @@ namespace Ryujinx.Graphics.Vulkan int index = (int)_count; - _buffers[index] = buffer; - _offsets[index] = offset; - _sizes[index] = size; + _bufferAutos[index] = autoBuffer; + _bufferOffsetsForGet[index] = offset; + _bufferSizesForGet[index] = size; + _offsets[index] = (ulong)offset; + _sizes[index] = (ulong)size; _strides[index] = stride; _count++; @@ -51,6 +61,12 @@ namespace Ryujinx.Graphics.Vulkan { if (_count != 0) { + for (int i = 0; i < _count; i++) + { + _buffers[i] = _bufferAutos[i].Get(cbs, _bufferOffsetsForGet[i], _bufferSizesForGet[i]).Value; + _bufferAutos[i] = null; + } + if (_gd.Capabilities.SupportsExtendedDynamicState) { _gd.ExtendedDynamicStateApi.CmdBindVertexBuffers2( diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs index 0a0d970c1..eb8d433be 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -391,12 +391,12 @@ namespace Ryujinx.Graphics.Vulkan { if (_effect != null) { + _gd.FlushAllCommands(); _gd.CommandBufferPool.Return( cbs, null, [PipelineStageFlags.ColorAttachmentOutputBit], null); - _gd.FlushAllCommands(); cbs.GetFence().Wait(); cbs = _gd.CommandBufferPool.Rent(); } @@ -455,6 +455,8 @@ namespace Ryujinx.Graphics.Vulkan ImageLayout.General, ImageLayout.PresentSrcKhr); + _gd.FlushAllCommands(); + _gd.CommandBufferPool.Return( cbs, [_imageAvailableSemaphores[semaphoreIndex]], From e756ad4556773ddf5b1d955e8372f0c736ab8337 Mon Sep 17 00:00:00 2001 From: Babib3l Date: Sat, 16 May 2026 01:11:06 +0000 Subject: [PATCH 56/61] Fix ProcessLoader stale PID validation against kernel process table (#102) This PR adresses the following issue : `ProcessLoader.ActiveApplication` could return invalid results when `_latestPid` pointed to a process that no longer existed in the kernel's process table. The original exception path was commented out and bypassed (by sh0inx?) with `GetValueOrDefault` to prevent UI lockups, but this only resolved the symptoms without fixing the root cause. This was due to sevral factors : - `_latestPid` was never reset or validated against the actual process state - ProcessLoader maintained its own `_processesByPid` dictionary separate from the kernel's `KernelContext.Processes` - No cleanups happened when processes exited or were terminated - ProcessLoader state could drift out of sync with the kernel process table **Solution/Fixes** - Validate` _latestPid` against the kernel process table before returning `ActiveApplication` - Check process state (Exited/Exiting) and automatically clear stale references - Add thread-safe cleanup methods (`ClearProcess`, `ClearAllProcesses`) - Integrate `ClearAllProcesses` into Switch.Dispose for proper shutdown cleanup - Add warning logs when stale PID is detected and cleared for debugging **Code Changes**: - `ProcessLoader.cs`: Add `_pidLock`, update `ActiveApplication` with validation, add cleanup methods - `Switch.cs`: Call `Processes.ClearAllProcesses()` in Dispose() Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/102 --- .../Loaders/Processes/ProcessLoader.cs | 92 +++++++++++++++++-- src/Ryujinx.HLE/Switch.cs | 1 + 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 900703f6e..f217ecd0b 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -28,21 +28,61 @@ namespace Ryujinx.HLE.Loaders.Processes private ulong _latestPid; + private readonly object _pidLock = new(); + #nullable enable public ProcessResult? ActiveApplication { get { - return _processesByPid.GetValueOrDefault(_latestPid); - - // Using this if statement locks up the UI and prevents a new game from loading. - // Haven't quite deduced why yet. - - if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value)) - throw new RyujinxException( - $"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?"); + lock (_pidLock) + { + // Check if _latestPid is still valid + if (_latestPid == 0) + { + return null; + } - return value; + // Verify process still exists in kernel (authoritative source) + if (!_device.System.KernelContext.Processes.TryGetValue(_latestPid, out HOS.Kernel.Process.KProcess? kernelProcess)) + { + // Process no longer exists in kernel, clear stale state + Logger.Warning?.Print(LogClass.Loader, + $"ActiveApplication PID {_latestPid} no longer exists in kernel, clearing stale state"); + + _processesByPid.TryRemove(_latestPid, out _); + _latestPid = 0; + TitleIDs.CurrentApplication.Value = null; + + return null; + } + + // Verify process still exists in ProcessLoader's dictionary + if (_processesByPid.TryGetValue(_latestPid, out ProcessResult? processResult)) + { + // Additional check: verify process state + if (kernelProcess.State == HOS.Kernel.Process.ProcessState.Exited || + kernelProcess.State == HOS.Kernel.Process.ProcessState.Exiting) + { + Logger.Warning?.Print(LogClass.Loader, + $"ActiveApplication PID {_latestPid} is in state {kernelProcess.State}, clearing"); + + _processesByPid.TryRemove(_latestPid, out _); + _latestPid = 0; + TitleIDs.CurrentApplication.Value = null; + + return null; + } + + return processResult; + } + + // Fallback: clear stale PID if not in our dictionary + Logger.Warning?.Print(LogClass.Loader, + $"ActiveApplication PID {_latestPid} not in ProcessLoader dictionary, clearing"); + _latestPid = 0; + return null; + } } } #nullable disable @@ -285,5 +325,39 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } + + /// + /// Clears a specific process from the ProcessLoader's tracking. + /// This should be called when a process exits or is terminated. + /// + /// The process ID to clear + public void ClearProcess(ulong pid) + { + lock (_pidLock) + { + if (_processesByPid.TryRemove(pid, out _)) + { + if (_latestPid == pid) + { + _latestPid = 0; + TitleIDs.CurrentApplication.Value = null; + } + } + } + } + + /// + /// Clears all processes from the ProcessLoader's tracking. + /// This should be called during system shutdown. + /// + public void ClearAllProcesses() + { + lock (_pidLock) + { + _processesByPid.Clear(); + _latestPid = 0; + TitleIDs.CurrentApplication.Value = null; + } + } } } diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 850c8b5fa..90af47988 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -183,6 +183,7 @@ namespace Ryujinx.HLE { if (disposing) { + Processes.ClearAllProcesses(); System.Dispose(); AudioDeviceDriver.Dispose(); FileSystem.Dispose(); From 3473044c6e4b30ef98208a23dd9003b8ab53a647 Mon Sep 17 00:00:00 2001 From: Babib3l Date: Sat, 16 May 2026 13:28:02 +0000 Subject: [PATCH 57/61] New French Translations (#101) French translations + capital letters on spanish translations Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/101 --- assets/Locales/Root.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index 8e3d25049..757ddeec8 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -6107,8 +6107,8 @@ "de_DE": "", "el_GR": "", "en_US": "Enable Net Logs", - "es_ES": "Habilitar registros de red.", - "fr_FR": "", + "es_ES": "Habilitar Registros de Red.", + "fr_FR": "Activer les Journaux Réseau.", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -17108,7 +17108,7 @@ "el_GR": "", "en_US": "Prints network log messages in the console.", "es_ES": "Imprimir registros de red en la consola.", - "fr_FR": "", + "fr_FR": "Affiche les journaux réseau dans la console.", "he_IL": "", "it_IT": "", "ja_JP": "", From b7772462f187760456dd0a421d0d3c0fe4dedbc5 Mon Sep 17 00:00:00 2001 From: AsperTheDog Date: Mon, 18 May 2026 16:34:04 +0000 Subject: [PATCH 58/61] Fix crash on Mac and Android caused by a buffer validation error fix in #92 (#105) This PR fixes a bug introduced in #92. In that PR, the problematic commit did its work directly on the updater's own arrays, calling Auto.Get() on each entry as it went, and nulling entries out along the way. The problem is that Get() can call back into Commit (via ClearMirrors -> Rebind), and when it did, that reentrant Commit would read from the same arrays the outer call was still in the middle of processing, hit one of the entries the outer had already nulled, and throw an NullReferenceException. The fix is to have Commit start by copying everything it needs into local variables and resetting _count to zero, so a reentrant call sees a clean updater and operates on its own data. The outer call then writes its snapshot back into the native arrays just before recording the Vulkan bind. Co-authored-by: AsperTheDog Co-authored-by: Max Co-authored-by: Renovate Bot Co-authored-by: Babib3l Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/105 --- .../VertexBufferUpdater.cs | 81 ++++++++++++++----- 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs b/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs index 8927d2264..c2c2ba6f2 100644 --- a/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs +++ b/src/Ryujinx.Graphics.Vulkan/VertexBufferUpdater.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using VkBuffer = Silk.NET.Vulkan.Buffer; namespace Ryujinx.Graphics.Vulkan @@ -61,29 +62,65 @@ namespace Ryujinx.Graphics.Vulkan { if (_count != 0) { - for (int i = 0; i < _count; i++) - { - _buffers[i] = _bufferAutos[i].Get(cbs, _bufferOffsetsForGet[i], _bufferSizesForGet[i]).Value; - _bufferAutos[i] = null; - } - - if (_gd.Capabilities.SupportsExtendedDynamicState) - { - _gd.ExtendedDynamicStateApi.CmdBindVertexBuffers2( - cbs.CommandBuffer, - _baseBinding, - _count, - _buffers.Pointer, - _offsets.Pointer, - _sizes.Pointer, - _strides.Pointer); - } - else - { - _gd.Api.CmdBindVertexBuffers(cbs.CommandBuffer, _baseBinding, _count, _buffers.Pointer, _offsets.Pointer); - } - + int count = (int)_count; + uint baseBinding = _baseBinding; _count = 0; + + Auto[] autos = ArrayPool>.Shared.Rent(count); + Span getOffsets = stackalloc int[Constants.MaxVertexBuffers]; + Span getSizes = stackalloc int[Constants.MaxVertexBuffers]; + Span offsets = stackalloc ulong[Constants.MaxVertexBuffers]; + Span sizes = stackalloc ulong[Constants.MaxVertexBuffers]; + Span strides = stackalloc ulong[Constants.MaxVertexBuffers]; + Span buffers = stackalloc VkBuffer[Constants.MaxVertexBuffers]; + + for (int i = 0; i < count; i++) + { + autos[i] = _bufferAutos[i]; + _bufferAutos[i] = null; + getOffsets[i] = _bufferOffsetsForGet[i]; + getSizes[i] = _bufferSizesForGet[i]; + offsets[i] = _offsets[i]; + sizes[i] = _sizes[i]; + strides[i] = _strides[i]; + } + + try + { + for (int i = 0; i < count; i++) + { + buffers[i] = autos[i].Get(cbs, getOffsets[i], getSizes[i]).Value; + autos[i] = null; + } + + for (int i = 0; i < count; i++) + { + _buffers[i] = buffers[i]; + _offsets[i] = offsets[i]; + _sizes[i] = sizes[i]; + _strides[i] = strides[i]; + } + + if (_gd.Capabilities.SupportsExtendedDynamicState) + { + _gd.ExtendedDynamicStateApi.CmdBindVertexBuffers2( + cbs.CommandBuffer, + baseBinding, + (uint)count, + _buffers.Pointer, + _offsets.Pointer, + _sizes.Pointer, + _strides.Pointer); + } + else + { + _gd.Api.CmdBindVertexBuffers(cbs.CommandBuffer, baseBinding, (uint)count, _buffers.Pointer, _offsets.Pointer); + } + } + finally + { + ArrayPool>.Shared.Return(autos, clearArray: true); + } } } From d94b759e8945e0e987f986981d8b1c745b2824a1 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 19 May 2026 20:23:06 +0000 Subject: [PATCH 59/61] Vulkan Package Update - Part 1 (#16) As described. Should hopefully bring speed-ups for macOS devices (thanks Ori!) and general improvements across the board for the vulkan backend (maybe). This will be followed up with a PR from @KeatonTheBot. Co-authored-by: V380-Ori Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/16 --- Directory.Packages.props | 11 ++++---- .../macos/construct_universal_dylib.py | 25 +++++++++---------- .../PipelineLayoutFactory.cs | 13 +++++----- src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 7 +++--- .../Ryujinx.HLE.Generators.csproj | 5 +++- src/Ryujinx/Ryujinx.csproj | 8 +++--- 6 files changed, 37 insertions(+), 32 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7c4da891f..8c5ce0410 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ - + @@ -53,9 +53,10 @@ - - - + + + + @@ -64,4 +65,4 @@ - \ No newline at end of file + diff --git a/distribution/macos/construct_universal_dylib.py b/distribution/macos/construct_universal_dylib.py index 5d9321860..18b84399f 100644 --- a/distribution/macos/construct_universal_dylib.py +++ b/distribution/macos/construct_universal_dylib.py @@ -47,14 +47,12 @@ def get_new_name( input_component = str(input_dylib_path).replace(str(input_directory), "")[1:] return Path(os.path.join(output_directory, input_component)) - -def is_fat_file(dylib_path: Path) -> str: - res = subprocess.check_output([LIPO, "-info", str(dylib_path.absolute())]).decode( - "utf-8" - ) - - return not res.split("\n")[0].startswith("Non-fat file") - +def get_archs(dylib_path: Path) -> list[str]: + res = subprocess.check_output([LIPO, "-info", str(dylib_path)]).decode("utf-8") + if res.startswith("Non-fat file"): + return [res.split(":")[-1].strip()] + else: + return res.split("are:")[-1].strip().split() def construct_universal_dylib( arm64_input_dylib_path: Path, x86_64_input_dylib_path: Path, output_dylib_path: Path @@ -69,11 +67,12 @@ def construct_universal_dylib( os.path.basename(arm64_input_dylib_path.resolve()), output_dylib_path ) else: - if is_fat_file(arm64_input_dylib_path) or not x86_64_input_dylib_path.exists(): - with open(output_dylib_path, "wb") as dst: - with open(arm64_input_dylib_path, "rb") as src: - dst.write(src.read()) - else: + arm64_archs = get_archs(arm64_input_dylib_path) + x86_64_archs = get_archs(x86_64_input_dylib_path) if x86_64_input_dylib_path.exists() else [] + + if "arm64" in arm64_archs and "x86_64" in arm64_archs: + shutil.copy2(arm64_input_dylib_path, output_dylib_path) + elif x86_64_archs: subprocess.check_call( [ LIPO, diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs index 612a8b25d..b285e57f5 100644 --- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs +++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs @@ -16,15 +16,15 @@ namespace Ryujinx.Graphics.Vulkan { DescriptorSetLayout[] layouts = new DescriptorSetLayout[setDescriptors.Count]; bool[] updateAfterBindFlags = new bool[setDescriptors.Count]; - + bool isMoltenVk = gd.IsMoltenVk; - + for (int setIndex = 0; setIndex < setDescriptors.Count; setIndex++) { ResourceDescriptorCollection rdc = setDescriptors[setIndex]; ResourceStages activeStages = ResourceStages.None; - + if (isMoltenVk) { for (int descIndex = 0; descIndex < rdc.Descriptors.Count; descIndex++) @@ -42,12 +42,13 @@ namespace Ryujinx.Graphics.Vulkan ResourceDescriptor descriptor = rdc.Descriptors[descIndex]; ResourceStages stages = descriptor.Stages; - if (descriptor.Type == ResourceType.StorageBuffer && isMoltenVk) + if (descriptor.Type == ResourceType.StorageBuffer && gd.IsMoltenVk) { - // There's a bug on MoltenVK where using the same buffer across different stages + // There's a bug in MoltenVK where using the same buffer across different stages // causes invalid resource errors, allow the binding on all active stages as workaround. + // https://github.com/KhronosGroup/MoltenVK/issues/1870 stages = activeStages; - } + } layoutBindings[descIndex] = new DescriptorSetLayoutBinding { diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index ccb541a34..a0b764158 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -435,8 +435,8 @@ namespace Ryujinx.Graphics.Vulkan _physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName), features2.Features.MultiViewport && !(IsMoltenVk && Vendor == Vendor.Amd), // Workaround for AMD on MoltenVK issue featuresRobustness2.NullDescriptor || IsMoltenVk, - supportsPushDescriptors && !IsMoltenVk, - propertiesPushDescriptor.MaxPushDescriptors, + supportsPushDescriptors, + IsMoltenVk ? 16 : propertiesPushDescriptor.MaxPushDescriptors, // In case an old version of MoltenVK is used, apply a limit to prevent vertex explosions. featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart, featuresPrimitiveTopologyListRestart.PrimitiveTopologyPatchListRestart, supportsTransformFeedback, @@ -775,10 +775,11 @@ namespace Ryujinx.Graphics.Vulkan supportsShaderBallot: false, supportsShaderBarrierDivergence: Vendor != Vendor.Intel, supportsShaderFloat64: Capabilities.SupportsShaderFloat64, + supportsShaderNonUniformIndexing: featuresVk12.ShaderSampledImageArrayNonUniformIndexing && featuresVk12.ShaderStorageImageArrayNonUniformIndexing, - supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended && !IsMoltenVk, + supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended, supportsTextureShadowLod: false, supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics, supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex, diff --git a/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj b/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj index 4791a3b27..b5335282e 100644 --- a/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj +++ b/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj @@ -14,7 +14,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + all + diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 3bfddbff6..8a89f3a46 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -28,14 +28,14 @@ true partial - + - + true @@ -63,7 +63,7 @@ - + @@ -73,7 +73,7 @@ - + From ce340e5d0b3ed736b9e7f75623a1eede7784f345 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 20 May 2026 13:00:09 +0000 Subject: [PATCH 60/61] [HID] Fixed HD Rumble latency (#104) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/104 --- assets/Locales/Root.json | 50 +++++++++++ .../Hid/Controller/RumbleConfigController.cs | 5 ++ src/Ryujinx.Input.SDL3/NpadHdRumble.cs | 88 +++++++++++++++---- src/Ryujinx.Input.SDL3/SDL3Gamepad.cs | 15 +++- src/Ryujinx.Input.SDL3/SDL3JoyCon.cs | 17 +++- src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs | 21 ++++- src/Ryujinx.Input.SDL3/SDL3Keyboard.cs | 10 ++- src/Ryujinx.Input.SDL3/SDL3Mouse.cs | 8 +- src/Ryujinx.Input/HLE/NpadController.cs | 56 ++++++------ src/Ryujinx.Input/IGamepad.cs | 9 +- src/Ryujinx/Headless/HeadlessRyujinx.Init.cs | 1 + src/Ryujinx/Input/AvaloniaKeyboard.cs | 16 +++- src/Ryujinx/Input/AvaloniaMouse.cs | 8 +- .../Configuration/ConfigurationFileFormat.cs | 2 +- .../ConfigurationState.Migration.cs | 1 + .../UI/Models/Input/GamepadInputConfig.cs | 3 + .../UI/ViewModels/Input/InputViewModel.cs | 1 + .../ViewModels/Input/RumbleInputViewModel.cs | 3 + .../UI/Views/Input/RumbleInputView.axaml | 9 ++ .../UI/Views/Input/RumbleInputView.axaml.cs | 2 + 20 files changed, 261 insertions(+), 64 deletions(-) diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index 757ddeec8..d9139904c 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -12250,6 +12250,56 @@ "zh_TW": "弱震動調節:" } }, + { + "ID": "ControllerSettingsRumbleUseHDRumble", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Enable HD Rumble", + "es_ES": "Activa vibración HD", + "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": "HDRumbleTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Sends more data to the controller for better rumble.\n\nCurrently only supports first-party Nintendo Switch controllers.\n\nLeave ON if you're using JoyCons or a Pro Controller.", + "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": "DialogMessageSaveNotAvailableMessage", "Translations": { diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs index ee8ab457d..f190996c1 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs @@ -16,5 +16,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller /// Enable Rumble ///
public bool EnableRumble { get; set; } + + /// + /// Enable HD Rumble support + /// = 0) + { + return true; + } + + if (!String.IsNullOrEmpty(SDL_GetError())) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + } + return false; } } - private static int EncodeLowFreq(float lowFreq) { float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f); - return (int)Math.Round(32 * Math.Log2(lf * 0.1f)) - 0x40; + return (int) Math.Round(32 * Math.Log2(lf * 0.1f) - 0x40); } private static int EncodeHighFreq(float highFreq) { float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f); - return ((int)Math.Round(32 * Math.Log2(hf * 0.1f)) - 0x60) * 4; + return (int) Math.Round((32 * Math.Log2(hf * 0.1f) - 0x60) * 4); } private static int EncodeLowAmp(float rawAmp) { - int encodedAmp = 0; + double encodedAmp = 0; if (rawAmp is > 0 and < 0.012f) { @@ -92,15 +103,15 @@ namespace Ryujinx.Input.SDL3 } else if (rawAmp is >= 0.012f and < 0.112f) { - encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f)); + encodedAmp = 4 * Math.Log2(rawAmp * 110f); } else if (rawAmp is >= 0.112f and < 0.225f) { - encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f)); + encodedAmp = 16 * Math.Log2(rawAmp * 17f); } else if (rawAmp is >= 0.225f and <= 1f) { - encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f)); + encodedAmp = 32 * Math.Log2(rawAmp * 8.7f); } return (int)Math.Floor(encodedAmp / 2.0) + 64; @@ -108,7 +119,7 @@ namespace Ryujinx.Input.SDL3 private static int EncodeHighAmp(float rawAmp) { - int encodedAmp = 0; + double encodedAmp = 0; if (rawAmp is > 0 and < 0.012f) { @@ -116,23 +127,23 @@ namespace Ryujinx.Input.SDL3 } else if (rawAmp is >= 0.012f and < 0.112f) { - encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f)); + encodedAmp = 4 * Math.Log2(rawAmp * 110f); } else if (rawAmp is >= 0.112f and < 0.225f) { - encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f)); + encodedAmp = 16 * Math.Log2(rawAmp * 17f); } else if (rawAmp is >= 0.225f and <= 1f) { - encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f)); + encodedAmp = 32 * Math.Log2(rawAmp * 8.7f); } - return encodedAmp * 2; + return (int) Math.Round(encodedAmp * 2); } public bool HdRumble(VibrationValue left, VibrationValue right) { - WriteHdRumble(EncodeLowFreq(left.FrequencyLow), + return WriteHdRumble(EncodeLowFreq(left.FrequencyLow), EncodeLowAmp(left.AmplitudeLow), EncodeHighFreq(left.FrequencyHigh), EncodeHighAmp(left.AmplitudeHigh), @@ -140,7 +151,34 @@ namespace Ryujinx.Input.SDL3 EncodeLowAmp(right.AmplitudeLow), EncodeHighFreq(right.FrequencyHigh), EncodeHighAmp(right.AmplitudeHigh)); - return true; + } + + private int SendHDRumble(byte* data, nuint length) + { + int result = 0; + ulong currentTicks = SDL_GetTicks(); + + // Ditch rumble if we haven't hit the poll-rate yet. + // TODO: figure out a better way to do this + // While the polling check makes the rumble accurate, it also causes it to miss signals. + if ((currentTicks - _lastWriteTicks) < 8) // https://docs.handheldlegend.com/s/progcc-3/doc/lag-comparison-aAR1mV3JLX + { + return result; + } + + SDL_LockJoysticks(); + { + // Fun fact: Mario Kart 8 Deluxe sends rumble packets + // where the amplitude is zero, but the frequency isn't. + result = SDL_hid_write(_hidHandle, data, length); + if (result >= 0) + { + _lastWriteTicks = currentTicks; + } + } + SDL_UnlockJoysticks(); + + return result; } public void Dispose() @@ -148,4 +186,18 @@ namespace Ryujinx.Input.SDL3 SDL_hid_close(_hidHandle); } } + + public enum HDRumbleSupported : ushort + { + JoyConLeft = 0x2006, + JoyConRight = 0x2007, + JoyconPair = 0x2008, + ProController = 0x2009, + JoyconGrip = 0x200e, + Joycon2Right = 0x2066, + Joycon2Left = 0x2067, + Joycon2Pair = 0x2068, + Switch2ProController = 0x2069, + GamecubeController = 0x2073 + } } diff --git a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs index c23b64304..57f2940c8 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs @@ -197,10 +197,12 @@ namespace Ryujinx.Input.SDL3 return _hdRumble?.HdRumble(left, right) ?? false; } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { if ((Features & GamepadFeaturesFlag.Rumble) == 0) - return; + { + return false; + } ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); @@ -219,6 +221,15 @@ namespace Ryujinx.Input.SDL3 if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); } + + if (!String.IsNullOrEmpty(SDL_GetError())) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + return false; + } + + return true; } public Vector3 GetMotionData(MotionInputId inputId) diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs index 5d779518d..e9f11d713 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs @@ -162,7 +162,7 @@ namespace Ryujinx.Input.SDL3 public void SetTriggerThreshold(float triggerThreshold) { - + // No operations } public bool HDRumble(VibrationValue left, VibrationValue right) @@ -170,10 +170,12 @@ namespace Ryujinx.Input.SDL3 return _hdRumble?.HdRumble(left, right) ?? false; } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { if ((Features & GamepadFeaturesFlag.Rumble) == 0) - return; + { + return false; + } ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); @@ -192,6 +194,15 @@ namespace Ryujinx.Input.SDL3 if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); } + + if (!String.IsNullOrEmpty(SDL_GetError())) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + return false; + } + + return true; } public Vector3 GetMotionData(MotionInputId inputId) diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs b/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs index 14352e5a4..6114674ad 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs @@ -1,4 +1,7 @@ +using Gommon; using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -61,7 +64,14 @@ namespace Ryujinx.Input.SDL3 return left.IsPressed(inputId) || right.IsPressed(inputId); } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool HDRumble(VibrationValue left, VibrationValue right) + { + // return _hdRumble?.HdRumble(left, right) ?? false; + // TODO: Track rumble and motion on both controllers + return false; + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { if (lowFrequency != 0) { @@ -78,6 +88,15 @@ namespace Ryujinx.Input.SDL3 left.Rumble(0, 0, durationMs); right.Rumble(0, 0, durationMs); } + + if (!SDL_GetError().IsNullOrEmpty()) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + return false; + } + + return true; } public void SetConfiguration(InputConfig configuration) diff --git a/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs b/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs index f5da11a19..8b179f43f 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Collections.Generic; using System.Numerics; @@ -396,9 +397,14 @@ namespace Ryujinx.Input.SDL3 // No operations } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool HDRumble(VibrationValue left, VibrationValue right) { - // No operations + return false; + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) + { + return false; } public Vector3 GetMotionData(MotionInputId inputId) diff --git a/src/Ryujinx.Input.SDL3/SDL3Mouse.cs b/src/Ryujinx.Input.SDL3/SDL3Mouse.cs index 9fdeb36ab..289a60d85 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Mouse.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Mouse.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Drawing; using System.Numerics; @@ -67,7 +68,12 @@ namespace Ryujinx.Input.SDL3 throw new NotImplementedException(); } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool HDRumble(VibrationValue left, VibrationValue right) + { + throw new NotImplementedException(); + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { throw new NotImplementedException(); } diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index a46ff8daf..85ca5ffcb 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -5,7 +5,6 @@ using Ryujinx.Common.Configuration.Hid.Controller.Motion; using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Hid; using System; -using System.Buffers; using System.Collections.Concurrent; using System.Numerics; using System.Runtime.CompilerServices; @@ -555,34 +554,37 @@ namespace Ryujinx.Input.HLE { if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue)) { - if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble) + if (_config is not StandardControllerInputConfig controllerConfig || + !controllerConfig.Rumble.EnableRumble) { - VibrationValue leftVibrationValue = dualVibrationValue.Item1; - VibrationValue rightVibrationValue = dualVibrationValue.Item2; - - float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble)); - float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble)); - - leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; - leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; - rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; - rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; - - if (_gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false) - { - _gamepad?.Rumble(low, high, uint.MaxValue); - } - - Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " + - $"L.low.amp={leftVibrationValue.AmplitudeLow}, " + - $"L.high.amp={leftVibrationValue.AmplitudeHigh}, " + - $"L.low.freq={leftVibrationValue.FrequencyLow}, " + - $"L.high.freq={leftVibrationValue.FrequencyHigh}, " + - $"R.low.amp={rightVibrationValue.AmplitudeLow}, " + - $"R.high.amp={rightVibrationValue.AmplitudeHigh} " + - $"R.low.freq={rightVibrationValue.FrequencyLow}, " + - $"R.high.freq={rightVibrationValue.FrequencyHigh}"); + return; } + + VibrationValue leftVibrationValue = dualVibrationValue.Item1; + VibrationValue rightVibrationValue = dualVibrationValue.Item2; + + leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; + leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; + rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; + rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; + + if (!controllerConfig.Rumble.UseHDRumble || _gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false) + { + float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15))); + float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85))); + _gamepad?.Rumble(low, high, 0xFFFFFFFF); + } + + Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " + + // Value=value/multiplier * multiplier (result) + $"L.low.amp={leftVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeLow}), " + + $"L.high.amp={leftVibrationValue.AmplitudeHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeHigh}), " + + $"L.low.freq={leftVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyLow}), " + + $"L.high.freq={leftVibrationValue.FrequencyHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyHigh}), " + + $"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeLow}), " + + $"R.high.amp={rightVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeHigh}), " + + $"R.low.freq={rightVibrationValue.FrequencyLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyLow}), " + + $"R.high.freq={rightVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyHigh})"); } } } diff --git a/src/Ryujinx.Input/IGamepad.cs b/src/Ryujinx.Input/IGamepad.cs index d45ac0444..587fd53c0 100644 --- a/src/Ryujinx.Input/IGamepad.cs +++ b/src/Ryujinx.Input/IGamepad.cs @@ -80,10 +80,7 @@ namespace Ryujinx.Input ///
/// The vibration data for the left side /// The vibration data for the right side - bool HDRumble(VibrationValue left, VibrationValue right) - { - return false; - } + bool HDRumble(VibrationValue left, VibrationValue right); /// /// Starts a rumble effect on the gamepad. @@ -91,10 +88,10 @@ namespace Ryujinx.Input /// The intensity of the low frequency from 0.0f to 1.0f /// The intensity of the high frequency from 0.0f to 1.0f /// The duration of the rumble effect in milliseconds. - void Rumble(float lowFrequency, float highFrequency, uint durationMs); + bool Rumble(float lowFrequency, float highFrequency, uint durationMs); /// - /// Get a snaphost of the state of the gamepad that is remapped with the informations from the set via . + /// Get a snaphost of the state of the gamepad that is remapped with the information from the set via . /// /// A remapped snaphost of the state of the gamepad. GamepadStateSnapshot GetMappedStateSnapshot(); diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs index af61b7b63..3574c3061 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs @@ -221,6 +221,7 @@ namespace Ryujinx.Headless StrongRumble = 1f, WeakRumble = 1f, EnableRumble = false, + UseHDRumble = true }, }; } diff --git a/src/Ryujinx/Input/AvaloniaKeyboard.cs b/src/Ryujinx/Input/AvaloniaKeyboard.cs index 031d8b033..704a15ba7 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboard.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboard.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.Input; using System; using System.Collections.Generic; @@ -149,9 +150,20 @@ namespace Ryujinx.Ava.Input Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard"); } - public void SetTriggerThreshold(float triggerThreshold) { } + public void SetTriggerThreshold(float triggerThreshold) + { + // No operations + } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { } + public bool HDRumble(VibrationValue left, VibrationValue right) + { + return false; + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) + { + return false; + } public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero; diff --git a/src/Ryujinx/Input/AvaloniaMouse.cs b/src/Ryujinx/Input/AvaloniaMouse.cs index 52a341a01..8c449b9ee 100644 --- a/src/Ryujinx/Input/AvaloniaMouse.cs +++ b/src/Ryujinx/Input/AvaloniaMouse.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.Input; using System; using System.Drawing; @@ -64,8 +65,13 @@ namespace Ryujinx.Ava.Input { throw new NotImplementedException(); } + + public bool HDRumble(VibrationValue left, VibrationValue right) + { + throw new NotImplementedException(); + } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { throw new NotImplementedException(); } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs index 0b451eacb..1fe98ee69 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs @@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 72; + public const int CurrentVersion = 73; /// /// Version of the configuration file format diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index 90a045a67..728321985 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -333,6 +333,7 @@ namespace Ryujinx.Ava.Systems.Configuration EnableRumble = false, StrongRumble = 1f, WeakRumble = 1f, + UseHDRumble = true }; } } diff --git a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs index 0eeef45f5..2a076f525 100644 --- a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs +++ b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs @@ -20,6 +20,7 @@ namespace Ryujinx.Ava.UI.Models.Input public float WeakRumble { get; set; } public float StrongRumble { get; set; } + public bool UseHDRumble { get; set; } public string Id { get; set; } @@ -236,6 +237,7 @@ namespace Ryujinx.Ava.UI.Models.Input EnableRumble = controllerInput.Rumble.EnableRumble; WeakRumble = controllerInput.Rumble.WeakRumble; StrongRumble = controllerInput.Rumble.StrongRumble; + UseHDRumble = controllerInput.Rumble.UseHDRumble; } if (controllerInput.Led != null) @@ -307,6 +309,7 @@ namespace Ryujinx.Ava.UI.Models.Input EnableRumble = EnableRumble, WeakRumble = WeakRumble, StrongRumble = StrongRumble, + UseHDRumble = UseHDRumble, }, Led = new LedConfigController { diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 51229af72..68559d6c1 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -789,6 +789,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input StrongRumble = 1f, WeakRumble = 1f, EnableRumble = false, + UseHDRumble = true }, }; } diff --git a/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs index e2323f567..74e0cd289 100644 --- a/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs @@ -9,5 +9,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input [ObservableProperty] public partial float WeakRumble { get; set; } + + [ObservableProperty] + public partial bool EnableHDRumble { get; set; } } } diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml index 98489aab0..49eb1a717 100644 --- a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml @@ -53,6 +53,15 @@ Margin="5,0" Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" /> + + + diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs index 347d011d5..655bdf591 100644 --- a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs @@ -22,6 +22,7 @@ namespace Ryujinx.Ava.UI.Views.Input { StrongRumble = config.StrongRumble, WeakRumble = config.WeakRumble, + EnableHDRumble = config.UseHDRumble }; InitializeComponent(); @@ -45,6 +46,7 @@ namespace Ryujinx.Ava.UI.Views.Input GamepadInputConfig config = viewModel.Config; config.StrongRumble = content.ViewModel.StrongRumble; config.WeakRumble = content.ViewModel.WeakRumble; + config.UseHDRumble = content.ViewModel.EnableHDRumble; }; await contentDialog.ShowAsync(); From a3eda287b50a2781fa74dd9436ff2ec88b219bfb Mon Sep 17 00:00:00 2001 From: Mabel Date: Wed, 20 May 2026 13:22:56 +0000 Subject: [PATCH 61/61] Fix Clipboard Copy Operation Crash (#108) Fixes a COM exception crash related to clipboard copy events from changes in Avalonia 11.3 Solves [Ryubing/Issues#294](https://github.com/Ryubing/Issues/issues/294) caused by Avalonia 11.3's changes, see [AvaloniaUI/Avalonia#20007](https://github.com/AvaloniaUI/Avalonia/issues/20007) for more information I've only tested this on Windows, no idea if this has issues in MacOS or Linux, or if it's even a problem there at all. Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/108 --- src/Ryujinx/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index cb219b216..91dff22b4 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -46,6 +46,7 @@ namespace Ryujinx.Ava private const uint MbIconwarning = 0x30; + [STAThread] public static int Main(string[] args) { Version = ReleaseInformation.Version;