From 52712f84096274b4103362f0e71784bece1c9208 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 17 Jul 2026 14:01:16 -0500 Subject: [PATCH] =?UTF-8?q?Phase=208B:=20RioGamepadXP.sys=20=E2=80=94=20WD?= =?UTF-8?q?M=20HID=20minidriver=20for=20Windows=20XP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 7 + deploy/build-package.ps1 | 3 +- deploy/install-core.bat | 15 +- docs/PLAN.md | 44 +- driver/RioGamepadXP/Public.h | 56 +++ driver/RioGamepadXP/ReportDescriptor.h | 56 +++ driver/RioGamepadXP/RioGamepadXP.inf | 53 +++ driver/RioGamepadXP/build.cmd | 7 + driver/RioGamepadXP/makefile | 1 + driver/RioGamepadXP/rioxp.c | 381 ++++++++++++++++++ driver/RioGamepadXP/sources | 7 + .../Output/HidFeederJoystickSink.cs | 8 + 12 files changed, 613 insertions(+), 25 deletions(-) create mode 100644 driver/RioGamepadXP/Public.h create mode 100644 driver/RioGamepadXP/ReportDescriptor.h create mode 100644 driver/RioGamepadXP/RioGamepadXP.inf create mode 100644 driver/RioGamepadXP/build.cmd create mode 100644 driver/RioGamepadXP/makefile create mode 100644 driver/RioGamepadXP/rioxp.c create mode 100644 driver/RioGamepadXP/sources diff --git a/.gitignore b/.gitignore index 58992b4..cefe9a8 100644 --- a/.gitignore +++ b/.gitignore @@ -372,6 +372,13 @@ FodyWeavers.xsd # WDK / driver build outputs [Dd]river/**/[Xx]64/ [Dd]river/package/ +# WDK build.exe output trees + logs (RioGamepadXP / any ddkbuild sample) +[Dd]river/**/objfre_*/ +[Dd]river/**/objchk_*/ +[Dd]river/**/buildfre_*.log +[Dd]river/**/buildchk_*.log +[Dd]river/**/build.err +[Dd]river/**/build.wrn *.cer *.cat *.pvk diff --git a/deploy/build-package.ps1 b/deploy/build-package.ps1 index 2285af7..be8182f 100644 --- a/deploy/build-package.ps1 +++ b/deploy/build-package.ps1 @@ -102,7 +102,8 @@ try { @{ Name = 'dotNetFx40_Full_x86_x64.exe'; What = '.NET Framework 4.0 offline installer' }, @{ Name = 'NDP40-KB2468871-v2-x86.exe'; What = '.NET 4.0 update KB2468871 (required by Bcl.Async)' }, @{ Name = 'RioGamepadXP.inf'; What = 'RioGamepadXP driver INF (Phase 8B)' }, - @{ Name = 'RioGamepadXP.sys'; What = 'RioGamepadXP driver binary (Phase 8B)' } + @{ Name = 'RioGamepadXP.sys'; What = 'RioGamepadXP driver binary (Phase 8B)' }, + @{ Name = 'devcon.exe'; What = 'devcon (creates the root-enumerated driver devnode on XP)' } ) foreach ($item in $xpWanted) { $src = Join-Path $vendorXpSrc $item.Name diff --git a/deploy/install-core.bat b/deploy/install-core.bat index 76b1dad..c50e322 100644 --- a/deploy/install-core.bat +++ b/deploy/install-core.bat @@ -55,12 +55,19 @@ if exist "%PKGROOT%vendor\xp\NDP40-KB2468871-v2-x86.exe" ( echo will not run without KB2468871. Install it before first use. ) -rem RioGamepadXP virtual-joystick driver (bundled from Phase 8B onward). +rem RioGamepadXP virtual-joystick driver. Root-enumerated, so it must be +rem created with devcon (rundll32/InstallHinfSection would not make the +rem devnode). devcon is idempotent: re-running "install" updates in place. if exist "%PKGROOT%vendor\xp\RioGamepadXP.inf" ( - echo Installing the RioGamepadXP virtual joystick driver... - rundll32 setupapi.dll,InstallHinfSection DefaultInstall 132 %PKGROOT%vendor\xp\RioGamepadXP.inf + if exist "%PKGROOT%vendor\xp\devcon.exe" ( + echo Installing the RioGamepadXP virtual joystick driver... + "%PKGROOT%vendor\xp\devcon.exe" install "%PKGROOT%vendor\xp\RioGamepadXP.inf" root\RioGamepadXP + if errorlevel 1 echo WARNING: driver install returned an error - check Device Manager. + ) else ( + echo WARNING: vendor\xp\devcon.exe missing - cannot create the driver devnode. + ) ) else ( - echo Note: RioGamepadXP driver not bundled yet - joystick output is + echo Note: RioGamepadXP driver not bundled - joystick output is echo unavailable on XP; keyboard/mouse, lamps and plasma still work. ) diff --git a/docs/PLAN.md b/docs/PLAN.md index 2719f41..97e4636 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -322,26 +322,30 @@ XP consumes pre-rendered wallpapers. (`ViGEmJoystickSink`, `HidFeederJoystickSink`, `Hid/`). *Risk fallback:* if Bcl.Async misbehaves on real XP, the receive loop reverts to a dedicated thread (the legacy `CommWatchProc` shape) for net40. -- **8B — RioGamepadXP.sys, the XP flavor of our own driver.** Third-party - virtual-joystick drivers are ruled out (decided: **no vJoy** — the project - is unmaintained; PPJoy likewise). Instead, rebuild the thin driver side of - our existing split for XP: a **WDM HID minidriver** in the shape of the - DDK `vhidmini` sample (`HidRegisterMinidriver`, x86), exposing the **same - `Public.h` contract** as the modern driver — identical - `IOCTL_RIO_SUBMIT_REPORT`, identical 25-byte report, same descriptor - (6×16-bit axes, hat, 96 buttons) — so the existing `HidFeederJoystickSink` - drives it unchanged (only the device path differs). Precedent: the - original FASA `tasgame.sys` was exactly an XP HID minidriver - (docs/Win32RIO/, analyzed); ours stays thin with serial in user mode. - Toolchain: **WDK 7.1.0** (last XP-capable kit) under `driver/RioGamepadXP/`; - XP x86 enforces no kernel signing, so install is just the INF — none of - the Phase 1 test-signing/Secure Boot friction exists there. - Acquisition order (decided): **the Xbox 360 pad (ViGEm) stays preferred - on 10/11** — net48: ViGEm → RioGamepad → Null (unchanged); - net40: RioGamepadXP → Null. - *Staging:* the XP app is useful before the driver lands — milestone 1 - ships keyboard/mouse + lamps + plasma (joystick = Null sink), the driver - follows as milestone 2. +- **8B — RioGamepadXP.sys — built ✅ (static; XP bring-up pending in 8E).** + Source in [`driver/RioGamepadXP/`](../driver/RioGamepadXP/): a WDM HID + minidriver (`HidRegisterMinidriver`, **polled mode** so there's no pending- + IRP/cancel machinery) presenting the 6-axis/hat/96-button joystick, plus a + named sideband control device (`\\.\RioGamepadXP`) that takes + `IOCTL_RIO_SUBMIT_REPORT` — the **identical `Public.h` contract** as the + modern driver (same 25-byte report, VID/PID). A dispatch wrapper routes the + control device's CREATE/CLOSE/DEVICE_CONTROL to us and forwards the HID FDO's + to hidclass. Builds with WDK 7.1.0 (`build.cmd` → `setenv … fre x86 WXP + no_oacr`) to `RioGamepadXP.sys` (x86, subsystem 5.01). XP enforces no kernel + signing, so install is unsigned; the device is root-enumerated so it needs + **devcon** (`devcon install RioGamepadXP.inf root\RioGamepadXP`, also built + from the WDK) rather than InstallHinfSection. The net40 feeder opens the + driver by name (`#if NET40` branch in `HidFeederJoystickSink`); XP can't + register a device interface on a bare control device (no PDO), the one + contract nuance. Bundled into the package's `vendor\xp\`. Precedent: the + original FASA `tasgame.sys` was itself an XP HID minidriver (docs/Win32RIO/, + analyzed); ours stays thin with serial in user mode. No third-party virtual + joystick (vJoy/PPJoy unmaintained). Acquisition order: net48 ViGEm → + RioGamepad → Null (unchanged); net40 RioGamepadXP → Null. ⏳ **8E:** the + driver compiles clean but is **unverified at runtime** — needs an XP target + to confirm joy.cpl enumeration + report flow. *Staging:* the app is useful + before the driver — milestone 1 ships keyboard/mouse + lamps + plasma + (joystick = Null sink) if the driver isn't present. - **8C — Tray on net40/x86 — done ✅:** multi-target `RioJoy.Tray` (net40 drops the ViGEm + RioJoy.Overlay references); gate `WallpaperMakerForm` + overlay generation; `WallpaperApplier` converts PNG→BMP via GDI+ before diff --git a/driver/RioGamepadXP/Public.h b/driver/RioGamepadXP/Public.h new file mode 100644 index 0000000..63668c2 --- /dev/null +++ b/driver/RioGamepadXP/Public.h @@ -0,0 +1,56 @@ +/*++ +RioGamepadXP — shared definitions for the Windows XP virtual HID gamepad driver +and its user-mode client (the RIOJoy net40 tray app's HID feeder). + +This is the XP-flavor sibling of driver/RioGamepad/Public.h. The contract is +IDENTICAL — same interface GUID, same IOCTL_RIO_SUBMIT_REPORT, same 25-byte +report layout, same VID/PID — so RioJoy.Core's HidFeederJoystickSink drives +either driver with only the device-open path differing. + +The one XP difference: XP-era hidclass minidrivers cannot register a device +interface on a bare control device (IoRegisterDeviceInterface needs a PDO), so +the sideband control device is reachable by a fixed symbolic-link name instead +of by interface GUID. The net40 feeder opens RIO_XP_USERMODE_PATH directly; the +net48 feeder keeps using SetupDi + GUID_DEVINTERFACE_RIOGAMEPAD. + +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 (same as the modern driver; kept for parity, and used +// if a future XP build ever exposes an interface). {b6a3f1c2-...} +// +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). +// Identical code to the modern driver. +// +#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 modern driver). +// +#define RIO_VENDOR_ID 0x1209 // pid.codes test/community VID +#define RIO_PRODUCT_ID 0x5249 // 'R','I' +#define RIO_VERSION 0x0100 + +// +// Sideband control-device names. The kernel object and its DOS-device symlink; +// the user-mode client opens RIO_XP_USERMODE_PATH. +// +#define RIO_XP_DEVICE_NAME L"\\Device\\RioGamepadXP" +#define RIO_XP_SYMLINK_NAME L"\\DosDevices\\RioGamepadXP" +#define RIO_XP_USERMODE_PATH "\\\\.\\RioGamepadXP" diff --git a/driver/RioGamepadXP/ReportDescriptor.h b/driver/RioGamepadXP/ReportDescriptor.h new file mode 100644 index 0000000..5aaa68b --- /dev/null +++ b/driver/RioGamepadXP/ReportDescriptor.h @@ -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) +}; diff --git a/driver/RioGamepadXP/RioGamepadXP.inf b/driver/RioGamepadXP/RioGamepadXP.inf new file mode 100644 index 0000000..bad4aa4 --- /dev/null +++ b/driver/RioGamepadXP/RioGamepadXP.inf @@ -0,0 +1,53 @@ +; +; RioGamepadXP.inf — RIOJoy virtual HID gamepad for Windows XP (WDM HID +; minidriver, root-enumerated). No catalog / signing: 32-bit XP does not +; enforce kernel-mode driver signing, so this installs unsigned. +; +; Install (see deploy\install-core.bat): +; devcon.exe install RioGamepadXP.inf root\RioGamepadXP +; + +[Version] +Signature = "$WINDOWS NT$" +Class = HIDClass +ClassGuid = {745a17a0-74d3-11d0-b6fe-00a0c90f57da} +Provider = %ManufacturerName% +DriverVer = 07/11/2026,1.0.0.0 + +[DestinationDirs] +DefaultDestDir = 12 ; %SystemRoot%\System32\drivers +RioGamepadXP_CopyFiles = 12 + +[SourceDisksNames] +1 = %DiskName% + +[SourceDisksFiles] +RioGamepadXP.sys = 1 + +[Manufacturer] +%ManufacturerName% = Standard, NTx86 + +[Standard.NTx86] +%DeviceName% = RioGamepadXP_Device, root\RioGamepadXP + +[RioGamepadXP_Device.NT] +CopyFiles = RioGamepadXP_CopyFiles + +[RioGamepadXP_CopyFiles] +RioGamepadXP.sys + +[RioGamepadXP_Device.NT.Services] +AddService = RioGamepadXP, 0x00000002, RioGamepadXP_Service + +[RioGamepadXP_Service] +DisplayName = %ServiceName% +ServiceType = 1 ; SERVICE_KERNEL_DRIVER +StartType = 3 ; SERVICE_DEMAND_START +ErrorControl = 1 ; SERVICE_ERROR_NORMAL +ServiceBinary = %12%\RioGamepadXP.sys + +[Strings] +ManufacturerName = "VWE" +DiskName = "RioGamepadXP Installation Disk" +DeviceName = "RIOJoy Virtual Gamepad (XP)" +ServiceName = "RIOJoy Virtual Gamepad Service (XP)" diff --git a/driver/RioGamepadXP/build.cmd b/driver/RioGamepadXP/build.cmd new file mode 100644 index 0000000..0dc2f70 --- /dev/null +++ b/driver/RioGamepadXP/build.cmd @@ -0,0 +1,7 @@ +@echo off +rem Build RioGamepadXP.sys for Windows XP (x86, free). Requires WDK 7.1.0. +rem Override WDK if installed elsewhere: set WDK= before calling. +if "%WDK%"=="" set WDK=C:\WinDDK\WinDDK\7600.16385.win7_wdk.100208-1538 +call %WDK%\bin\setenv.bat %WDK% fre x86 WXP no_oacr +cd /d %~dp0 +build -cZ diff --git a/driver/RioGamepadXP/makefile b/driver/RioGamepadXP/makefile new file mode 100644 index 0000000..5acbbd2 --- /dev/null +++ b/driver/RioGamepadXP/makefile @@ -0,0 +1 @@ +!INCLUDE $(NTMAKEENV)\makefile.def diff --git a/driver/RioGamepadXP/rioxp.c b/driver/RioGamepadXP/rioxp.c new file mode 100644 index 0000000..dda2a66 --- /dev/null +++ b/driver/RioGamepadXP/rioxp.c @@ -0,0 +1,381 @@ +/*++ +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; +} diff --git a/driver/RioGamepadXP/sources b/driver/RioGamepadXP/sources new file mode 100644 index 0000000..325b5c3 --- /dev/null +++ b/driver/RioGamepadXP/sources @@ -0,0 +1,7 @@ +TARGETNAME=RioGamepadXP +TARGETTYPE=DRIVER + +# Link against hidclass's minidriver import library (HidRegisterMinidriver). +TARGETLIBS=$(DDK_LIB_PATH)\hidclass.lib + +SOURCES=rioxp.c diff --git a/src/RioJoy.Core/Output/HidFeederJoystickSink.cs b/src/RioJoy.Core/Output/HidFeederJoystickSink.cs index 56066d9..3df4632 100644 --- a/src/RioJoy.Core/Output/HidFeederJoystickSink.cs +++ b/src/RioJoy.Core/Output/HidFeederJoystickSink.cs @@ -36,7 +36,15 @@ public sealed class HidFeederJoystickSink : IJoystickSink, IDisposable public static bool TryCreate(out HidFeederJoystickSink? sink) { sink = null; +#if NET40 + // Windows XP: RioGamepadXP is a HID minidriver whose sideband control + // device can't carry a device interface (no PDO), so it's opened by its + // fixed symbolic-link name rather than via SetupDi + interface GUID. + // Contract is otherwise identical (see driver/RioGamepadXP/Public.h). + string? path = @"\\.\RioGamepadXP"; +#else string? path = FindDevicePath(); +#endif if (path is null) return false;