Phase 1: RioGamepad virtual HID driver (KMDF + VHF) + C# report packer

Author the custom virtual HID gamepad that replaces vJoy, and pin its wire
format on both sides. Builds clean to RioGamepad.sys against the EWDK
(KMDF 1.15 + VHF, x64, warnings-as-errors).

driver/RioGamepad/:
- ReportDescriptor.h: 6x16-bit axes (X,Y,Z,Rx,Ry,Rz), one 4-direction hat with
  null state, and 96 buttons — the legacy vJoy layout. 25-byte input report.
- Device.c/Driver.c: KMDF root-enumerated device that creates the VHF virtual HID
  device (VhfCreate in DeviceAdd, VhfStart in D0Entry, VhfDelete on cleanup) and
  exposes a device interface + IOCTL_RIO_SUBMIT_REPORT that forwards the caller's
  report bytes to VhfReadReportSubmit. Thin relay: no report logic in the kernel.
- Public.h: device-interface GUID, IOCTL, and the report byte layout shared with
  the C# client. RioGamepad.inf + build.cmd (EWDK build, catalog/sign disabled).

src/RioJoy.Core/Hid/RioHidReport.cs: packs AxisOutputs + hat + 96 buttons into
the exact 25-byte report (LE axes, hat nibble with 0x0F=centered, button bitmap).
13 new xUnit tests (136 total).

Remaining (deploy-side): test-sign + pnputil install + verify in joy.cpl, and
wire the real DeviceIoControl feeder sink (replacing NullJoystickSink). The
EWDK's in-build catalog task (DrvCat) can't load Microsoft.Kits.Logger on this
image, so the .cat is produced with inf2cat/signtool at install time instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-06-26 21:06:16 -05:00
co-authored by Claude Opus 4.8
parent 1348040e1c
commit 24cdf495e3
13 changed files with 664 additions and 15 deletions
+85
View File
@@ -0,0 +1,85 @@
using RioJoy.Core.Calibration;
using RioJoy.Core.Mapping;
namespace RioJoy.Core.Hid;
/// <summary>
/// Builds the fixed 25-byte input report consumed by the RioGamepad driver
/// (see <c>driver/RioGamepad/Public.h</c>). Layout:
/// <list type="bullet">
/// <item>bytes 011: axes X,Y,Z,Rx,Ry,Rz — unsigned 16-bit little-endian</item>
/// <item>byte 12: low nibble = hat (03; 0x0F = centered/null)</item>
/// <item>bytes 1324: 96 button bits (button N at byte 13 + (N-1)/8, bit (N-1)%8)</item>
/// </list>
/// The instance holds current state so successive axis/button/hat updates compose
/// into one report, mirroring how a gamepad reports its whole state each time.
/// </summary>
public sealed class RioHidReport
{
/// <summary>Report size in bytes (must match the driver's RIO_REPORT_SIZE).</summary>
public const int Size = 25;
/// <summary>Number of buttons in the report.</summary>
public const int ButtonCount = 96;
/// <summary>Logical maximum for an axis (HID descriptor logical max).</summary>
public const int AxisMax = 32767;
private const int HatByte = 12;
private const int ButtonsOffset = 13;
private const byte HatCentered = 0x0F;
private readonly byte[] _buffer = new byte[Size];
public RioHidReport()
{
// Center the axes and center the hat at rest.
SetAxis(JoyAxis.X, AxisOutputs.Center);
SetAxis(JoyAxis.Y, AxisOutputs.Center);
SetAxis(JoyAxis.Z, AxisOutputs.Center);
SetAxis(JoyAxis.Rx, AxisOutputs.Center);
SetAxis(JoyAxis.Ry, AxisOutputs.Center);
SetAxis(JoyAxis.Rz, AxisOutputs.Center);
SetHat(RioHat.Centered);
}
/// <summary>The current report bytes (length <see cref="Size"/>).</summary>
public ReadOnlySpan<byte> Bytes => _buffer;
/// <summary>Copy of the current report bytes.</summary>
public byte[] ToArray() => (byte[])_buffer.Clone();
/// <summary>Set an axis value (clamped to 0..<see cref="AxisMax"/>), little-endian.</summary>
public void SetAxis(JoyAxis axis, int value)
{
ushort v = (ushort)Math.Clamp(value, 0, AxisMax);
int offset = (int)axis * 2;
_buffer[offset] = (byte)(v & 0xFF);
_buffer[offset + 1] = (byte)(v >> 8);
}
/// <summary>Set the POV hat position (<see cref="RioHat.Centered"/> = null).</summary>
public void SetHat(RioHat position)
{
byte nibble = position == RioHat.Centered ? HatCentered : (byte)((int)position & 0x0F);
_buffer[HatByte] = (byte)((_buffer[HatByte] & 0xF0) | nibble);
}
/// <summary>
/// Set a button's state. <paramref name="button"/> is the 1-based button
/// number (1..96), matching the legacy vJoy <c>SetBtn</c> numbering.
/// </summary>
public void SetButton(int button, bool pressed)
{
if (button < 1 || button > ButtonCount)
throw new ArgumentOutOfRangeException(nameof(button), $"Button must be 1..{ButtonCount}.");
int bit = button - 1;
int index = ButtonsOffset + (bit / 8);
byte mask = (byte)(1 << (bit % 8));
if (pressed)
_buffer[index] |= mask;
else
_buffer[index] &= (byte)~mask;
}
}