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
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/assets/Locales/Root.json b/assets/Locales/Root.json
index db84fd3ae..e5de7b412 100644
--- a/assets/Locales/Root.json
+++ b/assets/Locales/Root.json
@@ -12275,6 +12275,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/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.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
+ /// [] 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);
+ }
}
}
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.Input.SDL3/NpadHdRumble.cs b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs
index e367f6a9c..408b5213b 100644
--- a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs
+++ b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using SDL;
using static SDL.SDL3;
@@ -11,8 +12,9 @@ namespace Ryujinx.Input.SDL3
public unsafe class NpadHdRumble : IDisposable
{
private readonly SDL_hid_device* _hidHandle;
-
+
private int _globalCount;
+ private ulong _lastWriteTicks;
private NpadHdRumble(SDL_hid_device* hidHandle)
{
@@ -28,7 +30,7 @@ namespace Ryujinx.Input.SDL3
}
ushort product = SDL_GetGamepadProduct(gamepadHandle);
- if (product != 0x2006 && product != 0x2007 && product != 0x2009 && product != 0x200e)
+ if (!Enum.IsDefined(typeof(HDRumbleSupported), product))
{
return null;
}
@@ -37,7 +39,7 @@ namespace Ryujinx.Input.SDL3
}
// Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble
- private void WriteHdRumble(
+ private bool WriteHdRumble(
int encLeftLowFreq, int encLeftLowAmp,
int encLeftHighFreq, int encLeftHighAmp,
int encRightLowFreq, int encRightLowAmp,
@@ -65,26 +67,35 @@ namespace Ryujinx.Input.SDL3
fixed (byte* ptr = buf)
{
- SDL_hid_write(_hidHandle, ptr, (nuint)buf.Length);
+ if (SendHDRumble(ptr, (nuint)buf.Length) >= 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/Program.cs b/src/Ryujinx/Program.cs
index 344962f09..35abab811 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;
@@ -54,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;
}
@@ -74,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");
@@ -316,7 +399,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/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 @@
-
+
diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs
index 8890055ac..1b86e4f39 100644
--- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs
+++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs
@@ -334,6 +334,7 @@ namespace Ryujinx.Ava.Systems.Configuration
EnableRumble = false,
StrongRumble = 1f,
WeakRumble = 1f,
+ UseHDRumble = true
};
}
}
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}";
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;
+ }
+ }
+}
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();