/*++ 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 #include #include // 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 typedef struct _RIO_HID_DESCRIPTOR { UCHAR bLength; UCHAR bDescriptorType; USHORT bcdHID; UCHAR bCountry; UCHAR bNumDescriptors; UCHAR bReportType; USHORT wReportLength; } RIO_HID_DESCRIPTOR; #include 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; }