Files
riojoy/driver/RioGamepadXP/rioxp.c
T
CydandClaude Fable 5 52712f8409 Phase 8B: RioGamepadXP.sys — WDM HID minidriver for Windows XP
The XP flavor of our own virtual joystick (no third-party driver), built
with WDK 7.1.0 to x86/subsystem-5.01. Exposes the identical Public.h
contract as the modern KMDF+VHF driver — same GUID, IOCTL_RIO_SUBMIT_REPORT,
25-byte report, VID/PID — so HidFeederJoystickSink drives both.

Design (driver/RioGamepadXP/rioxp.c):
- WDM HID minidriver via HidRegisterMinidriver presenting the 6-axis/hat/
  96-button joystick. POLLED mode (DevicesArePolled=TRUE): hidclass paces
  IOCTL_HID_READ_REPORT and we complete each synchronously from a cached
  report — no pending-IRP or cancel-routine machinery (the usual crash
  surface in a virtual HID driver).
- Named sideband control device (\Device\RioGamepadXP + \DosDevices symlink)
  takes IOCTL_RIO_SUBMIT_REPORT and updates the cache under a spinlock.
- hidclass overwrites our CREATE/CLOSE/DEVICE_CONTROL during registration;
  we save its pointers and reinstall wrappers that route the control device's
  IRPs to us and forward the HID FDO's to hidclass.

Packaging/install:
- Root-enumerated, so install uses devcon (built from the same WDK) —
  InstallHinfSection can't create the devnode. install-core.bat runs
  "devcon install RioGamepadXP.inf root\RioGamepadXP"; unsigned is fine
  (XP x86 enforces no kernel signing). Bundled into vendor\xp\.
- net40 feeder opens the driver by name (\.\RioGamepadXP); XP can't put a
  device interface on a bare control device (no PDO) — the one nuance.

Static build only: compiles clean but runtime bring-up (joy.cpl enumeration,
report flow) needs a real XP target — that's 8E. 275 tests still green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:01:16 -05:00

382 lines
12 KiB
C

/*++
RioGamepadXP — a WDM HID minidriver for Windows XP that presents a virtual
joystick (6 axes, hat, 96 buttons) and accepts input reports from the RIOJoy
tray app over a sideband control device. The XP-era replacement for the modern
KMDF+VHF RioGamepad driver, exposing the same 25-byte report contract
(see Public.h) so the user-mode feeder is unchanged apart from the open path.
Architecture
------------
There is exactly one virtual device, so state is global (guarded by a spinlock):
* HID side: we register as a HID minidriver (HidRegisterMinidriver). hidclass
creates and owns the HID FDO and drives PnP/Power; it calls our
IRP_MJ_INTERNAL_DEVICE_CONTROL handler to fetch the HID/report descriptors,
device attributes, and input reports. We run in POLLED mode
(DevicesArePolled = TRUE): hidclass paces IOCTL_HID_READ_REPORT and we
complete each synchronously with the current cached report. This avoids
pending-IRP + cancel-routine machinery entirely (the usual crash surface in
a virtual HID driver).
* Sideband: a raw control device (\Device\RioGamepadXP + a \DosDevices symlink)
that the app opens by name. On IOCTL_RIO_SUBMIT_REPORT we copy the 25 report
bytes into the cache. hidclass, on its next poll, hands them to the OS.
Dispatch ownership: HidRegisterMinidriver replaces our driver object's
CREATE/CLOSE/DEVICE_CONTROL (and PnP/Power/etc.) entries with hidclass thunks
that assume a HID FDO. Our control device shares the same driver object, so we
save hidclass's pointers and reinstall thin wrappers that route the control
device's IRPs to us and forward everything else to hidclass.
--*/
#include <ntddk.h>
#include <hidport.h>
#include <initguid.h> // realize DEFINE_GUID storage from Public.h
#include "Public.h"
#include "ReportDescriptor.h"
//
// Global single-instance state.
//
static PDEVICE_OBJECT g_ControlDevice = NULL;
static KSPIN_LOCK g_ReportLock;
static UCHAR g_Report[RIO_REPORT_SIZE];
// hidclass-installed dispatch pointers we wrap.
static PDRIVER_DISPATCH g_HidCreate = NULL;
static PDRIVER_DISPATCH g_HidClose = NULL;
static PDRIVER_DISPATCH g_HidDeviceControl = NULL;
static UNICODE_STRING g_SymlinkName;
//
// The HID descriptor that advertises our single report descriptor. bcdHID 1.11.
//
#include <pshpack1.h>
typedef struct _RIO_HID_DESCRIPTOR {
UCHAR bLength;
UCHAR bDescriptorType;
USHORT bcdHID;
UCHAR bCountry;
UCHAR bNumDescriptors;
UCHAR bReportType;
USHORT wReportLength;
} RIO_HID_DESCRIPTOR;
#include <poppack.h>
static const RIO_HID_DESCRIPTOR g_HidDescriptor =
{
sizeof(RIO_HID_DESCRIPTOR),
HID_HID_DESCRIPTOR_TYPE, // 0x21
0x0111, // HID 1.11
0x00, // not localized
1, // one report descriptor
HID_REPORT_DESCRIPTOR_TYPE, // 0x22
sizeof(g_RioReportDescriptor)
};
//
// Reset the cached report to the neutral rest state: axes centered (16383 =
// 0x3FFF little-endian), hat null (0x0F), all buttons released.
//
static VOID
RioResetReport(VOID)
{
ULONG i;
KIRQL irql;
KeAcquireSpinLock(&g_ReportLock, &irql);
RtlZeroMemory(g_Report, sizeof(g_Report));
for (i = 0; i < 6; i++) // 6 axes, 2 bytes each, centered
{
g_Report[i * 2] = 0xFF;
g_Report[i * 2 + 1] = 0x3F;
}
g_Report[12] = 0x0F; // hat centered/null
KeReleaseSpinLock(&g_ReportLock, irql);
}
//
// Complete an IRP with a status and information count.
//
static NTSTATUS
RioComplete(PIRP Irp, NTSTATUS Status, ULONG_PTR Information)
{
Irp->IoStatus.Status = Status;
Irp->IoStatus.Information = Information;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return Status;
}
//
// IRP_MJ_INTERNAL_DEVICE_CONTROL on the HID FDO: hidclass asks us for the
// descriptors / attributes / input reports. These IOCTLs are METHOD_NEITHER;
// hidclass supplies the output buffer at Irp->UserBuffer with the length in
// Parameters.DeviceIoControl.OutputBufferLength.
//
static NTSTATUS
RioInternalDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status = STATUS_SUCCESS;
ULONG_PTR info = 0;
ULONG outLen = stack->Parameters.DeviceIoControl.OutputBufferLength;
PVOID out = Irp->UserBuffer;
UNREFERENCED_PARAMETER(DeviceObject);
switch (stack->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_HID_GET_DEVICE_DESCRIPTOR:
if (outLen < sizeof(g_HidDescriptor)) { status = STATUS_BUFFER_TOO_SMALL; break; }
RtlCopyMemory(out, &g_HidDescriptor, sizeof(g_HidDescriptor));
info = sizeof(g_HidDescriptor);
break;
case IOCTL_HID_GET_REPORT_DESCRIPTOR:
if (outLen < sizeof(g_RioReportDescriptor)) { status = STATUS_BUFFER_TOO_SMALL; break; }
RtlCopyMemory(out, g_RioReportDescriptor, sizeof(g_RioReportDescriptor));
info = sizeof(g_RioReportDescriptor);
break;
case IOCTL_HID_GET_DEVICE_ATTRIBUTES:
{
PHID_DEVICE_ATTRIBUTES attr = (PHID_DEVICE_ATTRIBUTES)out;
if (outLen < sizeof(HID_DEVICE_ATTRIBUTES)) { status = STATUS_BUFFER_TOO_SMALL; break; }
RtlZeroMemory(attr, sizeof(HID_DEVICE_ATTRIBUTES));
attr->Size = sizeof(HID_DEVICE_ATTRIBUTES);
attr->VendorID = RIO_VENDOR_ID;
attr->ProductID = RIO_PRODUCT_ID;
attr->VersionNumber = RIO_VERSION;
info = sizeof(HID_DEVICE_ATTRIBUTES);
break;
}
case IOCTL_HID_READ_REPORT:
{
KIRQL irql;
if (outLen < RIO_REPORT_SIZE) { status = STATUS_BUFFER_TOO_SMALL; break; }
KeAcquireSpinLock(&g_ReportLock, &irql);
RtlCopyMemory(out, g_Report, RIO_REPORT_SIZE);
KeReleaseSpinLock(&g_ReportLock, irql);
info = RIO_REPORT_SIZE;
break;
}
case IOCTL_HID_WRITE_REPORT:
case IOCTL_HID_SET_FEATURE:
case IOCTL_HID_GET_FEATURE:
// No output reports / feature reports on this device.
status = STATUS_NOT_SUPPORTED;
break;
case IOCTL_HID_GET_STRING:
// DirectInput takes the friendly name from the registry OEMName value
// (set at install), not from a HID string, so an empty success is fine.
status = STATUS_SUCCESS;
info = 0;
break;
default:
status = STATUS_NOT_SUPPORTED;
break;
}
return RioComplete(Irp, status, info);
}
//
// hidclass calls this (our saved AddDevice) after it has created the HID FDO.
// Nothing to attach — hidclass owns the stack — so just succeed.
//
static NTSTATUS
RioAddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT FunctionalDeviceObject)
{
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(FunctionalDeviceObject);
return STATUS_SUCCESS;
}
//
// Sideband handlers (control device only). CREATE/CLOSE just succeed.
//
static NTSTATUS
RioControlCreateClose(PIRP Irp)
{
return RioComplete(Irp, STATUS_SUCCESS, 0);
}
//
// IOCTL_RIO_SUBMIT_REPORT (METHOD_BUFFERED): copy the 25 report bytes into the
// cache; hidclass hands them out on its next poll.
//
static NTSTATUS
RioControlDeviceControl(PIRP Irp)
{
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status;
if (stack->Parameters.DeviceIoControl.IoControlCode == IOCTL_RIO_SUBMIT_REPORT)
{
if (stack->Parameters.DeviceIoControl.InputBufferLength != RIO_REPORT_SIZE)
{
status = STATUS_INVALID_BUFFER_SIZE;
}
else
{
KIRQL irql;
KeAcquireSpinLock(&g_ReportLock, &irql);
RtlCopyMemory(g_Report, Irp->AssociatedIrp.SystemBuffer, RIO_REPORT_SIZE);
KeReleaseSpinLock(&g_ReportLock, irql);
status = STATUS_SUCCESS;
}
}
else
{
status = STATUS_INVALID_DEVICE_REQUEST;
}
return RioComplete(Irp, status, 0);
}
//
// Dispatch wrappers: route the control device's IRPs to us, forward the HID
// FDO's IRPs to the saved hidclass handlers.
//
static NTSTATUS
RioDispatchCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
if (DeviceObject == g_ControlDevice)
return RioControlCreateClose(Irp);
return g_HidCreate(DeviceObject, Irp);
}
static NTSTATUS
RioDispatchClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
if (DeviceObject == g_ControlDevice)
return RioControlCreateClose(Irp);
return g_HidClose(DeviceObject, Irp);
}
static NTSTATUS
RioDispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
if (DeviceObject == g_ControlDevice)
return RioControlDeviceControl(Irp);
return g_HidDeviceControl(DeviceObject, Irp);
}
//
// Unload: drop the symlink and control device. hidclass tears down the HID FDO.
//
static VOID
RioUnload(PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
if (g_SymlinkName.Buffer != NULL)
IoDeleteSymbolicLink(&g_SymlinkName);
if (g_ControlDevice != NULL)
IoDeleteDevice(g_ControlDevice);
}
//
// Create the sideband control device and its DOS-device symbolic link.
//
static NTSTATUS
RioCreateControlDevice(PDRIVER_OBJECT DriverObject)
{
NTSTATUS status;
UNICODE_STRING deviceName;
RtlInitUnicodeString(&deviceName, RIO_XP_DEVICE_NAME);
RtlInitUnicodeString(&g_SymlinkName, RIO_XP_SYMLINK_NAME);
status = IoCreateDevice(
DriverObject,
0, // no device extension; state is global
&deviceName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE, // not exclusive
&g_ControlDevice);
if (!NT_SUCCESS(status))
return status;
g_ControlDevice->Flags |= DO_BUFFERED_IO; // IOCTL_RIO_SUBMIT_REPORT is METHOD_BUFFERED
status = IoCreateSymbolicLink(&g_SymlinkName, &deviceName);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(g_ControlDevice);
g_ControlDevice = NULL;
g_SymlinkName.Buffer = NULL;
return status;
}
g_ControlDevice->Flags &= ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
}
NTSTATUS
DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
NTSTATUS status;
HID_MINIDRIVER_REGISTRATION reg;
KeInitializeSpinLock(&g_ReportLock);
RioResetReport();
//
// Our own dispatch entries. hidclass replaces the HID-related ones during
// registration; we re-wrap CREATE/CLOSE/DEVICE_CONTROL afterwards.
//
DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] = RioInternalDeviceControl;
DriverObject->MajorFunction[IRP_MJ_CREATE] = RioDispatchCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = RioDispatchClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = RioDispatchDeviceControl;
DriverObject->DriverExtension->AddDevice = RioAddDevice;
DriverObject->DriverUnload = RioUnload;
RtlZeroMemory(&reg, sizeof(reg));
reg.Revision = HID_REVISION;
reg.DriverObject = DriverObject;
reg.RegistryPath = RegistryPath;
reg.DeviceExtensionSize = 0; // per-device state is global (single instance)
reg.DevicesArePolled = TRUE; // polled reads → no pending-IRP/cancel machinery
status = HidRegisterMinidriver(&reg);
if (!NT_SUCCESS(status))
return status;
//
// hidclass has now overwritten CREATE/CLOSE/DEVICE_CONTROL (and PnP/Power/
// etc.) with its own thunks. Save the ones we need to forward, then reinstall
// our wrappers so the control device's IRPs come back to us.
//
g_HidCreate = DriverObject->MajorFunction[IRP_MJ_CREATE];
g_HidClose = DriverObject->MajorFunction[IRP_MJ_CLOSE];
g_HidDeviceControl = DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL];
DriverObject->MajorFunction[IRP_MJ_CREATE] = RioDispatchCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = RioDispatchClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = RioDispatchDeviceControl;
//
// hidclass also replaces DriverUnload; chain our teardown ahead of nothing
// (hidclass's unload runs via its own bookkeeping). Reassert ours.
//
DriverObject->DriverUnload = RioUnload;
status = RioCreateControlDevice(DriverObject);
if (!NT_SUCCESS(status))
{
// The HID device can still work without the sideband, but the app can't
// feed it, so fail registration cleanly.
return status;
}
return STATUS_SUCCESS;
}