Compare commits

...

2 Commits

Author SHA1 Message Date
Max
ce340e5d0b [HID] Fixed HD Rumble latency (#104)
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/104
2026-05-20 13:00:09 +00:00
Max
d94b759e89 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 <infiniteloop0finsanity@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/16
2026-05-19 20:23:06 +00:00
26 changed files with 298 additions and 96 deletions

View File

@@ -45,7 +45,7 @@
<!--<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />--> <!--<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />-->
<PackageVersion Include="Ryujinx.Audio.OpenAL" Version="1.25.1" /> <PackageVersion Include="Ryujinx.Audio.OpenAL" Version="1.25.1" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.4-build6" /> <PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.4-build6" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" /> <PackageVersion Include="Ryujinx.Graphics.Vulkan.MoltenVK" Version="1.4.2-ryujinx.3" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.133" /> <PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.133" />
<PackageVersion Include="Ryujinx.UpdateClient" Version="2.0.6" /> <PackageVersion Include="Ryujinx.UpdateClient" Version="2.0.6" />
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="2.0.6" /> <PackageVersion Include="Ryujinx.Systems.Update.Common" Version="2.0.6" />
@@ -53,9 +53,10 @@
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" /> <PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.14.1" /> <PackageVersion Include="Sep" Version="0.14.1" />
<PackageVersion Include="shaderc.net" Version="0.1.0" /> <PackageVersion Include="shaderc.net" Version="0.1.0" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.22.0" /> <PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.22.0" /> <PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.22.0" /> <PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
<PackageVersion Include="SkiaSharp" Version="2.88.9" /> <PackageVersion Include="SkiaSharp" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.NativeAssets.Win32" Version="2.88.9" /> <PackageVersion Include="SkiaSharp.NativeAssets.Win32" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.NativeAssets.macOS" Version="2.88.9" /> <PackageVersion Include="SkiaSharp.NativeAssets.macOS" Version="2.88.9" />
@@ -64,4 +65,4 @@
<PackageVersion Include="System.IO.Hashing" Version="9.0.15" /> <PackageVersion Include="System.IO.Hashing" Version="9.0.15" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.1.3" /> <PackageVersion Include="UnicornEngine.Unicorn" Version="2.1.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -12250,6 +12250,56 @@
"zh_TW": "弱震動調節:" "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", "ID": "DialogMessageSaveNotAvailableMessage",
"Translations": { "Translations": {

View File

@@ -47,14 +47,12 @@ def get_new_name(
input_component = str(input_dylib_path).replace(str(input_directory), "")[1:] input_component = str(input_dylib_path).replace(str(input_directory), "")[1:]
return Path(os.path.join(output_directory, input_component)) return Path(os.path.join(output_directory, input_component))
def get_archs(dylib_path: Path) -> list[str]:
def is_fat_file(dylib_path: Path) -> str: res = subprocess.check_output([LIPO, "-info", str(dylib_path)]).decode("utf-8")
res = subprocess.check_output([LIPO, "-info", str(dylib_path.absolute())]).decode( if res.startswith("Non-fat file"):
"utf-8" return [res.split(":")[-1].strip()]
) else:
return res.split("are:")[-1].strip().split()
return not res.split("\n")[0].startswith("Non-fat file")
def construct_universal_dylib( def construct_universal_dylib(
arm64_input_dylib_path: Path, x86_64_input_dylib_path: Path, output_dylib_path: Path 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 os.path.basename(arm64_input_dylib_path.resolve()), output_dylib_path
) )
else: else:
if is_fat_file(arm64_input_dylib_path) or not x86_64_input_dylib_path.exists(): arm64_archs = get_archs(arm64_input_dylib_path)
with open(output_dylib_path, "wb") as dst: x86_64_archs = get_archs(x86_64_input_dylib_path) if x86_64_input_dylib_path.exists() else []
with open(arm64_input_dylib_path, "rb") as src:
dst.write(src.read()) if "arm64" in arm64_archs and "x86_64" in arm64_archs:
else: shutil.copy2(arm64_input_dylib_path, output_dylib_path)
elif x86_64_archs:
subprocess.check_call( subprocess.check_call(
[ [
LIPO, LIPO,

View File

@@ -16,5 +16,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller
/// Enable Rumble /// Enable Rumble
/// </summary> /// </summary>
public bool EnableRumble { get; set; } public bool EnableRumble { get; set; }
/// <summary>
/// Enable HD Rumble support
/// </summary
public bool UseHDRumble { get; set; }
} }
} }

View File

@@ -16,15 +16,15 @@ namespace Ryujinx.Graphics.Vulkan
{ {
DescriptorSetLayout[] layouts = new DescriptorSetLayout[setDescriptors.Count]; DescriptorSetLayout[] layouts = new DescriptorSetLayout[setDescriptors.Count];
bool[] updateAfterBindFlags = new bool[setDescriptors.Count]; bool[] updateAfterBindFlags = new bool[setDescriptors.Count];
bool isMoltenVk = gd.IsMoltenVk; bool isMoltenVk = gd.IsMoltenVk;
for (int setIndex = 0; setIndex < setDescriptors.Count; setIndex++) for (int setIndex = 0; setIndex < setDescriptors.Count; setIndex++)
{ {
ResourceDescriptorCollection rdc = setDescriptors[setIndex]; ResourceDescriptorCollection rdc = setDescriptors[setIndex];
ResourceStages activeStages = ResourceStages.None; ResourceStages activeStages = ResourceStages.None;
if (isMoltenVk) if (isMoltenVk)
{ {
for (int descIndex = 0; descIndex < rdc.Descriptors.Count; descIndex++) for (int descIndex = 0; descIndex < rdc.Descriptors.Count; descIndex++)
@@ -42,12 +42,13 @@ namespace Ryujinx.Graphics.Vulkan
ResourceDescriptor descriptor = rdc.Descriptors[descIndex]; ResourceDescriptor descriptor = rdc.Descriptors[descIndex];
ResourceStages stages = descriptor.Stages; 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. // causes invalid resource errors, allow the binding on all active stages as workaround.
// https://github.com/KhronosGroup/MoltenVK/issues/1870
stages = activeStages; stages = activeStages;
} }
layoutBindings[descIndex] = new DescriptorSetLayoutBinding layoutBindings[descIndex] = new DescriptorSetLayoutBinding
{ {

View File

@@ -435,8 +435,8 @@ namespace Ryujinx.Graphics.Vulkan
_physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName), _physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName),
features2.Features.MultiViewport && !(IsMoltenVk && Vendor == Vendor.Amd), // Workaround for AMD on MoltenVK issue features2.Features.MultiViewport && !(IsMoltenVk && Vendor == Vendor.Amd), // Workaround for AMD on MoltenVK issue
featuresRobustness2.NullDescriptor || IsMoltenVk, featuresRobustness2.NullDescriptor || IsMoltenVk,
supportsPushDescriptors && !IsMoltenVk, supportsPushDescriptors,
propertiesPushDescriptor.MaxPushDescriptors, IsMoltenVk ? 16 : propertiesPushDescriptor.MaxPushDescriptors, // In case an old version of MoltenVK is used, apply a limit to prevent vertex explosions.
featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart, featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart,
featuresPrimitiveTopologyListRestart.PrimitiveTopologyPatchListRestart, featuresPrimitiveTopologyListRestart.PrimitiveTopologyPatchListRestart,
supportsTransformFeedback, supportsTransformFeedback,
@@ -775,10 +775,11 @@ namespace Ryujinx.Graphics.Vulkan
supportsShaderBallot: false, supportsShaderBallot: false,
supportsShaderBarrierDivergence: Vendor != Vendor.Intel, supportsShaderBarrierDivergence: Vendor != Vendor.Intel,
supportsShaderFloat64: Capabilities.SupportsShaderFloat64, supportsShaderFloat64: Capabilities.SupportsShaderFloat64,
supportsShaderNonUniformIndexing: supportsShaderNonUniformIndexing:
featuresVk12.ShaderSampledImageArrayNonUniformIndexing && featuresVk12.ShaderSampledImageArrayNonUniformIndexing &&
featuresVk12.ShaderStorageImageArrayNonUniformIndexing, featuresVk12.ShaderStorageImageArrayNonUniformIndexing,
supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended && !IsMoltenVk, supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended,
supportsTextureShadowLod: false, supportsTextureShadowLod: false,
supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics, supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics,
supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex, supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex,

View File

@@ -14,7 +14,10 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" >
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,3 +1,4 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using SDL; using SDL;
using static SDL.SDL3; using static SDL.SDL3;
@@ -11,8 +12,9 @@ namespace Ryujinx.Input.SDL3
public unsafe class NpadHdRumble : IDisposable public unsafe class NpadHdRumble : IDisposable
{ {
private readonly SDL_hid_device* _hidHandle; private readonly SDL_hid_device* _hidHandle;
private int _globalCount; private int _globalCount;
private ulong _lastWriteTicks;
private NpadHdRumble(SDL_hid_device* hidHandle) private NpadHdRumble(SDL_hid_device* hidHandle)
{ {
@@ -28,7 +30,7 @@ namespace Ryujinx.Input.SDL3
} }
ushort product = SDL_GetGamepadProduct(gamepadHandle); ushort product = SDL_GetGamepadProduct(gamepadHandle);
if (product != 0x2006 && product != 0x2007 && product != 0x2009 && product != 0x200e) if (!Enum.IsDefined(typeof(HDRumbleSupported), product))
{ {
return null; return null;
} }
@@ -37,7 +39,7 @@ namespace Ryujinx.Input.SDL3
} }
// Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble // 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 encLeftLowFreq, int encLeftLowAmp,
int encLeftHighFreq, int encLeftHighAmp, int encLeftHighFreq, int encLeftHighAmp,
int encRightLowFreq, int encRightLowAmp, int encRightLowFreq, int encRightLowAmp,
@@ -65,26 +67,35 @@ namespace Ryujinx.Input.SDL3
fixed (byte* ptr = buf) 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) private static int EncodeLowFreq(float lowFreq)
{ {
float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f); 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) private static int EncodeHighFreq(float highFreq)
{ {
float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f); 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) private static int EncodeLowAmp(float rawAmp)
{ {
int encodedAmp = 0; double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f) if (rawAmp is > 0 and < 0.012f)
{ {
@@ -92,15 +103,15 @@ namespace Ryujinx.Input.SDL3
} }
else if (rawAmp is >= 0.012f and < 0.112f) 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) 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) 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; return (int)Math.Floor(encodedAmp / 2.0) + 64;
@@ -108,7 +119,7 @@ namespace Ryujinx.Input.SDL3
private static int EncodeHighAmp(float rawAmp) private static int EncodeHighAmp(float rawAmp)
{ {
int encodedAmp = 0; double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f) if (rawAmp is > 0 and < 0.012f)
{ {
@@ -116,23 +127,23 @@ namespace Ryujinx.Input.SDL3
} }
else if (rawAmp is >= 0.012f and < 0.112f) 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) 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) 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) public bool HdRumble(VibrationValue left, VibrationValue right)
{ {
WriteHdRumble(EncodeLowFreq(left.FrequencyLow), return WriteHdRumble(EncodeLowFreq(left.FrequencyLow),
EncodeLowAmp(left.AmplitudeLow), EncodeLowAmp(left.AmplitudeLow),
EncodeHighFreq(left.FrequencyHigh), EncodeHighFreq(left.FrequencyHigh),
EncodeHighAmp(left.AmplitudeHigh), EncodeHighAmp(left.AmplitudeHigh),
@@ -140,7 +151,34 @@ namespace Ryujinx.Input.SDL3
EncodeLowAmp(right.AmplitudeLow), EncodeLowAmp(right.AmplitudeLow),
EncodeHighFreq(right.FrequencyHigh), EncodeHighFreq(right.FrequencyHigh),
EncodeHighAmp(right.AmplitudeHigh)); 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() public void Dispose()
@@ -148,4 +186,18 @@ namespace Ryujinx.Input.SDL3
SDL_hid_close(_hidHandle); 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
}
} }

View File

@@ -197,10 +197,12 @@ namespace Ryujinx.Input.SDL3
return _hdRumble?.HdRumble(left, right) ?? false; 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) if ((Features & GamepadFeaturesFlag.Rumble) == 0)
return; {
return false;
}
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
@@ -219,6 +221,15 @@ namespace Ryujinx.Input.SDL3
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); 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) public Vector3 GetMotionData(MotionInputId inputId)

View File

@@ -162,7 +162,7 @@ namespace Ryujinx.Input.SDL3
public void SetTriggerThreshold(float triggerThreshold) public void SetTriggerThreshold(float triggerThreshold)
{ {
// No operations
} }
public bool HDRumble(VibrationValue left, VibrationValue right) public bool HDRumble(VibrationValue left, VibrationValue right)
@@ -170,10 +170,12 @@ namespace Ryujinx.Input.SDL3
return _hdRumble?.HdRumble(left, right) ?? false; 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) if ((Features & GamepadFeaturesFlag.Rumble) == 0)
return; {
return false;
}
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
@@ -192,6 +194,15 @@ namespace Ryujinx.Input.SDL3
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); 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) public Vector3 GetMotionData(MotionInputId inputId)

View File

@@ -1,4 +1,7 @@
using Gommon;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@@ -61,7 +64,14 @@ namespace Ryujinx.Input.SDL3
return left.IsPressed(inputId) || right.IsPressed(inputId); 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) if (lowFrequency != 0)
{ {
@@ -78,6 +88,15 @@ namespace Ryujinx.Input.SDL3
left.Rumble(0, 0, durationMs); left.Rumble(0, 0, durationMs);
right.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) public void SetConfiguration(InputConfig configuration)

View File

@@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
@@ -396,9 +397,14 @@ namespace Ryujinx.Input.SDL3
// No operations // 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) public Vector3 GetMotionData(MotionInputId inputId)

View File

@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Drawing; using System.Drawing;
using System.Numerics; using System.Numerics;
@@ -67,7 +68,12 @@ namespace Ryujinx.Input.SDL3
throw new NotImplementedException(); 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(); throw new NotImplementedException();
} }

View File

@@ -5,7 +5,6 @@ using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@@ -555,34 +554,37 @@ namespace Ryujinx.Input.HLE
{ {
if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue)) 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; return;
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}");
} }
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})");
} }
} }
} }

View File

@@ -80,10 +80,7 @@ namespace Ryujinx.Input
/// </summary> /// </summary>
/// <param name="left">The vibration data for the left side</param> /// <param name="left">The vibration data for the left side</param>
/// <param name="right">The vibration data for the right side</param> /// <param name="right">The vibration data for the right side</param>
bool HDRumble(VibrationValue left, VibrationValue right) bool HDRumble(VibrationValue left, VibrationValue right);
{
return false;
}
/// <summary> /// <summary>
/// Starts a rumble effect on the gamepad. /// Starts a rumble effect on the gamepad.
@@ -91,10 +88,10 @@ namespace Ryujinx.Input
/// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param> /// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param>
/// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param> /// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param>
/// <param name="durationMs">The duration of the rumble effect in milliseconds.</param> /// <param name="durationMs">The duration of the rumble effect in milliseconds.</param>
void Rumble(float lowFrequency, float highFrequency, uint durationMs); bool Rumble(float lowFrequency, float highFrequency, uint durationMs);
/// <summary> /// <summary>
/// Get a snaphost of the state of the gamepad that is remapped with the informations from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>. /// Get a snaphost of the state of the gamepad that is remapped with the information from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>.
/// </summary> /// </summary>
/// <returns>A remapped snaphost of the state of the gamepad.</returns> /// <returns>A remapped snaphost of the state of the gamepad.</returns>
GamepadStateSnapshot GetMappedStateSnapshot(); GamepadStateSnapshot GetMappedStateSnapshot();

View File

@@ -221,6 +221,7 @@ namespace Ryujinx.Headless
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
EnableRumble = false, EnableRumble = false,
UseHDRumble = true
}, },
}; };
} }

View File

@@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -149,9 +150,20 @@ namespace Ryujinx.Ava.Input
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard"); 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; public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero;

View File

@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Drawing; using System.Drawing;
@@ -64,8 +65,13 @@ namespace Ryujinx.Ava.Input
{ {
throw new NotImplementedException(); 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(); throw new NotImplementedException();
} }

View File

@@ -28,14 +28,14 @@
<PublishTrimmed>true</PublishTrimmed> <PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode> <TrimMode>partial</TrimMode>
</PropertyGroup> </PropertyGroup>
<!-- <!--
FluentAvalonia, used in the Avalonia UI, requires a workaround for the json serializer used internally when using .NET 8+ System.Text.Json. FluentAvalonia, used in the Avalonia UI, requires a workaround for the json serializer used internally when using .NET 8+ System.Text.Json.
See: See:
https://github.com/amwx/FluentAvalonia/issues/481 https://github.com/amwx/FluentAvalonia/issues/481
https://devblogs.microsoft.com/dotnet/system-text-json-in-dotnet-8/ https://devblogs.microsoft.com/dotnet/system-text-json-in-dotnet-8/
--> -->
<PropertyGroup> <PropertyGroup>
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault> <JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup> </PropertyGroup>
@@ -63,7 +63,7 @@
<PackageReference Include="OpenTK.Core" /> <PackageReference Include="OpenTK.Core" />
<PackageReference Include="Ryujinx.Audio.OpenAL" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" /> <PackageReference Include="Ryujinx.Audio.OpenAL" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" /> <PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'win-arm64'" /> <PackageReference Include="Ryujinx.Graphics.Vulkan.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'win-arm64'" />
<PackageReference Include="Ryujinx.UpdateClient" /> <PackageReference Include="Ryujinx.UpdateClient" />
<PackageReference Include="Ryujinx.Systems.Update.Common" /> <PackageReference Include="Ryujinx.Systems.Update.Common" />
<PackageReference Include="securifybv.ShellLink" /> <PackageReference Include="securifybv.ShellLink" />
@@ -73,7 +73,7 @@
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" /> <PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
<PackageReference Include="SPB" /> <PackageReference Include="SPB" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj" />

View File

@@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Systems.Configuration
/// <summary> /// <summary>
/// The current version of the file format /// The current version of the file format
/// </summary> /// </summary>
public const int CurrentVersion = 72; public const int CurrentVersion = 73;
/// <summary> /// <summary>
/// Version of the configuration file format /// Version of the configuration file format

View File

@@ -333,6 +333,7 @@ namespace Ryujinx.Ava.Systems.Configuration
EnableRumble = false, EnableRumble = false,
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
UseHDRumble = true
}; };
} }
} }

View File

@@ -20,6 +20,7 @@ namespace Ryujinx.Ava.UI.Models.Input
public float WeakRumble { get; set; } public float WeakRumble { get; set; }
public float StrongRumble { get; set; } public float StrongRumble { get; set; }
public bool UseHDRumble { get; set; }
public string Id { get; set; } public string Id { get; set; }
@@ -236,6 +237,7 @@ namespace Ryujinx.Ava.UI.Models.Input
EnableRumble = controllerInput.Rumble.EnableRumble; EnableRumble = controllerInput.Rumble.EnableRumble;
WeakRumble = controllerInput.Rumble.WeakRumble; WeakRumble = controllerInput.Rumble.WeakRumble;
StrongRumble = controllerInput.Rumble.StrongRumble; StrongRumble = controllerInput.Rumble.StrongRumble;
UseHDRumble = controllerInput.Rumble.UseHDRumble;
} }
if (controllerInput.Led != null) if (controllerInput.Led != null)
@@ -307,6 +309,7 @@ namespace Ryujinx.Ava.UI.Models.Input
EnableRumble = EnableRumble, EnableRumble = EnableRumble,
WeakRumble = WeakRumble, WeakRumble = WeakRumble,
StrongRumble = StrongRumble, StrongRumble = StrongRumble,
UseHDRumble = UseHDRumble,
}, },
Led = new LedConfigController Led = new LedConfigController
{ {

View File

@@ -789,6 +789,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
EnableRumble = false, EnableRumble = false,
UseHDRumble = true
}, },
}; };
} }

View File

@@ -9,5 +9,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
[ObservableProperty] [ObservableProperty]
public partial float WeakRumble { get; set; } public partial float WeakRumble { get; set; }
[ObservableProperty]
public partial bool EnableHDRumble { get; set; }
} }
} }

View File

@@ -53,6 +53,15 @@
Margin="5,0" Margin="5,0"
Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" /> Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" />
</StackPanel> </StackPanel>
<CheckBox
Margin="5"
IsChecked="{Binding EnableHDRumble}">
<TextBlock
Margin="0,3,0,0"
VerticalAlignment="Center"
Text="{ext:Locale ControllerSettingsRumbleUseHDRumble}"
ToolTip.Tip="{ext:Locale HDRumbleTooltip}" />
</CheckBox>
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -22,6 +22,7 @@ namespace Ryujinx.Ava.UI.Views.Input
{ {
StrongRumble = config.StrongRumble, StrongRumble = config.StrongRumble,
WeakRumble = config.WeakRumble, WeakRumble = config.WeakRumble,
EnableHDRumble = config.UseHDRumble
}; };
InitializeComponent(); InitializeComponent();
@@ -45,6 +46,7 @@ namespace Ryujinx.Ava.UI.Views.Input
GamepadInputConfig config = viewModel.Config; GamepadInputConfig config = viewModel.Config;
config.StrongRumble = content.ViewModel.StrongRumble; config.StrongRumble = content.ViewModel.StrongRumble;
config.WeakRumble = content.ViewModel.WeakRumble; config.WeakRumble = content.ViewModel.WeakRumble;
config.UseHDRumble = content.ViewModel.EnableHDRumble;
}; };
await contentDialog.ShowAsync(); await contentDialog.ShowAsync();