Files
VRIO/src/VRio.App/XInputGamepad.cs
T
CydandClaude Fable 5 e590b89c47 Keyboard and Xbox-gamepad input drive the panel
New input pipeline: sources feed an InputRouter (VRio.Core.Input) that
calls the same VRioDevice press/release/axis entry points the mouse
canvas uses, so routed input is indistinguishable on the wire.

- Bindings live in %APPDATA%\vRIO\bindings.txt (plain text, commented
  defaults written on first run; Reload/Edit buttons in the new Input
  group). Keys and pad buttons press any RIO address, momentary or
  toggle; axes bind in normalized units of each axis realistic travel
  window with deflect (spring-back), rate (position holds), deadzone,
  and invert options - RioAxisRange supplies the wire signs.
- Router suppresses key auto-repeat, edge-detects pad buttons, and
  hold-counts per address so overlapping sources press once and
  release last; axes only write when the composed value changes, so
  mouse drags keep working while sources idle.
- Xbox pad via a zero-dependency XInput P/Invoke poller (xinput1_4,
  fallback xinput9_1_0), throttled rescan while disconnected.
- MainForm intercepts bound keys in ProcessCmdKey so arrows/space
  reach the panel instead of moving focus; keys release on focus
  loss; center-axes and host ResetRequest reset the rate integrators.
- Canvas highlights router-held addresses like clicks.
- Defaults: arrows=stick, W/S=throttle, Q/E=pedals, numpad=internal
  keypad, IJKL/Space=hat+main; pad left stick/triggers/right stick =
  stick/pedals/throttle-rate, ABXY/dpad/shoulders = named buttons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:30:55 -05:00

121 lines
3.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.InteropServices;
using VRio.Core.Input;
namespace VRio.App;
/// <summary>
/// Polls the first connected XInput (Xbox) controller and converts its state
/// to the Core's normalized <see cref="PadState"/>. Uses the in-box
/// xinput1_4.dll (Windows 8+), falling back to xinput9_1_0.dll, so no
/// redistributable is needed. Polling a disconnected user index is expensive,
/// so while no pad is present the 03 scan only runs every ~1 s.
/// </summary>
internal sealed class XInputGamepad
{
private const int MaxUsers = 4;
private const uint ErrorSuccess = 0;
private const int ScanEveryNPolls = 60; // ≈1 s at the 16 ms input tick
private static bool _tryXInput14 = true;
private int _user = -1;
private int _scanCountdown;
/// <summary>True while a controller is connected and being polled.</summary>
public bool Connected => _user >= 0;
/// <summary>XInput user index of the connected controller (0-based).</summary>
public int UserIndex => _user;
/// <summary>
/// Poll the pad. False (with a rest-state snapshot) while disconnected.
/// </summary>
public bool TryRead(out PadState state)
{
if (_user >= 0)
{
if (GetState(_user, out XINPUT_STATE s) == ErrorSuccess)
{
state = Convert(s.Gamepad);
return true;
}
_user = -1; // unplugged; fall through to (throttled) rescan
_scanCountdown = 0;
}
if (--_scanCountdown <= 0)
{
_scanCountdown = ScanEveryNPolls;
for (int i = 0; i < MaxUsers; i++)
{
if (GetState(i, out XINPUT_STATE s) == ErrorSuccess)
{
_user = i;
state = Convert(s.Gamepad);
return true;
}
}
}
state = default;
return false;
}
private static PadState Convert(in XINPUT_GAMEPAD g) => new(
(PadButtons)g.wButtons,
Thumb(g.sThumbLX), Thumb(g.sThumbLY),
Thumb(g.sThumbRX), Thumb(g.sThumbRY),
g.bLeftTrigger / 255f, g.bRightTrigger / 255f);
private static float Thumb(short v) => Math.Max(-1f, v / 32767f);
private static uint GetState(int user, out XINPUT_STATE state)
{
if (_tryXInput14)
{
try
{
return XInputGetState14(user, out state);
}
catch (DllNotFoundException)
{
_tryXInput14 = false;
}
}
try
{
return XInputGetState910(user, out state);
}
catch (DllNotFoundException)
{
state = default;
return uint.MaxValue; // no XInput on this system — behaves as "no pad"
}
}
[DllImport("xinput1_4.dll", EntryPoint = "XInputGetState")]
private static extern uint XInputGetState14(int dwUserIndex, out XINPUT_STATE state);
[DllImport("xinput9_1_0.dll", EntryPoint = "XInputGetState")]
private static extern uint XInputGetState910(int dwUserIndex, out XINPUT_STATE state);
[StructLayout(LayoutKind.Sequential)]
private struct XINPUT_STATE
{
public uint dwPacketNumber;
public XINPUT_GAMEPAD Gamepad;
}
[StructLayout(LayoutKind.Sequential)]
private struct XINPUT_GAMEPAD
{
public ushort wButtons;
public byte bLeftTrigger;
public byte bRightTrigger;
public short sThumbLX;
public short sThumbLY;
public short sThumbRX;
public short sThumbRY;
}
}