Add signed ViGEmBus virtual-joystick support (Xbox 360)

Replace the test-signed RioGamepad driver as the default joystick output with a
ViGEmBus virtual Xbox 360 controller (Nefarius.ViGEm.Client) — properly signed,
so it installs with no test-signing / Secure Boot change / reboot.

The coordinator now selects ViGEm first, then the RioGamepad HID feeder, then a
no-op. The XInput layout caps joystick buttons at 11, so the editor's joystick
picker lists the named Xbox buttons (A, B, X, Y, LB, RB, Back, Start, L3, R3,
Guide); the hat maps to the D-pad and the six axes to the sticks + triggers.
Bind anything beyond 11 to the keyboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-06-29 09:28:48 -05:00
co-authored by Claude Opus 4.8
parent 6ad180df89
commit 2f434517b7
4 changed files with 163 additions and 27 deletions
+129
View File
@@ -0,0 +1,129 @@
using System.Runtime.Versioning;
using Nefarius.ViGEm.Client;
using Nefarius.ViGEm.Client.Targets;
using Nefarius.ViGEm.Client.Targets.Xbox360;
using RioJoy.Core.Calibration;
using RioJoy.Core.Mapping;
namespace RioJoy.Core.Output;
/// <summary>
/// <see cref="IJoystickSink"/> backed by a <b>signed ViGEmBus</b> virtual Xbox 360
/// controller. Properly signed — installs with no test-signing / Secure-Boot
/// changes — at the cost of the XInput layout: 11 buttons, two sticks + two
/// triggers, and the D-pad for the hat. RIO joystick buttons beyond
/// <see cref="MappableButtonCount"/> are dropped (map those to the keyboard).
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class ViGEmJoystickSink : IJoystickSink, IDisposable
{
// RIO joystick button (1-based) -> Xbox 360 button, in a natural order.
private static readonly Xbox360Button[] ButtonMap =
{
Xbox360Button.A, Xbox360Button.B, Xbox360Button.X, Xbox360Button.Y,
Xbox360Button.LeftShoulder, Xbox360Button.RightShoulder,
Xbox360Button.Back, Xbox360Button.Start,
Xbox360Button.LeftThumb, Xbox360Button.RightThumb,
Xbox360Button.Guide,
};
/// <summary>Display names for the mappable buttons, aligned with <see cref="ButtonMap"/>.</summary>
public static IReadOnlyList<string> ButtonNames { get; } = new[]
{
"A", "B", "X", "Y", "LB", "RB", "Back", "Start", "L3", "R3", "Guide",
};
/// <summary>How many RIO joystick buttons map onto the Xbox 360 pad (11).</summary>
public static int MappableButtonCount => ButtonMap.Length;
private readonly ViGEmClient _client;
private readonly IXbox360Controller _pad;
private readonly object _gate = new();
private ViGEmJoystickSink(ViGEmClient client, IXbox360Controller pad)
{
_client = client;
_pad = pad;
}
/// <summary>
/// Try to connect to ViGEmBus and create a virtual Xbox 360 controller. Returns
/// <see langword="false"/> if ViGEmBus is not installed, so callers can fall back
/// to another joystick sink.
/// </summary>
public static bool TryCreate(out ViGEmJoystickSink? sink)
{
sink = null;
try
{
var client = new ViGEmClient();
IXbox360Controller pad = client.CreateXbox360Controller();
pad.AutoSubmitReport = false; // submit once per logical update
pad.Connect();
sink = new ViGEmJoystickSink(client, pad);
return true;
}
catch
{
// VigemBusNotFoundException (driver absent) or any connect failure.
return false;
}
}
public void SetButton(int button, bool pressed)
{
int index = button - 1; // RIO joystick buttons are 1-based
if (index < 0 || index >= ButtonMap.Length)
return; // beyond the Xbox 360 layout — bind these to the keyboard instead
lock (_gate)
{
_pad.SetButtonState(ButtonMap[index], pressed);
_pad.SubmitReport();
}
}
public void SetHat(RioHat position)
{
lock (_gate)
{
_pad.SetButtonState(Xbox360Button.Up, position == RioHat.Up);
_pad.SetButtonState(Xbox360Button.Right, position == RioHat.Right);
_pad.SetButtonState(Xbox360Button.Down, position == RioHat.Down);
_pad.SetButtonState(Xbox360Button.Left, position == RioHat.Left);
_pad.SubmitReport();
}
}
public void SetAxis(JoyAxis axis, int value)
{
lock (_gate)
{
switch (axis)
{
case JoyAxis.X: _pad.SetAxisValue(Xbox360Axis.LeftThumbX, ToThumb(value)); break;
case JoyAxis.Y: _pad.SetAxisValue(Xbox360Axis.LeftThumbY, ToThumb(value)); break;
case JoyAxis.Rx: _pad.SetAxisValue(Xbox360Axis.RightThumbX, ToThumb(value)); break;
case JoyAxis.Ry: _pad.SetAxisValue(Xbox360Axis.RightThumbY, ToThumb(value)); break;
case JoyAxis.Z: _pad.SetSliderValue(Xbox360Slider.LeftTrigger, ToTrigger(value)); break;
case JoyAxis.Rz: _pad.SetSliderValue(Xbox360Slider.RightTrigger, ToTrigger(value)); break;
default: return;
}
_pad.SubmitReport();
}
}
// RIO axis 0..32766 (centre 16383) -> Xbox thumb short -32768..32767.
private static short ToThumb(int value) =>
(short)Math.Clamp((value - AxisOutputs.Center) * 2, short.MinValue, short.MaxValue);
// RIO axis 0..32766 -> Xbox trigger byte 0..255.
private static byte ToTrigger(int value) =>
(byte)Math.Clamp(value * 255 / AxisOutputs.Max, 0, 255);
public void Dispose()
{
try { _pad.Disconnect(); } catch { /* already disconnected / bus gone */ }
_client.Dispose();
}
}
+1
View File
@@ -11,6 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nefarius.ViGEm.Client" Version="1.21.256" />
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
</ItemGroup>