Skip to content

Commit

Permalink
Add option to change controller LED color (Ryubing#572)
Browse files Browse the repository at this point in the history
This allows the user to change the controller LED while using Ryujinx.
Useful for PS4 and PS5 controllers as an example.

You can also use a spectrum-cycling Rainbow color option, or turn the LED off for DualSense controllers.

---------

Co-authored-by: Evan Husted <greem@greemdev.net>
  • Loading branch information
2 people authored and SomeoneIsWorking committed Feb 1, 2025
1 parent 534ea15 commit 97d8720
Show file tree
Hide file tree
Showing 24 changed files with 389 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@
{
public class LedConfigController
{
/// <summary>
/// Packed RGB int of the color
/// </summary>
public uint LedColor { get; set; }

/// <summary>
/// Enable LED color changing by the emulator
/// </summary>
public bool EnableLed { get; set; }

/// <summary>
/// Ignores the color and disables the LED entirely.
/// </summary>
public bool TurnOffLed { get; set; }

/// <summary>
/// Ignores the color and uses the rainbow color functionality for the LED.
/// </summary>
public bool UseRainbow { get; set; }

/// <summary>
/// Packed RGB int of the color
/// </summary>
public uint LedColor { get; set; }
}
}
76 changes: 76 additions & 0 deletions src/Ryujinx.Common/Utilities/Rainbow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Drawing;

namespace Ryujinx.Common.Utilities
{
public class Rainbow
{
public const float Speed = 1;

public static Color Color { get; private set; } = Color.Blue;

public static void Tick()
{
Color = HsbToRgb(
(Color.GetHue() + Speed) / 360,
1,
1
);

RainbowColorUpdated?.Invoke(Color.ToArgb());
}

public static event Action<int> RainbowColorUpdated;

private static Color HsbToRgb(float hue, float saturation, float brightness)
{
int r = 0, g = 0, b = 0;
if (saturation == 0)
{
r = g = b = (int)(brightness * 255.0f + 0.5f);
}
else
{
float h = (hue - (float)Math.Floor(hue)) * 6.0f;
float f = h - (float)Math.Floor(h);
float p = brightness * (1.0f - saturation);
float q = brightness * (1.0f - saturation * f);
float t = brightness * (1.0f - (saturation * (1.0f - f)));
switch ((int)h)
{
case 0:
r = (int)(brightness * 255.0f + 0.5f);
g = (int)(t * 255.0f + 0.5f);
b = (int)(p * 255.0f + 0.5f);
break;
case 1:
r = (int)(q * 255.0f + 0.5f);
g = (int)(brightness * 255.0f + 0.5f);
b = (int)(p * 255.0f + 0.5f);
break;
case 2:
r = (int)(p * 255.0f + 0.5f);
g = (int)(brightness * 255.0f + 0.5f);
b = (int)(t * 255.0f + 0.5f);
break;
case 3:
r = (int)(p * 255.0f + 0.5f);
g = (int)(q * 255.0f + 0.5f);
b = (int)(brightness * 255.0f + 0.5f);
break;
case 4:
r = (int)(t * 255.0f + 0.5f);
g = (int)(p * 255.0f + 0.5f);
b = (int)(brightness * 255.0f + 0.5f);
break;
case 5:
r = (int)(brightness * 255.0f + 0.5f);
g = (int)(p * 255.0f + 0.5f);
b = (int)(q * 255.0f + 0.5f);
break;
}
}
return Color.FromArgb(Convert.ToByte(255), Convert.ToByte(r), Convert.ToByte(g), Convert.ToByte(b));
}
}
}
31 changes: 27 additions & 4 deletions src/Ryujinx.Input.SDL2/SDL2Gamepad.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Hid;
using SDL2;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -86,7 +88,7 @@ public SDL2Gamepad(nint gamepadHandle, string driverId)
Id = driverId;
Features = GetFeaturesFlag();
_triggerThreshold = 0.0f;

// Enable motion tracking
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
{
Expand All @@ -102,6 +104,18 @@ public SDL2Gamepad(nint gamepadHandle, string driverId)
}
}

public void SetLed(uint packedRgb)
{
if (!Features.HasFlag(GamepadFeaturesFlag.Led)) return;

byte red = packedRgb > 0 ? (byte)(packedRgb >> 16) : (byte)0;
byte green = packedRgb > 0 ? (byte)(packedRgb >> 8) : (byte)0;
byte blue = packedRgb > 0 ? (byte)(packedRgb % 256) : (byte)0;

if (SDL_GameControllerSetLED(_gamepadHandle, red, green, blue) != 0)
Logger.Error?.Print(LogClass.Hid, "LED is not supported on this game controller.");
}

private GamepadFeaturesFlag GetFeaturesFlag()
{
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
Expand All @@ -112,9 +126,7 @@ private GamepadFeaturesFlag GetFeaturesFlag()
result |= GamepadFeaturesFlag.Motion;
}

int error = SDL_GameControllerRumble(_gamepadHandle, 0, 0, 100);

if (error == 0)
if (SDL_GameControllerHasRumble(_gamepadHandle) == SDL_bool.SDL_TRUE)
{
result |= GamepadFeaturesFlag.Rumble;
}
Expand Down Expand Up @@ -220,6 +232,17 @@ public void SetConfiguration(InputConfig configuration)
{
_configuration = (StandardControllerInputConfig)configuration;

if (Features.HasFlag(GamepadFeaturesFlag.Led) && _configuration.Led.EnableLed)
{
if (_configuration.Led.TurnOffLed)
(this as IGamepad).ClearLed();
else if (_configuration.Led.UseRainbow)
Rainbow.RainbowColorUpdated += clr => SetLed((uint)clr);
else
SetLed(_configuration.Led.LedColor);

}

_buttonsUserMapping.Clear();

// First update sticks
Expand Down
11 changes: 11 additions & 0 deletions src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,16 @@ public IGamepad GetGamepad(string id)

return new SDL2Gamepad(gamepadHandle, id);
}

public IEnumerable<IGamepad> GetGamepads()
{
lock (_gamepadsIds)
{
foreach (string gamepadId in _gamepadsIds)
{
yield return GetGamepad(gamepadId);
}
}
}
}
}
6 changes: 6 additions & 0 deletions src/Ryujinx.Input.SDL2/SDL2Keyboard.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Numerics;
Expand Down Expand Up @@ -385,6 +386,11 @@ public void SetConfiguration(InputConfig configuration)
}
}

public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL2Keyboard");
}

public void SetTriggerThreshold(float triggerThreshold)
{
// No operations
Expand Down
6 changes: 6 additions & 0 deletions src/Ryujinx.Input.SDL2/SDL2Mouse.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using System;
using System.Drawing;
using System.Numerics;
Expand Down Expand Up @@ -76,6 +77,11 @@ public void SetConfiguration(InputConfig configuration)
throw new NotImplementedException();
}

public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL2Mouse");
}

public void SetTriggerThreshold(float triggerThreshold)
{
throw new NotImplementedException();
Expand Down
3 changes: 3 additions & 0 deletions src/Ryujinx.Input.SDL2/SDL2MouseDriver.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Numerics;
Expand Down Expand Up @@ -164,6 +165,8 @@ public IGamepad GetGamepad(string id)
return new SDL2Mouse(this);
}

public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];

public void Dispose()
{
if (_isDisposed)
Expand Down
9 changes: 9 additions & 0 deletions src/Ryujinx.Input.SDL2/SDLKeyboardDriver.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Ryujinx.SDL2.Common;
using System;
using System.Collections.Generic;

namespace Ryujinx.Input.SDL2
{
Expand Down Expand Up @@ -51,5 +52,13 @@ public IGamepad GetGamepad(string id)

return new SDL2Keyboard(this, _keyboardIdentifers[0], "All keyboards");
}

public IEnumerable<IGamepad> GetGamepads()
{
foreach (var keyboardId in _keyboardIdentifers)
{
yield return GetGamepad(keyboardId);
}
}
}
}
9 changes: 9 additions & 0 deletions src/Ryujinx.Input/IGamepad.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ public interface IGamepad : IDisposable
/// <param name="configuration">The configuration of the gamepad</param>
void SetConfiguration(InputConfig configuration);

/// <summary>
/// Set the LED on the gamepad to a given color.
/// </summary>
/// <remarks>Does nothing on a controller without LED functionality.</remarks>
/// <param name="packedRgb">The packed RGB integer.</param>
void SetLed(uint packedRgb);

public void ClearLed() => SetLed(0);

/// <summary>
/// Starts a rumble effect on the gamepad.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Ryujinx.Input/IGamepadDriver.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

namespace Ryujinx.Input
{
Expand Down Expand Up @@ -33,6 +34,11 @@ public interface IGamepadDriver : IDisposable
/// <param name="id">The unique id of the gamepad</param>
/// <returns>An instance of <see cref="IGamepad"/> associated to the gamepad id given or null if not found</returns>
IGamepad GetGamepad(string id);

/// <summary>
/// Returns an <see cref="IEnumerable{T}"/> of the connected gamepads.
/// </summary>
IEnumerable<IGamepad> GetGamepads();

/// <summary>
/// Clear the internal state of the driver.
Expand Down
3 changes: 3 additions & 0 deletions src/Ryujinx.SDL2.Common/SDL2Driver.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
Expand Down Expand Up @@ -167,6 +168,8 @@ private void EventWorker()
HandleSDLEvent(ref evnt);
}
});

Rainbow.Tick();

waitHandle.Wait(WaitTimeMs);
}
Expand Down
5 changes: 5 additions & 0 deletions src/Ryujinx/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,11 @@ private void Exit()
return;
}

foreach (IGamepad gamepad in RyujinxApp.MainWindow.InputManager.GamepadDriver.GetGamepads())
{
gamepad?.ClearLed();
}

_isStopped = true;
Stop();
}
Expand Down
52 changes: 51 additions & 1 deletion src/Ryujinx/Assets/locales.json
Original file line number Diff line number Diff line change
Expand Up @@ -7628,7 +7628,57 @@
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Custom LED",
"en_US": "LED",
"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": "ControllerSettingsLedColorDisable",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Disable",
"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": "ControllerSettingsLedColorRainbow",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Rainbow",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
Expand Down
Loading

0 comments on commit 97d8720

Please sign in to comment.