diff --git a/src/RioJoy.Core/Output/ViGEmJoystickSink.cs b/src/RioJoy.Core/Output/ViGEmJoystickSink.cs
new file mode 100644
index 0000000..a40849d
--- /dev/null
+++ b/src/RioJoy.Core/Output/ViGEmJoystickSink.cs
@@ -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;
+
+///
+/// backed by a signed ViGEmBus 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
+/// are dropped (map those to the keyboard).
+///
+[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,
+ };
+
+ /// Display names for the mappable buttons, aligned with .
+ public static IReadOnlyList ButtonNames { get; } = new[]
+ {
+ "A", "B", "X", "Y", "LB", "RB", "Back", "Start", "L3", "R3", "Guide",
+ };
+
+ /// How many RIO joystick buttons map onto the Xbox 360 pad (11).
+ 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;
+ }
+
+ ///
+ /// Try to connect to ViGEmBus and create a virtual Xbox 360 controller. Returns
+ /// if ViGEmBus is not installed, so callers can fall back
+ /// to another joystick sink.
+ ///
+ 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();
+ }
+}
diff --git a/src/RioJoy.Core/RioJoy.Core.csproj b/src/RioJoy.Core/RioJoy.Core.csproj
index 6420a8f..b2e5387 100644
--- a/src/RioJoy.Core/RioJoy.Core.csproj
+++ b/src/RioJoy.Core/RioJoy.Core.csproj
@@ -11,6 +11,7 @@
+
diff --git a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
index 78d0795..ba45ced 100644
--- a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
+++ b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs
@@ -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 })
diff --git a/src/RioJoy.Tray/RioCoordinator.cs b/src/RioJoy.Tray/RioCoordinator.cs
index 8cc8e93..4ce6da1 100644
--- a/src/RioJoy.Tray/RioCoordinator.cs
+++ b/src/RioJoy.Tray/RioCoordinator.cs
@@ -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);