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>
+4 -3
View File
@@ -1,7 +1,7 @@
using System.Runtime.Versioning;
using System.Text;
using RioJoy.Core.Editing;
using RioJoy.Core.Hid;
using RioJoy.Core.Output;
using RioJoy.Core.Mapping;
using RioJoy.Core.Profiles;
using RioJoy.Core.Protocol;
@@ -262,8 +262,9 @@ public sealed class ProfileEditorForm : Form
_valueCombo.Items.Add(new ValueItem(k.Name, k.Value));
break;
case RioRouteKind.Joystick:
for (int btn = 1; btn <= RioHidReport.ButtonCount; btn++)
_valueCombo.Items.Add(new ValueItem($"Button {btn}", (byte)btn));
// The signed ViGEmBus Xbox 360 pad exposes 11 buttons; show their names.
for (int btn = 1; btn <= ViGEmJoystickSink.MappableButtonCount; btn++)
_valueCombo.Items.Add(new ValueItem($"Button {btn} ({ViGEmJoystickSink.ButtonNames[btn - 1]})", (byte)btn));
break;
case RioRouteKind.Hat:
foreach (RioHat h in new[] { RioHat.Up, RioHat.Right, RioHat.Down, RioHat.Left })
+29 -24
View File
@@ -146,6 +146,30 @@ public sealed class RioCoordinator : IDisposable
GoDormant("Dormant"); // the next watcher poll re-activates if a profiled game is foreground
}
// Pick the best available virtual-joystick sink and record it for disposal: the
// signed ViGEmBus Xbox 360 pad first, then the (test-signed) RioGamepad HID
// feeder, then a no-op when neither driver is present.
private IJoystickSink CreateJoystickSink(out string note)
{
if (ViGEmJoystickSink.TryCreate(out ViGEmJoystickSink? vigem))
{
_joystick = vigem;
note = string.Empty;
return vigem!;
}
if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? feeder))
{
_joystick = feeder;
note = string.Empty;
return feeder!;
}
_joystick = null;
note = " [no joystick driver]";
return new NullJoystickSink();
}
// routeInput=false holds the RIO link (and lamp feedback) but sends keyboard/
// mouse/joystick output to no-op sinks — used for the editor's live button check.
private void Activate(RioProfile profile, bool routeInput = true)
@@ -166,41 +190,22 @@ public sealed class RioCoordinator : IDisposable
IInputSink input;
IJoystickSink joystick;
string note;
IJoystickSink realJoystick = CreateJoystickSink(out string joystickNote);
if (!routeInput)
{
IJoystickSink realJoystick;
if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? editorFeeder))
{
realJoystick = editorFeeder!;
_joystick = editorFeeder;
}
else
{
realJoystick = new NullJoystickSink();
_joystick = null;
}
// Keyboard/mouse + joystick are gated off while editing so the RIO can
// be exercised without injecting input (the editor toggle opens them).
_editorInput = new GatedInputSink(new SendInputSink());
_editorJoystick = new GatedJoystickSink(realJoystick);
input = _editorInput;
joystick = _editorJoystick;
note = " [editing — outputs off]";
}
else if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? feeder))
{
// Use the real virtual-joystick driver if installed.
input = new SendInputSink();
joystick = feeder!;
_joystick = feeder;
note = string.Empty;
}
else
{
// Otherwise fall back to a no-op joystick (keyboard/mouse still work).
input = new SendInputSink();
joystick = new NullJoystickSink();
_joystick = null;
note = " [no joystick driver]";
joystick = realJoystick;
note = joystickNote;
}
_transport = _transportFactory(port);