[PATCH 5/6] xinput.sys: Create an internal PDO, on the XINPUT bus.

Rémi Bernon rbernon at codeweavers.com
Thu Aug 26 00:59:03 CDT 2021


This internal xinput PDO is an HID compatible pass-through device, but
it needs to be kept private and is listed on the internal XINPUT device
interface class, instead of the public HID device interface class.

It filters the report read requests and copies the input reports to the
pending gamepad PDO read requests, completing them at the same time.

This is a Wine extension for convenience and native XInput driver uses a
different, undocumented, device interface.

Signed-off-by: Rémi Bernon <rbernon at codeweavers.com>
---
 dlls/hidclass.sys/hid.h     |   1 +
 dlls/hidclass.sys/pnp.c     |   6 +-
 dlls/xinput.sys/Makefile.in |   2 +-
 dlls/xinput.sys/main.c      | 226 +++++++++++++++++++++++++++++++++++-
 4 files changed, 230 insertions(+), 5 deletions(-)

diff --git a/dlls/hidclass.sys/hid.h b/dlls/hidclass.sys/hid.h
index 1ca6a926872..4fa9e72e615 100644
--- a/dlls/hidclass.sys/hid.h
+++ b/dlls/hidclass.sys/hid.h
@@ -84,6 +84,7 @@ typedef struct _BASE_DEVICE_EXTENSION
      * for convenience. */
     WCHAR device_id[MAX_DEVICE_ID_LEN];
     WCHAR instance_id[MAX_DEVICE_ID_LEN];
+    const GUID *interface_guid;
 
     BOOL is_fdo;
 } BASE_DEVICE_EXTENSION;
diff --git a/dlls/hidclass.sys/pnp.c b/dlls/hidclass.sys/pnp.c
index db45aea2fd3..60aaa2e4a6f 100644
--- a/dlls/hidclass.sys/pnp.c
+++ b/dlls/hidclass.sys/pnp.c
@@ -38,6 +38,7 @@
 WINE_DEFAULT_DEBUG_CHANNEL(hid);
 
 DEFINE_DEVPROPKEY(DEVPROPKEY_HID_HANDLE, 0xbc62e415, 0xf4fe, 0x405c, 0x8e, 0xda, 0x63, 0x6f, 0xb5, 0x9f, 0x08, 0x98, 2);
+DEFINE_GUID(GUID_DEVINTERFACE_XINPUT, 0xec87f1e3, 0xc13b, 0x4100, 0xb5, 0xf7, 0x8b, 0x84, 0xd5, 0x42, 0x60, 0xcb);
 
 #if defined(__i386__) && !defined(_WIN32)
 
@@ -165,6 +166,8 @@ static NTSTATUS WINAPI driver_add_device(DRIVER_OBJECT *driver, DEVICE_OBJECT *b
     ext->u.fdo.hid_ext.NextDeviceObject = bus_pdo;
     swprintf(ext->device_id, ARRAY_SIZE(ext->device_id), L"HID\\%s", wcsrchr(device_id, '\\') + 1);
     wcscpy(ext->instance_id, instance_id);
+    ext->interface_guid = !wcsncmp(device_id, L"XINPUT\\", 7) ? &GUID_DEVINTERFACE_XINPUT
+                                                              : &GUID_DEVINTERFACE_HID;
 
     status = minidriver->AddDevice(minidriver->minidriver.DriverObject, fdo);
     if (status != STATUS_SUCCESS)
@@ -220,6 +223,7 @@ static void create_child(minidriver *minidriver, DEVICE_OBJECT *fdo)
     KeInitializeSpinLock(&pdo_ext->u.pdo.irp_queue_lock);
     wcscpy(pdo_ext->device_id, fdo_ext->device_id);
     wcscpy(pdo_ext->instance_id, fdo_ext->instance_id);
+    pdo_ext->interface_guid = fdo_ext->interface_guid;
 
     pdo_ext->u.pdo.information.VendorID = attr.VendorID;
     pdo_ext->u.pdo.information.ProductID = attr.ProductID;
@@ -445,7 +449,7 @@ static NTSTATUS pdo_pnp(DEVICE_OBJECT *device, IRP *irp)
         }
 
         case IRP_MN_START_DEVICE:
-            if ((status = IoRegisterDeviceInterface(device, &GUID_DEVINTERFACE_HID, NULL, &ext->u.pdo.link_name)))
+            if ((status = IoRegisterDeviceInterface(device, ext->interface_guid, NULL, &ext->u.pdo.link_name)))
             {
                 ERR("Failed to register interface, status %#x.\n", status);
                 break;
diff --git a/dlls/xinput.sys/Makefile.in b/dlls/xinput.sys/Makefile.in
index 52b00ef0528..f75c92a189f 100644
--- a/dlls/xinput.sys/Makefile.in
+++ b/dlls/xinput.sys/Makefile.in
@@ -1,5 +1,5 @@
 MODULE        = xinput.sys
-IMPORTS       = ntoskrnl
+IMPORTS       = ntoskrnl hidparse
 EXTRADLLFLAGS = -mno-cygwin -Wl,--subsystem,native
 
 C_SRCS = \
diff --git a/dlls/xinput.sys/main.c b/dlls/xinput.sys/main.c
index d1c547f0572..15d9a8b4735 100644
--- a/dlls/xinput.sys/main.c
+++ b/dlls/xinput.sys/main.c
@@ -20,6 +20,7 @@
 
 #include <stdarg.h>
 #include <stdlib.h>
+#include <assert.h>
 
 #include "ntstatus.h"
 #define WIN32_NO_STATUS
@@ -31,6 +32,7 @@
 #include "cfgmgr32.h"
 #include "ddk/wdm.h"
 #include "ddk/hidport.h"
+#include "ddk/hidpddi.h"
 
 #include "wine/debug.h"
 
@@ -52,14 +54,24 @@ struct device_state
 {
     DEVICE_OBJECT *bus_pdo;
     DEVICE_OBJECT *gamepad_pdo;
+    DEVICE_OBJECT *xinput_pdo;
 
     WCHAR bus_id[MAX_DEVICE_ID_LEN];
     WCHAR instance_id[MAX_DEVICE_ID_LEN];
+
+    /* everything below requires holding the cs */
+    CRITICAL_SECTION cs;
+    IRP *pending_read;
+    BOOL pending_is_gamepad;
+
+    ULONG report_len;
+    char *report_buf;
 };
 
 struct device_extension
 {
     BOOL is_fdo;
+    BOOL is_gamepad;
     BOOL removed;
 
     WCHAR device_id[MAX_DEVICE_ID_LEN];
@@ -71,6 +83,111 @@ static inline struct device_extension *ext_from_DEVICE_OBJECT(DEVICE_OBJECT *dev
     return (struct device_extension *)device->DeviceExtension;
 }
 
+static NTSTATUS sync_ioctl(DEVICE_OBJECT *device, DWORD code, void *in_buf, DWORD in_len, void *out_buf, DWORD out_len)
+{
+    IO_STATUS_BLOCK io;
+    KEVENT event;
+    IRP *irp;
+
+    KeInitializeEvent(&event, NotificationEvent, FALSE);
+    irp = IoBuildDeviceIoControlRequest(code, device, in_buf, in_len, out_buf, out_len, TRUE, &event, &io);
+    if (IoCallDriver(device, irp) == STATUS_PENDING) KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);
+
+    return io.Status;
+}
+
+/* check for a pending read from the other PDO, and complete both at a time.
+ * if there's none, save irp as pending, the other PDO will complete it.
+ * if the device is being removed, complete irp with an error. */
+static NTSTATUS try_complete_pending_read(DEVICE_OBJECT *device, IRP *irp)
+{
+    struct device_extension *ext = ext_from_DEVICE_OBJECT(device);
+    IRP *pending, *xinput_irp, *gamepad_irp;
+    struct device_state *state = ext->state;
+    BOOL removed, pending_is_gamepad;
+    NTSTATUS status;
+    ULONG offset;
+
+    RtlEnterCriticalSection(&state->cs);
+    pending_is_gamepad = state->pending_is_gamepad;
+    if ((removed = ext->removed))
+        pending = NULL;
+    else if ((pending = state->pending_read))
+        state->pending_read = NULL;
+    else
+    {
+        state->pending_read = irp;
+        state->pending_is_gamepad = ext->is_gamepad;
+        IoMarkIrpPending(irp);
+    }
+    RtlLeaveCriticalSection(&state->cs);
+
+    if (removed)
+    {
+        irp->IoStatus.Status = STATUS_DELETE_PENDING;
+        irp->IoStatus.Information = 0;
+        IoCompleteRequest(irp, IO_NO_INCREMENT);
+        return STATUS_DELETE_PENDING;
+    }
+
+    if (!pending) return STATUS_PENDING;
+
+    /* only one read at a time per device from hidclass.sys design */
+    assert(pending_is_gamepad != ext->is_gamepad);
+    gamepad_irp = ext->is_gamepad ? irp : pending;
+    xinput_irp = ext->is_gamepad ? pending : irp;
+
+    offset = state->report_buf[0] ? 0 : 1;
+    if (!(status = sync_ioctl(state->bus_pdo, IOCTL_HID_READ_REPORT, NULL, 0,
+                              state->report_buf + offset, state->report_len - offset)))
+    {
+        memcpy(xinput_irp->UserBuffer, state->report_buf + offset, state->report_len - offset);
+        xinput_irp->IoStatus.Information = state->report_len;
+        memcpy(gamepad_irp->UserBuffer, state->report_buf + offset, state->report_len - offset);
+        gamepad_irp->IoStatus.Information = state->report_len;
+    }
+
+    xinput_irp->IoStatus.Status = status;
+    IoCompleteRequest(xinput_irp, IO_NO_INCREMENT);
+
+    gamepad_irp->IoStatus.Status = status;
+    IoCompleteRequest(gamepad_irp, IO_NO_INCREMENT);
+    return status;
+}
+
+static NTSTATUS WINAPI gamepad_pdo_internal_ioctl(DEVICE_OBJECT *device, IRP *irp)
+{
+    struct device_extension *ext = ext_from_DEVICE_OBJECT(device);
+    IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation(irp);
+    ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
+    struct device_state *state = ext->state;
+
+    TRACE("device %p, irp %p, code %#x, bus_pdo %p.\n", device, irp, code, state->bus_pdo);
+
+    switch (code)
+    {
+    case IOCTL_HID_GET_INPUT_REPORT:
+    {
+        HID_XFER_PACKET *packet = (HID_XFER_PACKET *)irp->UserBuffer;
+
+        RtlEnterCriticalSection(&state->cs);
+        memcpy(packet->reportBuffer, state->report_buf, state->report_len);
+        irp->IoStatus.Information = state->report_len;
+        RtlLeaveCriticalSection(&state->cs);
+
+        irp->IoStatus.Status = STATUS_SUCCESS;
+        IoCompleteRequest(irp, IO_NO_INCREMENT);
+        return STATUS_SUCCESS;
+    }
+
+    default:
+        IoSkipCurrentIrpStackLocation(irp);
+        return IoCallDriver(state->bus_pdo, irp);
+    }
+
+    return STATUS_SUCCESS;
+}
+
 static NTSTATUS WINAPI internal_ioctl(DEVICE_OBJECT *device, IRP *irp)
 {
     struct device_extension *ext = ext_from_DEVICE_OBJECT(device);
@@ -88,10 +205,74 @@ static NTSTATUS WINAPI internal_ioctl(DEVICE_OBJECT *device, IRP *irp)
 
     TRACE("device %p, irp %p, code %#x, bus_pdo %p.\n", device, irp, code, state->bus_pdo);
 
+    if (code == IOCTL_HID_READ_REPORT) return try_complete_pending_read(device, irp);
+    if (ext->is_gamepad) return gamepad_pdo_internal_ioctl(device, irp);
+
     IoSkipCurrentIrpStackLocation(irp);
     return IoCallDriver(state->bus_pdo, irp);
 }
 
+static NTSTATUS gamepad_pdo_start_device(DEVICE_OBJECT *device)
+{
+    struct device_extension *ext = ext_from_DEVICE_OBJECT(device);
+    struct device_state *state = ext->state;
+    ULONG i, report_desc_len, report_count;
+    PHIDP_REPORT_DESCRIPTOR report_desc;
+    PHIDP_PREPARSED_DATA preparsed;
+    HIDP_DEVICE_DESC device_desc;
+    HIDP_REPORT_IDS *reports;
+    HID_DESCRIPTOR hid_desc;
+    NTSTATUS status;
+    HIDP_CAPS caps;
+
+    if ((status = sync_ioctl(state->bus_pdo, IOCTL_HID_GET_DEVICE_DESCRIPTOR, NULL, 0, &hid_desc, sizeof(hid_desc))))
+        return status;
+
+    if (!(report_desc_len = hid_desc.DescriptorList[0].wReportLength)) return STATUS_UNSUCCESSFUL;
+    if (!(report_desc = malloc(report_desc_len))) return STATUS_NO_MEMORY;
+
+    status = sync_ioctl(state->bus_pdo, IOCTL_HID_GET_REPORT_DESCRIPTOR, NULL, 0, report_desc, report_desc_len);
+    if (!status) status = HidP_GetCollectionDescription(report_desc, report_desc_len, PagedPool, &device_desc);
+    free(report_desc);
+    if (status != HIDP_STATUS_SUCCESS) return status;
+
+    preparsed = device_desc.CollectionDesc->PreparsedData;
+    status = HidP_GetCaps(preparsed, &caps);
+    if (status != HIDP_STATUS_SUCCESS) WARN("HidP_GetCaps returned %#x\n", status);
+
+    reports = device_desc.ReportIDs;
+    report_count = device_desc.ReportIDsLength;
+    for (i = 0; i < report_count; ++i) if (!reports[i].ReportID || reports[i].InputLength) break;
+    if (i == report_count) i = 0; /* no input report?!, just use first ID */
+
+    status = STATUS_SUCCESS;
+    RtlEnterCriticalSection(&state->cs);
+    state->report_len = caps.InputReportByteLength;
+    if (!(state->report_buf = malloc(state->report_len))) status = STATUS_NO_MEMORY;
+    else state->report_buf[0] = reports[i].ReportID;
+    RtlLeaveCriticalSection(&state->cs);
+
+    HidP_FreeCollectionDescription(&device_desc);
+    return status;
+}
+
+static NTSTATUS gamepad_pdo_remove_device(DEVICE_OBJECT *device)
+{
+    struct device_extension *ext = ext_from_DEVICE_OBJECT(device);
+    struct device_state *state = ext->state;
+    char *report_buf;
+
+    RtlEnterCriticalSection(&state->cs);
+    report_buf = state->report_buf;
+    state->report_buf = NULL;
+    state->report_len = 0;
+    RtlLeaveCriticalSection(&state->cs);
+
+    free(report_buf);
+
+    return STATUS_SUCCESS;
+}
+
 static WCHAR *query_instance_id(DEVICE_OBJECT *device)
 {
     struct device_extension *ext = ext_from_DEVICE_OBJECT(device);
@@ -153,21 +334,36 @@ static NTSTATUS WINAPI pdo_pnp(DEVICE_OBJECT *device, IRP *irp)
     struct device_state *state = ext->state;
     ULONG code = stack->MinorFunction;
     NTSTATUS status;
+    IRP *pending;
 
     TRACE("device %p, irp %p, code %#x, bus_pdo %p.\n", device, irp, code, state->bus_pdo);
 
     switch (code)
     {
     case IRP_MN_START_DEVICE:
-        status = STATUS_SUCCESS;
+        if (ext->is_gamepad) status = gamepad_pdo_start_device(device);
+        else status = STATUS_SUCCESS;
         break;
 
     case IRP_MN_SURPRISE_REMOVAL:
         status = STATUS_SUCCESS;
         if (InterlockedExchange(&ext->removed, TRUE)) break;
+
+        RtlEnterCriticalSection(&state->cs);
+        pending = state->pending_read;
+        state->pending_read = NULL;
+        RtlLeaveCriticalSection(&state->cs);
+
+        if (pending)
+        {
+            pending->IoStatus.Status = STATUS_DELETE_PENDING;
+            pending->IoStatus.Information = 0;
+            IoCompleteRequest(pending, IO_NO_INCREMENT);
+        }
         break;
 
     case IRP_MN_REMOVE_DEVICE:
+        if (ext->is_gamepad) gamepad_pdo_remove_device(device);
         irp->IoStatus.Status = STATUS_SUCCESS;
         IoCompleteRequest(irp, IO_NO_INCREMENT);
         IoDeleteDevice(device);
@@ -211,7 +407,7 @@ static void create_child_pdos(DEVICE_OBJECT *fdo)
 {
     struct device_extension *fdo_ext = fdo->DeviceExtension, *pdo_ext;
     struct device_state *state = fdo_ext->state;
-    DEVICE_OBJECT *gamepad_pdo;
+    DEVICE_OBJECT *xinput_pdo, *gamepad_pdo;
     UNICODE_STRING string;
     WCHAR *tmp, pdo_name[255];
     NTSTATUS status;
@@ -224,16 +420,33 @@ static void create_child_pdos(DEVICE_OBJECT *fdo)
         return;
     }
 
+    swprintf(pdo_name, ARRAY_SIZE(pdo_name), L"\\Device\\XINPUT#%p&%p", fdo->DriverObject, state->bus_pdo);
+    RtlInitUnicodeString(&string, pdo_name);
+    if ((status = IoCreateDevice(fdo->DriverObject, sizeof(*pdo_ext), &string, 0, 0, FALSE, &xinput_pdo)))
+    {
+        ERR( "failed to create xinput PDO, status %#x.\n", status );
+        IoDeleteDevice(gamepad_pdo);
+        return;
+    }
+
     pdo_ext = gamepad_pdo->DeviceExtension;
     pdo_ext->is_fdo = FALSE;
+    pdo_ext->is_gamepad = TRUE;
     pdo_ext->state = state;
     wcscpy(pdo_ext->device_id, fdo_ext->device_id);
     if ((tmp = wcsstr(pdo_ext->device_id, L"&MI_"))) memcpy(tmp, L"&IG", 6);
     else wcscat(pdo_ext->device_id, L"&IG_00");
 
+    pdo_ext = xinput_pdo->DeviceExtension;
+    pdo_ext->is_fdo = FALSE;
+    pdo_ext->is_gamepad = FALSE;
+    pdo_ext->state = state;
+    swprintf(pdo_ext->device_id, MAX_DEVICE_ID_LEN, L"XINPUT\\%s", wcsrchr(fdo_ext->device_id, '\\') + 1);
+
     state->gamepad_pdo = gamepad_pdo;
+    state->xinput_pdo = xinput_pdo;
 
-    TRACE("fdo %p, gamepad PDO %p.\n", fdo, gamepad_pdo);
+    TRACE("fdo %p, xinput PDO %p, gamepad PDO %p.\n", fdo, xinput_pdo, gamepad_pdo);
 
     IoInvalidateDeviceRelations(state->bus_pdo, BusRelations);
 }
@@ -263,6 +476,12 @@ static NTSTATUS WINAPI fdo_pnp(DEVICE_OBJECT *device, IRP *irp)
             }
 
             devices->Count = 0;
+            if ((child = state->xinput_pdo))
+            {
+                devices->Objects[devices->Count] = child;
+                call_fastcall_func1(ObfReferenceObject, child);
+                devices->Count++;
+            }
             if ((child = state->gamepad_pdo))
             {
                 devices->Objects[devices->Count] = child;
@@ -374,6 +593,7 @@ static NTSTATUS WINAPI add_device(DRIVER_OBJECT *driver, DEVICE_OBJECT *bus_pdo)
     wcscpy(state->bus_id, bus_id);
     swprintf(ext->device_id, MAX_DEVICE_ID_LEN, L"%s\\%s", bus_id, device_id);
     wcscpy(state->instance_id, instance_id);
+    RtlInitializeCriticalSection(&state->cs);
 
     TRACE("fdo %p, bus %s, device %s, instance %s.\n", fdo, debugstr_w(state->bus_id), debugstr_w(ext->device_id), debugstr_w(state->instance_id));
 
-- 
2.33.0




More information about the wine-devel mailing list