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
@@ -0,0 +1,85 @@
using RioJoy.Core.Calibration;
using RioJoy.Core.Hid;
using RioJoy.Core.Mapping;
using Xunit;
namespace RioJoy.Core.Tests.Hid;
public class RioHidReportTests
{
[Fact]
public void DefaultReport_CentersAxesAndHat()
{
var report = new RioHidReport();
byte[] b = report.ToArray();
Assert.Equal(RioHidReport.Size, b.Length);
// Each axis = 16383 = 0x3FFF, little-endian.
for (int offset = 0; offset < 12; offset += 2)
{
Assert.Equal(0xFF, b[offset]);
Assert.Equal(0x3F, b[offset + 1]);
}
Assert.Equal(0x0F, b[12] & 0x0F); // hat centered/null
}
[Fact]
public void SetAxis_WritesLittleEndian_AndClamps()
{
var report = new RioHidReport();
report.SetAxis(JoyAxis.Z, 0x1234);
Assert.Equal(0x34, report.Bytes[4]);
Assert.Equal(0x12, report.Bytes[5]);
report.SetAxis(JoyAxis.Z, 70000); // over max → clamped to 32767
Assert.Equal(0xFF, report.Bytes[4]);
Assert.Equal(0x7F, report.Bytes[5]);
}
[Theory]
[InlineData(RioHat.Up, 0x00)]
[InlineData(RioHat.Right, 0x01)]
[InlineData(RioHat.Down, 0x02)]
[InlineData(RioHat.Left, 0x03)]
[InlineData(RioHat.Centered, 0x0F)]
public void SetHat_EncodesNibble(RioHat hat, int expected)
{
var report = new RioHidReport();
report.SetHat(hat);
Assert.Equal(expected, report.Bytes[12] & 0x0F);
}
[Theory]
[InlineData(1, 13, 0x01)] // button 1 → byte 13 bit 0
[InlineData(8, 13, 0x80)] // button 8 → byte 13 bit 7
[InlineData(9, 14, 0x01)] // button 9 → byte 14 bit 0
[InlineData(96, 24, 0x80)] // button 96 → byte 24 bit 7
public void SetButton_SetsCorrectBit(int button, int byteIndex, int mask)
{
var report = new RioHidReport();
report.SetButton(button, pressed: true);
Assert.Equal(mask, report.Bytes[byteIndex]);
report.SetButton(button, pressed: false);
Assert.Equal(0, report.Bytes[byteIndex]);
}
[Fact]
public void SetButton_RejectsOutOfRange()
{
var report = new RioHidReport();
Assert.Throws<ArgumentOutOfRangeException>(() => report.SetButton(0, true));
Assert.Throws<ArgumentOutOfRangeException>(() => report.SetButton(97, true));
}
[Fact]
public void Buttons_ComposeIndependently()
{
var report = new RioHidReport();
report.SetButton(1, true);
report.SetButton(2, true);
report.SetButton(1, false);
Assert.Equal(0x02, report.Bytes[13]); // only button 2 remains
}
}