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
|
||||
|
||||
Phases 2–5 are code-complete and unit-tested (123 tests): serial + RIO protocol
|
||||
core, input mapping + output routing, axis calibration + plasma display, and the
|
||||
tray app + profiles (JSON config, `RIO.ini` importer, the three-state auto-switch
|
||||
watcher, and the `RioRuntime` that wires it together). The virtual-HID feeder and
|
||||
on-cabinet verification are pending on the Phase 1 driver. See
|
||||
Phases 1–5 are implemented and tested (136 unit tests): the `RioGamepad` virtual
|
||||
HID driver compiles to `.sys` against the EWDK (KMDF + VHF), and the C# side
|
||||
covers the serial + RIO protocol core, input mapping + output routing, axis
|
||||
calibration + plasma display, the tray app + profiles (JSON config, `RIO.ini`
|
||||
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.
|
||||
|
||||
+13
-5
@@ -106,12 +106,20 @@ tray menu is always available.
|
||||
(tray app); `driver/` placeholder for the WDK project.
|
||||
- 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,
|
||||
96 buttons.
|
||||
- Control device + custom IOCTL → `VhfReadReportSubmit`.
|
||||
- Test harness (throwaway C#) wiggles axes/buttons; verify in `joy.cpl`.
|
||||
- Test-signing setup for the cabinets.
|
||||
96 buttons ([`ReportDescriptor.h`](../driver/RioGamepad/ReportDescriptor.h)).
|
||||
- Device interface + custom `IOCTL_RIO_SUBMIT_REPORT` → `VhfReadReportSubmit`; the
|
||||
driver is a thin relay, with the 25-byte report layout pinned in
|
||||
[`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 ✅
|
||||
Implemented in `src/RioJoy.Core/Protocol` + `Serial`, covered by
|
||||
|
||||
+27
-5
@@ -17,13 +17,35 @@ to Windows via `VhfReadReportSubmit`.
|
||||
|
||||
## 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
|
||||
- A separate WDK/MSBuild project lives here (`RioGamepad.vcxproj`); it is **not**
|
||||
part of `RioJoy.sln` (different toolchain).
|
||||
## Building
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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