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:
@@ -35,9 +35,11 @@ dotnet test RioJoy.sln
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Phases 2–5 are code-complete and unit-tested (123 tests): serial + RIO protocol
|
Phases 1–5 are implemented and tested (136 unit tests): the `RioGamepad` virtual
|
||||||
core, input mapping + output routing, axis calibration + plasma display, and the
|
HID driver compiles to `.sys` against the EWDK (KMDF + VHF), and the C# side
|
||||||
tray app + profiles (JSON config, `RIO.ini` importer, the three-state auto-switch
|
covers the serial + RIO protocol core, input mapping + output routing, axis
|
||||||
watcher, and the `RioRuntime` that wires it together). The virtual-HID feeder and
|
calibration + plasma display, the tray app + profiles (JSON config, `RIO.ini`
|
||||||
on-cabinet verification are pending on the Phase 1 driver. See
|
importer, three-state auto-switch), and the HID report packer that matches the
|
||||||
|
driver's wire format. Remaining work is **deploy-side**: test-sign + install the
|
||||||
|
driver, wire the real `DeviceIoControl` feeder, and verify on a cabinet. See
|
||||||
[`docs/PLAN.md`](docs/PLAN.md) for the full roadmap.
|
[`docs/PLAN.md`](docs/PLAN.md) for the full roadmap.
|
||||||
|
|||||||
+13
-5
@@ -106,12 +106,20 @@ tray menu is always available.
|
|||||||
(tray app); `driver/` placeholder for the WDK project.
|
(tray app); `driver/` placeholder for the WDK project.
|
||||||
- This plan + [PROTOCOL.md](PROTOCOL.md).
|
- This plan + [PROTOCOL.md](PROTOCOL.md).
|
||||||
|
|
||||||
### Phase 1 — Virtual HID driver (highest risk; do first)
|
### Phase 1 — Virtual HID driver — compiling ✅ (sign/install pending)
|
||||||
|
Implemented in [`driver/RioGamepad/`](../driver/RioGamepad/); builds to
|
||||||
|
`RioGamepad.sys` against the EWDK (KMDF 1.15 + VHF, x64, warnings-as-errors).
|
||||||
- KMDF + VHF virtual HID gamepad: report descriptor = 6×16-bit axes, 1 hat,
|
- KMDF + VHF virtual HID gamepad: report descriptor = 6×16-bit axes, 1 hat,
|
||||||
96 buttons.
|
96 buttons ([`ReportDescriptor.h`](../driver/RioGamepad/ReportDescriptor.h)).
|
||||||
- Control device + custom IOCTL → `VhfReadReportSubmit`.
|
- Device interface + custom `IOCTL_RIO_SUBMIT_REPORT` → `VhfReadReportSubmit`; the
|
||||||
- Test harness (throwaway C#) wiggles axes/buttons; verify in `joy.cpl`.
|
driver is a thin relay, with the 25-byte report layout pinned in
|
||||||
- Test-signing setup for the cabinets.
|
[`Public.h`](../driver/RioGamepad/Public.h).
|
||||||
|
- C# side of the contract: `RioJoy.Core.Hid.RioHidReport` packs axes/hat/buttons
|
||||||
|
into that exact report (unit-tested). Replaces the throwaway test harness idea.
|
||||||
|
- ⏳ **Remaining (Phase 6 / on-cabinet):** test-sign + `pnputil` install + verify
|
||||||
|
in `joy.cpl`; wire the real `DeviceIoControl` feeder sink (replacing
|
||||||
|
`NullJoystickSink`). The EWDK's in-build catalog/sign tasks are bypassed; the
|
||||||
|
`.cat` is made with `inf2cat`/`signtool` at install time.
|
||||||
|
|
||||||
### Phase 2 — Serial + RIO protocol core (`RioJoy.Core`) — code-complete ✅
|
### Phase 2 — Serial + RIO protocol core (`RioJoy.Core`) — code-complete ✅
|
||||||
Implemented in `src/RioJoy.Core/Protocol` + `Serial`, covered by
|
Implemented in `src/RioJoy.Core/Protocol` + `Serial`, covered by
|
||||||
|
|||||||
+27
-5
@@ -17,13 +17,35 @@ to Windows via `VhfReadReportSubmit`.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Phase 0 placeholder. Implementation is **Phase 1** in [../docs/PLAN.md](../docs/PLAN.md).
|
**Phase 1 — implemented and compiling.** The driver source under
|
||||||
|
[`RioGamepad/`](RioGamepad/) builds cleanly to `RioGamepad.sys` against the EWDK
|
||||||
|
(KMDF 1.15 + VHF, x64). It is a thin VHF relay: it creates the virtual HID device
|
||||||
|
from the report descriptor and, on each `IOCTL_RIO_SUBMIT_REPORT`, forwards the
|
||||||
|
caller's 25-byte report to `VhfReadReportSubmit`. All report packing lives in the
|
||||||
|
C# client (`RioJoy.Core.Hid.RioHidReport`, unit-tested) so the wire format is
|
||||||
|
pinned on both sides ([`RioGamepad/Public.h`](RioGamepad/Public.h)).
|
||||||
|
|
||||||
## Build prerequisites (Phase 1)
|
Not yet done: **test-signing + install + verify in `joy.cpl`** (the on-cabinet
|
||||||
|
step), and wiring the real `DeviceIoControl` feeder sink (replacing the C# side's
|
||||||
|
`NullJoystickSink`).
|
||||||
|
|
||||||
- Windows Driver Kit (WDK) for Windows 11 + matching Visual Studio + Windows SDK
|
## Building
|
||||||
- A separate WDK/MSBuild project lives here (`RioGamepad.vcxproj`); it is **not**
|
|
||||||
part of `RioJoy.sln` (different toolchain).
|
With the **EWDK** mounted (e.g. drive `E:`), from this folder:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
RioGamepad\build.cmd E:
|
||||||
|
```
|
||||||
|
|
||||||
|
This sources the EWDK env (`<EWDK>\BuildEnv\SetupBuildEnv.cmd`) and runs MSBuild,
|
||||||
|
producing `RioGamepad\x64\Release\RioGamepad.sys`. The project is a WDK/MSBuild
|
||||||
|
`.vcxproj`; it is **not** part of `RioJoy.sln` (different toolchain).
|
||||||
|
|
||||||
|
> **EWDK note:** the build disables the managed catalog task
|
||||||
|
> (`/p:DriverCatalog_Enable=false`) and auto-signing (`/p:SignMode=Off`). On this
|
||||||
|
> EWDK image the in-build `DrvCat` task can't load `Microsoft.Kits.Logger`, so the
|
||||||
|
> `.cat` is produced separately with `inf2cat.exe` and signed with `signtool.exe`
|
||||||
|
> as part of install (below), rather than during compilation.
|
||||||
|
|
||||||
## Signing
|
## Signing
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/*++
|
||||||
|
RioGamepad — device creation, VHF setup, and the report-submit IOCTL.
|
||||||
|
|
||||||
|
The driver is a thin relay: it creates a virtual HID device via VHF using the
|
||||||
|
report descriptor in ReportDescriptor.h, exposes a device interface, and on each
|
||||||
|
IOCTL_RIO_SUBMIT_REPORT forwards the caller's report bytes to the HID stack with
|
||||||
|
VhfReadReportSubmit. All report packing lives in the user-mode client.
|
||||||
|
--*/
|
||||||
|
|
||||||
|
#include <ntddk.h>
|
||||||
|
#include <initguid.h> // makes DEFINE_GUID in Public.h allocate the GUID storage
|
||||||
|
#include "Driver.h"
|
||||||
|
#include "ReportDescriptor.h"
|
||||||
|
|
||||||
|
NTSTATUS
|
||||||
|
RioGamepadEvtDeviceAdd(
|
||||||
|
_In_ WDFDRIVER Driver,
|
||||||
|
_Inout_ PWDFDEVICE_INIT DeviceInit
|
||||||
|
)
|
||||||
|
{
|
||||||
|
NTSTATUS status;
|
||||||
|
WDFDEVICE device;
|
||||||
|
WDF_OBJECT_ATTRIBUTES deviceAttributes;
|
||||||
|
WDF_PNPPOWER_EVENT_CALLBACKS pnpPowerCallbacks;
|
||||||
|
WDF_IO_QUEUE_CONFIG queueConfig;
|
||||||
|
PDEVICE_CONTEXT deviceContext;
|
||||||
|
VHF_CONFIG vhfConfig;
|
||||||
|
|
||||||
|
UNREFERENCED_PARAMETER(Driver);
|
||||||
|
|
||||||
|
WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);
|
||||||
|
pnpPowerCallbacks.EvtDeviceD0Entry = RioGamepadEvtDeviceD0Entry;
|
||||||
|
WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &pnpPowerCallbacks);
|
||||||
|
|
||||||
|
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
|
||||||
|
deviceAttributes.EvtCleanupCallback = RioGamepadEvtDeviceCleanup;
|
||||||
|
|
||||||
|
status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
|
||||||
|
if (!NT_SUCCESS(status))
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceContext = DeviceGetContext(device);
|
||||||
|
deviceContext->VhfHandle = NULL;
|
||||||
|
|
||||||
|
status = WdfDeviceCreateDeviceInterface(device, &GUID_DEVINTERFACE_RIOGAMEPAD, NULL);
|
||||||
|
if (!NT_SUCCESS(status))
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Default parallel queue for the report-submit IOCTL.
|
||||||
|
//
|
||||||
|
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queueConfig, WdfIoQueueDispatchParallel);
|
||||||
|
queueConfig.EvtIoDeviceControl = RioGamepadEvtIoDeviceControl;
|
||||||
|
|
||||||
|
status = WdfIoQueueCreate(device, &queueConfig, WDF_NO_OBJECT_ATTRIBUTES, NULL);
|
||||||
|
if (!NT_SUCCESS(status))
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create the virtual HID device. VhfStart is deferred to D0Entry.
|
||||||
|
//
|
||||||
|
VHF_CONFIG_INIT(
|
||||||
|
&vhfConfig,
|
||||||
|
WdfDeviceWdmGetDeviceObject(device),
|
||||||
|
(USHORT)sizeof(g_RioReportDescriptor),
|
||||||
|
(PUCHAR)g_RioReportDescriptor);
|
||||||
|
|
||||||
|
vhfConfig.VendorID = RIO_VENDOR_ID;
|
||||||
|
vhfConfig.ProductID = RIO_PRODUCT_ID;
|
||||||
|
vhfConfig.VersionNumber = RIO_VERSION;
|
||||||
|
|
||||||
|
status = VhfCreate(&vhfConfig, &deviceContext->VhfHandle);
|
||||||
|
if (!NT_SUCCESS(status))
|
||||||
|
{
|
||||||
|
deviceContext->VhfHandle = NULL;
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
return STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
NTSTATUS
|
||||||
|
RioGamepadEvtDeviceD0Entry(
|
||||||
|
_In_ WDFDEVICE Device,
|
||||||
|
_In_ WDF_POWER_DEVICE_STATE PreviousState
|
||||||
|
)
|
||||||
|
{
|
||||||
|
PDEVICE_CONTEXT deviceContext = DeviceGetContext(Device);
|
||||||
|
|
||||||
|
UNREFERENCED_PARAMETER(PreviousState);
|
||||||
|
|
||||||
|
return VhfStart(deviceContext->VhfHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
VOID
|
||||||
|
RioGamepadEvtDeviceCleanup(
|
||||||
|
_In_ WDFOBJECT Object
|
||||||
|
)
|
||||||
|
{
|
||||||
|
PDEVICE_CONTEXT deviceContext = DeviceGetContext((WDFDEVICE)Object);
|
||||||
|
|
||||||
|
if (deviceContext->VhfHandle != NULL)
|
||||||
|
{
|
||||||
|
VhfDelete(deviceContext->VhfHandle, TRUE);
|
||||||
|
deviceContext->VhfHandle = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VOID
|
||||||
|
RioGamepadEvtIoDeviceControl(
|
||||||
|
_In_ WDFQUEUE Queue,
|
||||||
|
_In_ WDFREQUEST Request,
|
||||||
|
_In_ size_t OutputBufferLength,
|
||||||
|
_In_ size_t InputBufferLength,
|
||||||
|
_In_ ULONG IoControlCode
|
||||||
|
)
|
||||||
|
{
|
||||||
|
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
|
||||||
|
PDEVICE_CONTEXT deviceContext = DeviceGetContext(WdfIoQueueGetDevice(Queue));
|
||||||
|
|
||||||
|
UNREFERENCED_PARAMETER(OutputBufferLength);
|
||||||
|
|
||||||
|
if (IoControlCode == IOCTL_RIO_SUBMIT_REPORT)
|
||||||
|
{
|
||||||
|
if (InputBufferLength != RIO_REPORT_SIZE)
|
||||||
|
{
|
||||||
|
status = STATUS_INVALID_BUFFER_SIZE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PVOID buffer;
|
||||||
|
size_t bufferSize;
|
||||||
|
|
||||||
|
status = WdfRequestRetrieveInputBuffer(Request, RIO_REPORT_SIZE, &buffer, &bufferSize);
|
||||||
|
if (NT_SUCCESS(status))
|
||||||
|
{
|
||||||
|
HID_XFER_PACKET packet;
|
||||||
|
packet.reportId = 0;
|
||||||
|
packet.reportBuffer = (PUCHAR)buffer;
|
||||||
|
packet.reportBufferLen = RIO_REPORT_SIZE;
|
||||||
|
|
||||||
|
status = VhfReadReportSubmit(deviceContext->VhfHandle, &packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WdfRequestComplete(Request, status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*++
|
||||||
|
RioGamepad — DriverEntry and driver object setup.
|
||||||
|
--*/
|
||||||
|
|
||||||
|
#include "Driver.h"
|
||||||
|
|
||||||
|
NTSTATUS
|
||||||
|
DriverEntry(
|
||||||
|
_In_ PDRIVER_OBJECT DriverObject,
|
||||||
|
_In_ PUNICODE_STRING RegistryPath
|
||||||
|
)
|
||||||
|
{
|
||||||
|
WDF_DRIVER_CONFIG config;
|
||||||
|
NTSTATUS status;
|
||||||
|
|
||||||
|
WDF_DRIVER_CONFIG_INIT(&config, RioGamepadEvtDeviceAdd);
|
||||||
|
|
||||||
|
status = WdfDriverCreate(
|
||||||
|
DriverObject,
|
||||||
|
RegistryPath,
|
||||||
|
WDF_NO_OBJECT_ATTRIBUTES,
|
||||||
|
&config,
|
||||||
|
WDF_NO_HANDLE);
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*++
|
||||||
|
RioGamepad — driver-wide declarations.
|
||||||
|
--*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ntddk.h>
|
||||||
|
#include <wdf.h>
|
||||||
|
#include <vhf.h> // VHF API; also defines HID_XFER_PACKET if hidclass.h absent
|
||||||
|
#include "Public.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// Per-device context: the VHF handle for the virtual HID device.
|
||||||
|
//
|
||||||
|
typedef struct _DEVICE_CONTEXT
|
||||||
|
{
|
||||||
|
VHFHANDLE VhfHandle;
|
||||||
|
} DEVICE_CONTEXT, *PDEVICE_CONTEXT;
|
||||||
|
|
||||||
|
WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DEVICE_CONTEXT, DeviceGetContext)
|
||||||
|
|
||||||
|
//
|
||||||
|
// WDF event callbacks.
|
||||||
|
//
|
||||||
|
DRIVER_INITIALIZE DriverEntry;
|
||||||
|
EVT_WDF_DRIVER_DEVICE_ADD RioGamepadEvtDeviceAdd;
|
||||||
|
EVT_WDF_DEVICE_D0_ENTRY RioGamepadEvtDeviceD0Entry;
|
||||||
|
EVT_WDF_OBJECT_CONTEXT_CLEANUP RioGamepadEvtDeviceCleanup;
|
||||||
|
EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL RioGamepadEvtIoDeviceControl;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*++
|
||||||
|
RioGamepad — shared definitions for the virtual HID gamepad driver and its
|
||||||
|
user-mode client (the RIOJoy tray app's HID feeder).
|
||||||
|
|
||||||
|
The input report is fixed-size and has no report ID:
|
||||||
|
bytes 0..11 : 6 axes (X,Y,Z,Rx,Ry,Rz), each unsigned 16-bit little-endian
|
||||||
|
byte 12 : low nibble = hat (0..3; 0x0F = centered/null), high nibble pad
|
||||||
|
bytes 13..24 : 96 button bits (button N at byte 13 + (N-1)/8, bit (N-1)%8)
|
||||||
|
--*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
//
|
||||||
|
// Device interface GUID the user-mode client opens to submit reports.
|
||||||
|
// {b6a3f1c2-7e84-4d2a-9c1f-2a5e8d3b6071}
|
||||||
|
//
|
||||||
|
DEFINE_GUID(GUID_DEVINTERFACE_RIOGAMEPAD,
|
||||||
|
0xb6a3f1c2, 0x7e84, 0x4d2a, 0x9c, 0x1f, 0x2a, 0x5e, 0x8d, 0x3b, 0x60, 0x71);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Custom IOCTL: submit one input report (input buffer = RIO_REPORT_SIZE bytes).
|
||||||
|
//
|
||||||
|
#define IOCTL_RIO_SUBMIT_REPORT \
|
||||||
|
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_WRITE_ACCESS)
|
||||||
|
|
||||||
|
//
|
||||||
|
// HID input report size in bytes: 6*2 (axes) + 1 (hat+pad) + 12 (96 buttons).
|
||||||
|
//
|
||||||
|
#define RIO_REPORT_SIZE 25
|
||||||
|
|
||||||
|
//
|
||||||
|
// Virtual device identity (matches the legacy vJoy layout's intent).
|
||||||
|
//
|
||||||
|
#define RIO_VENDOR_ID 0x1209 // pid.codes test/community VID
|
||||||
|
#define RIO_PRODUCT_ID 0x5249 // 'R','I'
|
||||||
|
#define RIO_VERSION 0x0100
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*++
|
||||||
|
RioGamepad — HID report descriptor: a joystick with 6 16-bit axes (X,Y,Z,Rx,
|
||||||
|
Ry,Rz), one 4-direction hat with null state, and 96 buttons. This mirrors the
|
||||||
|
fidelity the legacy app drove through vJoy. The resulting input report is
|
||||||
|
RIO_REPORT_SIZE (25) bytes; see Public.h for the byte layout.
|
||||||
|
--*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
static const UCHAR g_RioReportDescriptor[] =
|
||||||
|
{
|
||||||
|
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||||
|
0x09, 0x04, // Usage (Joystick)
|
||||||
|
0xA1, 0x01, // Collection (Application)
|
||||||
|
|
||||||
|
0x09, 0x01, // Usage (Pointer)
|
||||||
|
0xA1, 0x00, // Collection (Physical)
|
||||||
|
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||||
|
0x09, 0x30, // Usage (X)
|
||||||
|
0x09, 0x31, // Usage (Y)
|
||||||
|
0x09, 0x32, // Usage (Z)
|
||||||
|
0x09, 0x33, // Usage (Rx)
|
||||||
|
0x09, 0x34, // Usage (Ry)
|
||||||
|
0x09, 0x35, // Usage (Rz)
|
||||||
|
0x15, 0x00, // Logical Minimum (0)
|
||||||
|
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
|
||||||
|
0x75, 0x10, // Report Size (16)
|
||||||
|
0x95, 0x06, // Report Count (6)
|
||||||
|
0x81, 0x02, // Input (Data,Var,Abs)
|
||||||
|
0xC0, // End Collection (Physical)
|
||||||
|
|
||||||
|
0x09, 0x39, // Usage (Hat switch)
|
||||||
|
0x15, 0x00, // Logical Minimum (0)
|
||||||
|
0x25, 0x03, // Logical Maximum (3)
|
||||||
|
0x35, 0x00, // Physical Minimum (0)
|
||||||
|
0x46, 0x0E, 0x01, // Physical Maximum (270)
|
||||||
|
0x65, 0x14, // Unit (Degrees)
|
||||||
|
0x75, 0x04, // Report Size (4)
|
||||||
|
0x95, 0x01, // Report Count (1)
|
||||||
|
0x81, 0x42, // Input (Data,Var,Abs,Null)
|
||||||
|
0x65, 0x00, // Unit (None)
|
||||||
|
0x75, 0x04, // Report Size (4)
|
||||||
|
0x95, 0x01, // Report Count (1)
|
||||||
|
0x81, 0x03, // Input (Const,Var,Abs) ; 4-bit padding
|
||||||
|
|
||||||
|
0x05, 0x09, // Usage Page (Button)
|
||||||
|
0x19, 0x01, // Usage Minimum (Button 1)
|
||||||
|
0x29, 0x60, // Usage Maximum (Button 96)
|
||||||
|
0x15, 0x00, // Logical Minimum (0)
|
||||||
|
0x25, 0x01, // Logical Maximum (1)
|
||||||
|
0x75, 0x01, // Report Size (1)
|
||||||
|
0x95, 0x60, // Report Count (96)
|
||||||
|
0x81, 0x02, // Input (Data,Var,Abs)
|
||||||
|
|
||||||
|
0xC0 // End Collection (Application)
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
;
|
||||||
|
; RioGamepad.inf — RIOJoy virtual HID gamepad (KMDF + VHF), root-enumerated.
|
||||||
|
;
|
||||||
|
|
||||||
|
[Version]
|
||||||
|
Signature = "$WINDOWS NT$"
|
||||||
|
Class = System
|
||||||
|
ClassGuid = {4d36e97d-e325-11ce-bfc1-08002be10318}
|
||||||
|
Provider = %ManufacturerName%
|
||||||
|
CatalogFile = RioGamepad.cat
|
||||||
|
DriverVer =
|
||||||
|
PnpLockdown = 1
|
||||||
|
|
||||||
|
[DestinationDirs]
|
||||||
|
DefaultDestDir = 13
|
||||||
|
|
||||||
|
[SourceDisksNames]
|
||||||
|
1 = %DiskName%
|
||||||
|
|
||||||
|
[SourceDisksFiles]
|
||||||
|
RioGamepad.sys = 1
|
||||||
|
|
||||||
|
[Manufacturer]
|
||||||
|
%ManufacturerName% = Standard,NT$ARCH$.10.0...16299
|
||||||
|
|
||||||
|
[Standard.NT$ARCH$.10.0...16299]
|
||||||
|
%DeviceName% = RioGamepad_Device, root\RioGamepad
|
||||||
|
|
||||||
|
[RioGamepad_Device.NT]
|
||||||
|
CopyFiles = RioGamepad_CopyFiles
|
||||||
|
|
||||||
|
[RioGamepad_CopyFiles]
|
||||||
|
RioGamepad.sys
|
||||||
|
|
||||||
|
[RioGamepad_Device.NT.Services]
|
||||||
|
AddService = RioGamepad, 0x00000002, RioGamepad_Service
|
||||||
|
|
||||||
|
[RioGamepad_Service]
|
||||||
|
DisplayName = %ServiceName%
|
||||||
|
ServiceType = 1 ; SERVICE_KERNEL_DRIVER
|
||||||
|
StartType = 3 ; SERVICE_DEMAND_START
|
||||||
|
ErrorControl = 1 ; SERVICE_ERROR_NORMAL
|
||||||
|
ServiceBinary = %13%\RioGamepad.sys
|
||||||
|
|
||||||
|
[RioGamepad_Device.NT.Wdf]
|
||||||
|
KmdfService = RioGamepad, RioGamepad_wdfsect
|
||||||
|
|
||||||
|
[RioGamepad_wdfsect]
|
||||||
|
KmdfLibraryVersion = 1.15
|
||||||
|
|
||||||
|
[Strings]
|
||||||
|
ManufacturerName = "VWE"
|
||||||
|
DiskName = "RioGamepad Installation Disk"
|
||||||
|
DeviceName = "RIOJoy Virtual Gamepad"
|
||||||
|
ServiceName = "RIOJoy Virtual Gamepad Service"
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<ItemGroup Label="ProjectConfigurations">
|
||||||
|
<ProjectConfiguration Include="Debug|x64">
|
||||||
|
<Configuration>Debug</Configuration>
|
||||||
|
<Platform>x64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
<ProjectConfiguration Include="Release|x64">
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>x64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
</ItemGroup>
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<ProjectGuid>{4F1E9C2A-6B3D-4E58-9A77-1C2D3E4F5A6B}</ProjectGuid>
|
||||||
|
<MinimumVisualStudioVersion>12.0</MinimumVisualStudioVersion>
|
||||||
|
<Configuration Condition="'$(Configuration)' == ''">Release</Configuration>
|
||||||
|
<Platform Condition="'$(Platform)' == ''">x64</Platform>
|
||||||
|
<RootNamespace>RioGamepad</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||||
|
<TargetVersion>Windows10</TargetVersion>
|
||||||
|
<UseDebugLibraries>true</UseDebugLibraries>
|
||||||
|
<PlatformToolset>WindowsKernelModeDriver10.0</PlatformToolset>
|
||||||
|
<ConfigurationType>Driver</ConfigurationType>
|
||||||
|
<DriverType>KMDF</DriverType>
|
||||||
|
<KMDFVersionMajor>1</KMDFVersionMajor>
|
||||||
|
<KMDFVersionMinor>15</KMDFVersionMinor>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||||
|
<TargetVersion>Windows10</TargetVersion>
|
||||||
|
<UseDebugLibraries>false</UseDebugLibraries>
|
||||||
|
<PlatformToolset>WindowsKernelModeDriver10.0</PlatformToolset>
|
||||||
|
<ConfigurationType>Driver</ConfigurationType>
|
||||||
|
<DriverType>KMDF</DriverType>
|
||||||
|
<KMDFVersionMajor>1</KMDFVersionMajor>
|
||||||
|
<KMDFVersionMinor>15</KMDFVersionMinor>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||||
|
<ItemDefinitionGroup>
|
||||||
|
<ClCompile>
|
||||||
|
<WppEnabled>false</WppEnabled>
|
||||||
|
<TreatWarningAsError>true</TreatWarningAsError>
|
||||||
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<AdditionalDependencies>%(AdditionalDependencies);$(DDK_LIB_PATH)vhfkm.lib</AdditionalDependencies>
|
||||||
|
</Link>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClCompile Include="Driver.c" />
|
||||||
|
<ClCompile Include="Device.c" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClInclude Include="Driver.h" />
|
||||||
|
<ClInclude Include="Public.h" />
|
||||||
|
<ClInclude Include="ReportDescriptor.h" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Inf Include="RioGamepad.inf" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<FilesToPackage Include="$(TargetPath)" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
@echo off
|
||||||
|
REM Build RioGamepad.sys against a mounted Enterprise WDK (EWDK).
|
||||||
|
REM Usage: build.cmd [EWDK_DriveOrRoot] (default: E:)
|
||||||
|
REM
|
||||||
|
REM Produces driver\RioGamepad\x64\Release\RioGamepad.sys.
|
||||||
|
REM Catalog (.cat) creation and signing are intentionally disabled here — that is
|
||||||
|
REM the deploy/test-sign step (see driver\README.md). On this EWDK the managed
|
||||||
|
REM DrvCat MSBuild task fails to load Microsoft.Kits.Logger, so the .cat is built
|
||||||
|
REM separately with inf2cat.exe / signtool.exe at install time.
|
||||||
|
|
||||||
|
setlocal
|
||||||
|
set "EWDK=%~1"
|
||||||
|
if "%EWDK%"=="" set "EWDK=E:"
|
||||||
|
|
||||||
|
call "%EWDK%\BuildEnv\SetupBuildEnv.cmd" >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Failed to initialize the EWDK build environment from "%EWDK%".
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
msbuild "%~dp0RioGamepad.vcxproj" ^
|
||||||
|
/p:Configuration=Release /p:Platform=x64 ^
|
||||||
|
/p:SignMode=Off /p:DriverCatalog_Enable=false ^
|
||||||
|
/nologo /v:m /clp:NoSummary
|
||||||
|
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user