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>
382 lines
12 KiB
C
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(®, 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(®);
|
|
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;
|
|
}
|