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:
@@ -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 0–11: axes X,Y,Z,Rx,Ry,Rz — unsigned 16-bit little-endian</item>
|
||||
/// <item>byte 12: low nibble = hat (0–3; 0x0F = centered/null)</item>
|
||||
/// <item>bytes 13–24: 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user