From b7772462f187760456dd0a421d0d3c0fe4dedbc5 Mon Sep 17 00:00:00 2001 From: AsperTheDog Date: Mon, 18 May 2026 16:34:04 +0000 Subject: [PATCH 1/7] 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 2/7] 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 3/7] [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 4/7] 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; From 004a12005e552421b1e51a1435d571b399687af8 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 20 May 2026 13:37:05 +0000 Subject: [PATCH 5/7] UI: Included more launch checks (#4) - Added Onedrive folder check for Windows. - Added iCloud and Downloads folder checks for macOS. - Added sudo checks for macOS/Linux. - Added dialogue prompts for macOS/Linux. - Added unofficial build warning for detected Flatpak install. These checks have console fallbacks in case the GUI decides it doesn't want to work that day. Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/4 --- src/Ryujinx/Program.cs | 94 ++++++++++++++++++-- src/Ryujinx/UI/Helpers/LinuxSDLInterop.cs | 30 +++++++ src/Ryujinx/UI/Helpers/macOSNativeInterop.cs | 62 +++++++++++++ 3 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 src/Ryujinx/UI/Helpers/LinuxSDLInterop.cs create mode 100644 src/Ryujinx/UI/Helpers/macOSNativeInterop.cs diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 91dff22b4..17d941962 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -55,17 +55,39 @@ namespace Ryujinx.Ava { 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); + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run on an outdated version of Windows. Exiting..."); + _ = Win32NativeInterop.MessageBoxA(nint.Zero, + "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", + $"Ryujinx {Version}", MbIconwarning); return 0; } - var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + string onedriveFiles = Environment.GetEnvironmentVariable("Onedrive"); + string onedriveConsumerFiles = Environment.GetEnvironmentVariable("OnedriveConsumer"); + string onedriveCommercialFiles = Environment.GetEnvironmentVariable("OnedriveCommercial"); + + // Apparently not everyone has OneDrive shoved onto their system. + if ((onedriveFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveFiles)) + || (onedriveConsumerFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveConsumerFiles)) + || (onedriveCommercialFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveCommercialFiles))) + { + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from a OneDrive folder. Exiting..."); + _ = Win32NativeInterop.MessageBoxA(nint.Zero, + "Ryujinx is not intended to be run from a OneDrive folder. Please move it out and relaunch.", + $"Ryujinx {Version}", MbIconwarning); + return 0; + } + + string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); if (Environment.CurrentDirectory.StartsWithIgnoreCase(programFiles) || Environment.CurrentDirectory.StartsWithIgnoreCase(programFilesX86)) { - _ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", $"Ryujinx {Version}", MbIconwarning); + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the Program Files folder. Exiting..."); + _ = Win32NativeInterop.MessageBoxA(nint.Zero, + "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", + $"Ryujinx {Version}", MbIconwarning); return 0; } @@ -75,10 +97,70 @@ namespace Ryujinx.Ava // ...but this reads like it checks if the current is in/has the Windows admin role? lol if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) { - _ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run as administrator.", $"Ryujinx {Version}", MbIconwarning); + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting..."); + _ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run as administrator.", + $"Ryujinx {Version}", MbIconwarning); return 0; } } + else // Unix + { + // sudo check + [DllImport("libc")] + static extern uint geteuid(); + bool root = geteuid().Equals(0); + + if (OperatingSystem.IsMacOS()) + { + if (root) + { + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting..."); + macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}", + "Ryujinx is not intended to be run as administrator.", "Ok"); + return 0; + } + + string downloadFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"); + + if (Environment.CurrentDirectory.StartsWithIgnoreCase(downloadFiles)) + { + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the Downloads folder. Exiting..."); + macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}", + "Ryujinx is not intended to be run from the Downloads folder.", "Ok"); + return 0; + } + + string icloudFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library/Mobile Documents/com~apple~CloudDocs"); + + if (Environment.CurrentDirectory.StartsWithIgnoreCase(icloudFiles)) + { + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the iCloud folder. Exiting..."); + macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}", + "Ryujinx is not intended to be run from the iCloud folder.", "Ok"); + return 0; + } + } + + if (OperatingSystem.IsLinux()) + { + if (root) + { + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting..."); + LinuxSDLInterop.SimpleMessageBox($"Ryujinx {Version}", "Ryujinx is not intended to be run as administrator."); + return 0; + } + + string container = Environment.GetEnvironmentVariable("container"); + + if (container is not null && container.EqualsIgnoreCase("flatpak")) + { + Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-="); + Logger.Warning?.PrintMsg(LogClass.Application, "This is very likely an unofficial build, Ryujinx does NOT have a flatpak!"); + Logger.Info?.PrintMsg(LogClass.Application, "Please visit https://ryujinx.app/ for our official builds."); + Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-="); + } + } + } bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui"); bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps"); @@ -311,7 +393,7 @@ namespace Ryujinx.Ava "never" => HideCursorMode.Never, "onidle" => HideCursorMode.OnIdle, "always" => HideCursorMode.Always, - _ => ConfigurationState.Instance.HideCursor, + _ => ConfigurationState.Instance.HideCursor }; // Check if memoryManagerMode was overridden. diff --git a/src/Ryujinx/UI/Helpers/LinuxSDLInterop.cs b/src/Ryujinx/UI/Helpers/LinuxSDLInterop.cs new file mode 100644 index 000000000..95885ba5c --- /dev/null +++ b/src/Ryujinx/UI/Helpers/LinuxSDLInterop.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class LinuxSDLInterop + { + // TODO: add a parameter for prompt style + // TODO: look into adding text for the button + // TODO: check success of prompt box + public static int SimpleMessageBox(string caption, string text) + { + const string sdl = "SDL2"; + + [DllImport(sdl)] + static extern int SDL_Init(uint flags); + + [DllImport(sdl, CallingConvention = CallingConvention.Cdecl)] + static extern int SDL_ShowSimpleMessageBox(uint flags, string title, string message, IntPtr window); + + [DllImport(sdl)] + static extern void SDL_Quit(); + + SDL_Init(0); + SDL_ShowSimpleMessageBox(32 /* 32 = warning style */, caption, text, IntPtr.Zero); + SDL_Quit(); + return 0; + } + } +} diff --git a/src/Ryujinx/UI/Helpers/macOSNativeInterop.cs b/src/Ryujinx/UI/Helpers/macOSNativeInterop.cs new file mode 100644 index 000000000..6dda1c823 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/macOSNativeInterop.cs @@ -0,0 +1,62 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class macOSNativeInterop + { + // TODO: add a parameter for prompt style + // TODO: check success of prompt box + public static int SimpleMessageBox(string caption, string text, string button) + { + + // Grab what we need to make the message box. + const string ObjCRuntime = "/usr/lib/libobjc.A.dylib"; + const string FoundationFramework = "/System/Library/Frameworks/Foundation.framework/Foundation"; + const string AppKitFramework = "/System/Library/Frameworks/AppKit.framework/AppKit"; + + [DllImport(ObjCRuntime, EntryPoint = "sel_registerName")] + static extern IntPtr GetSelector(string name); + + [DllImport(ObjCRuntime, EntryPoint = "objc_getClass")] + static extern IntPtr GetClass(string name); + + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + static extern IntPtr SendMessage(IntPtr target, IntPtr selector); + + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + static extern IntPtr SendMessageWithParameter(IntPtr target, IntPtr selector, IntPtr param); + + [DllImport(ObjCRuntime)] + static extern IntPtr dlopen(string path, int mode); + + dlopen(AppKitFramework, 0x1); // have to invoke AppKit so that NSAlert doesn't return a null pointer + + IntPtr NSStringClass = GetClass("NSString"); + IntPtr Selector = GetSelector("stringWithUTF8String:"); + IntPtr SharedApp = SendMessage(GetClass("NSApplication"), GetSelector("sharedApplication")); + IntPtr NSAlert = SendMessage(GetClass("NSAlert"), GetSelector("alloc")); + IntPtr AlertInstance = SendMessage(NSAlert, GetSelector("init")); + + // Create caption, text, and button text. + IntPtr boxCaption = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(caption)); + IntPtr boxText = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(text)); + IntPtr boxButton = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(button)); + + // Set up the window. + SendMessageWithParameter(SharedApp, GetSelector("setActivationPolicy:"), IntPtr.Zero); // Give it a window. + SendMessageWithParameter(SharedApp, GetSelector("activateIgnoringOtherApps:"), (IntPtr) 1); // Force it to the front. + + // Set up the message box. + SendMessageWithParameter(AlertInstance, GetSelector("setAlertStyle:"), IntPtr.Zero); // Set style to warning. + SendMessageWithParameter(AlertInstance, GetSelector("setMessageText:"), boxCaption); + SendMessageWithParameter(AlertInstance, GetSelector("setInformativeText:"), boxText); + SendMessageWithParameter(AlertInstance, GetSelector("addButtonWithTitle:"), boxButton); + + // Send prompt to user, then clean up. + SendMessage(AlertInstance, GetSelector("runModal")); + SendMessage(AlertInstance, GetSelector("release")); + return 0; + } + } +} From 81468c1d25fe5f76ced0fd675ea8ed41feb8983e Mon Sep 17 00:00:00 2001 From: Mabel Date: Wed, 20 May 2026 13:38:57 +0000 Subject: [PATCH 6/7] Discord Rich Presence: Tomodachi Life LTD and Animal Crossing New Horizons (#103) Adds Discord Rich Presence for Tomodachi Life: Living the Dream, Tomodachi Life: Living the Dream - Welcome Version, and Animal Crossing: New Horizons Tomodachi Life Rich Presence uses your total Mii count, and your island level ![image](/attachments/46c20fba-f092-4f8c-af8d-c71340d94e78) Animal Crossing New Horizons Rich Presence uses your island name ![image](/attachments/f9dfbeaa-86a3-4989-b74a-d65b1d8e6260) Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/103 --- .../PlayReport/PlayReports.Formatters.cs | 17 +++++++++++++++++ src/Ryujinx/Systems/PlayReport/PlayReports.cs | 13 +++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs index 649c3cad6..d8df7b627 100644 --- a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs +++ b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs @@ -1070,6 +1070,23 @@ namespace Ryujinx.Ava.Systems.PlayReport _ => FormattedValue.ForceReset }; + private static FormattedValue TomodachiLifeLTD_Status(SingleValue value) + { + MessagePackObject messagePackObject = value.Matched.PackedValue; + MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary(); + + int miiCount = messagePackObjectDictionary["MiiNum"].AsInt32(); + int fountainLevel = messagePackObjectDictionary["FountainLevel"].AsInt32(); + + return $"Looking after {"Mii".ToQuantity(miiCount)}, with an island level of {fountainLevel}"; + } + + private static FormattedValue AnimalCrossingNewHorizons_AppCommon(SingleValue value) + { + MessagePackObject messagePackObject = value.Matched.PackedValue; + MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary(); + return $"Living on {messagePackObjectDictionary["LandName"].AsString()} Island"; + } } } diff --git a/src/Ryujinx/Systems/PlayReport/PlayReports.cs b/src/Ryujinx/Systems/PlayReport/PlayReports.cs index d483515bb..8880ed5d4 100644 --- a/src/Ryujinx/Systems/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Systems/PlayReport/PlayReports.cs @@ -119,6 +119,19 @@ namespace Ryujinx.Ava.Systems.PlayReport "based on what game you first launch.\n\nNSO emulators do not print any Play Report information past the first game launch so it's all we got.") .AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame) ) + .AddSpec( + [ "010051f0207b2000", "0100ca502552a000" ], // Tomodachi Life: Living the Dream + Demo + spec => spec + .WithDescription( + "based on your total Mii count and island level.") + .AddValueFormatter("Common", TomodachiLifeLTD_Status) + ) + .AddSpec( + "01006f8002326000", // Animal Crossing New Horizons + spec => spec + .WithDescription("based on your island name.") + .AddValueFormatter("AppCmn", AnimalCrossingNewHorizons_AppCommon) + ) ); private static string Playing(string game) => $"Playing {game}"; From e477ec714923ff98b96db4025cde263a8b68febf Mon Sep 17 00:00:00 2001 From: KeatonTheBot Date: Fri, 22 May 2026 21:47:41 +0000 Subject: [PATCH 7/7] CI: Re-enable win-arm64 builds (#12) Re-enable win-arm64 builds in CI now that they have been fixed with file trimming. Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/12 --- .forgejo/workflows/build.yml | 2 +- .forgejo/workflows/canary.yml | 6 +++--- .forgejo/workflows/release.yml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index e7052a6f6..27cf7405b 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -28,7 +28,7 @@ jobs: configuration: [Release] platform: - { name: win-x64, zip_os_name: win_x64 } - #- { name: win-arm64, zip_os_name: win_arm64 } + - { name: win-arm64, zip_os_name: win_arm64 } - { name: linux-x64, zip_os_name: linux_x64 } - { name: linux-arm64, zip_os_name: linux_arm64 } #- { name: osx-x64, zip_os_name: osx_x64 } diff --git a/.forgejo/workflows/canary.yml b/.forgejo/workflows/canary.yml index 930e6b253..816ad3a99 100644 --- a/.forgejo/workflows/canary.yml +++ b/.forgejo/workflows/canary.yml @@ -32,9 +32,9 @@ jobs: matrix: platform: - { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 } - #- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 } - - { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 } - - { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 } + - { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 } + - { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 } + - { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 } steps: - uses: actions/checkout@v6 diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 341430adb..905f22b21 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -26,9 +26,9 @@ jobs: matrix: platform: - { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 } - #- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 } - - { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 } - - { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 } + - { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 } + - { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 } + - { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 } steps: - uses: actions/checkout@v6