[PATCH 0/1] MR11205: Winebus usb compatible ids
### What winebus reports the USB class compatible IDs (`USB\Class_03&SubClass_xx&Prot_y`y and the two shorter forms) for USB devices, alongside the existing WINEBUS\\WINE_COMP_HID id. wineusb.sys already reports these IDs in this format for the devices it enumerates (dlls/wineusb.sys/wineusb.c, get_compatible_ids). winebus does not, so its synthesized USB-HID devices look different from a real Windows USB-HID device to any application that validates the USB parent. This makes winebus do the same. ### Why On Windows the USB parent carries these IDs and applications validate them through `CM_Get_Parent` or `SetupDi`. Wine omits them, so those applications fail. The Elgato Stream Deck app is one concrete case: it reports "no device found" for a connected Stream Deck Mini and recognises it once the IDs are present. winebus has no separate USB interface device to host the IDs. On a real stack the interface PDO from usbccgp is the HID device's parent; under winebus the IDs are reported on the HID device itself, which is the device an application reaches from the HID node. Only the hosting device differs from Windows; the IDs and their format match what wineusb already reports. ### How The interface class, subclass and protocol come from the udev usb_interface node (bInterfaceClass, bInterfaceSubClass, bInterfaceProtocol). get_compatible_ids formats them the same way wineusb.sys does, for USB devices that carry interface information, and keeps the existing ids ahead of them so driver binding is unchanged. ### Testing No automated test: winebus has no unit harness for the udev backend (the dinput HID test drives its own mock bus and never reaches this path), and the IDs derive from live sysfs values. Verified by hand: with the change the USB parent's CompatibleIds gains `USB\Class_03&SubClass_00&Prot_00` and the two shorter forms (read live from sysfs) alongside the existing WINE_COMP_HID, and the Elgato Stream Deck app then detects a connected Stream Deck Mini that it otherwise rejects. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59891 Signed-off-by: Thomas Portal portal.thomas@protonmail.com -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11205
From: Thomas Portal <portal.thomas@protonmail.com> Wine's synthesized HID devices only carry the WINEBUS\WINE_COMP_HID compatible ID. A real Windows USB-HID device also exposes the USB class compatible IDs (USB\Class_03&SubClass_xx&Prot_yy and the two shorter forms) on its USB interface parent. Applications that walk to that parent and validate the IDs, for example the Elgato Stream Deck app through CM_Get_Parent, reject the device when they are missing. winebus does not enumerate a separate USB interface device, so report the IDs on the HID device itself, which is what such an application reaches from the HID node. wineusb.sys already reports the same IDs in the same format for the devices it enumerates, so only the hosting device differs. The class, subclass and protocol come from the udev interface and are reported only for USB devices that carry interface information, which leaves the other backends unchanged. The existing WINE_COMP_XINPUT and WINE_COMP_HID ids stay ahead of them so driver binding does not change. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59891 Signed-off-by: Thomas Portal <portal.thomas@protonmail.com> --- dlls/winebus.sys/bus_udev.c | 13 +++++++++++++ dlls/winebus.sys/main.c | 39 ++++++++++++++++++++++++++++++++----- dlls/winebus.sys/unixlib.h | 8 ++++++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/dlls/winebus.sys/bus_udev.c b/dlls/winebus.sys/bus_udev.c index b307124c3be..e1714d0fa78 100644 --- a/dlls/winebus.sys/bus_udev.c +++ b/dlls/winebus.sys/bus_udev.c @@ -1174,6 +1174,17 @@ static void get_device_subsystem_info(struct udev_device *dev, const char *subsy } } +static void get_usb_interface_info(struct udev_device *dev, struct device_desc *desc) +{ + struct udev_device *iface; + const char *tmp; + + if (!(iface = udev_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_interface"))) return; + if ((tmp = udev_device_get_sysattr_value(iface, "bInterfaceClass"))) sscanf(tmp, "%x", &desc->class); + if ((tmp = udev_device_get_sysattr_value(iface, "bInterfaceSubClass"))) sscanf(tmp, "%x", &desc->subclass); + if ((tmp = udev_device_get_sysattr_value(iface, "bInterfaceProtocol"))) sscanf(tmp, "%x", &desc->protocol); +} + static NTSTATUS hidraw_device_create(struct udev_device *dev, int fd, const char *devnode, struct device_desc desc) { #ifdef HAVE_LINUX_HIDRAW_H @@ -1370,6 +1381,8 @@ static void udev_add_device(struct udev_device *dev, int fd) if (bus == BUS_BLUETOOTH) desc.bus_type = BUS_TYPE_BLUETOOTH; else if (bus == BUS_USB) desc.bus_type = BUS_TYPE_USB; + if (desc.bus_type == BUS_TYPE_USB) get_usb_interface_info(dev, &desc); + if (!(subsystem = udev_device_get_subsystem(dev))) { WARN("udev_device_get_subsystem failed for %s.\n", debugstr_a(devnode)); diff --git a/dlls/winebus.sys/main.c b/dlls/winebus.sys/main.c index 0132297a4a6..55e580e0350 100644 --- a/dlls/winebus.sys/main.c +++ b/dlls/winebus.sys/main.c @@ -262,16 +262,45 @@ static WCHAR *get_compatible_ids(DEVICE_OBJECT *device) static const WCHAR xinput_compat[] = L"WINEBUS\\WINE_COMP_XINPUT"; static const WCHAR hid_compat[] = L"WINEBUS\\WINE_COMP_HID"; struct device_extension *ext = (struct device_extension *)device->DeviceExtension; - DWORD size = sizeof(hid_compat); - WCHAR *dst; + WCHAR usb_compat[3][40]; + DWORD usb_count = 0, size, len, i; + WCHAR *dst, *pos; + + /* A real Windows USB-HID device exposes the USB class compatible IDs on its USB + interface parent. winebus has no separate interface device, so report them on + the HID device, where an application reaches them from the HID node through + CM_Get_Parent. desc.class stays zero for backends that read no USB interface, + which leaves them unchanged. */ + if (ext->desc.bus_type == BUS_TYPE_USB && ext->desc.class) + { + swprintf(usb_compat[usb_count++], ARRAY_SIZE(usb_compat[0]), + L"USB\\Class_%02x&SubClass_%02x&Prot_%02x", ext->desc.class, ext->desc.subclass, ext->desc.protocol); + swprintf(usb_compat[usb_count++], ARRAY_SIZE(usb_compat[0]), + L"USB\\Class_%02x&SubClass_%02x", ext->desc.class, ext->desc.subclass); + swprintf(usb_compat[usb_count++], ARRAY_SIZE(usb_compat[0]), L"USB\\Class_%02x", ext->desc.class); + } + size = sizeof(hid_compat); if (ext->desc.is_gamepad) size += sizeof(xinput_compat); + for (i = 0; i < usb_count; i++) size += (wcslen(usb_compat[i]) + 1) * sizeof(WCHAR); if ((dst = ExAllocatePool(PagedPool, size + sizeof(WCHAR)))) { - if (ext->desc.is_gamepad) memcpy(dst, xinput_compat, sizeof(xinput_compat)); - memcpy((char *)dst + size - sizeof(hid_compat), hid_compat, sizeof(hid_compat)); - dst[size / sizeof(WCHAR)] = 0; + pos = dst; + if (ext->desc.is_gamepad) + { + memcpy(pos, xinput_compat, sizeof(xinput_compat)); + pos += sizeof(xinput_compat) / sizeof(WCHAR); + } + memcpy(pos, hid_compat, sizeof(hid_compat)); + pos += sizeof(hid_compat) / sizeof(WCHAR); + for (i = 0; i < usb_count; i++) + { + len = wcslen(usb_compat[i]) + 1; + memcpy(pos, usb_compat[i], len * sizeof(WCHAR)); + pos += len; + } + *pos = 0; } return dst; diff --git a/dlls/winebus.sys/unixlib.h b/dlls/winebus.sys/unixlib.h index d2b17f4c6d3..fa99c62ea40 100644 --- a/dlls/winebus.sys/unixlib.h +++ b/dlls/winebus.sys/unixlib.h @@ -39,6 +39,9 @@ struct device_desc UINT input; UINT uid; UINT bus_type; + UINT class; + UINT subclass; + UINT protocol; BOOL is_gamepad; BOOL is_hidraw; @@ -159,9 +162,10 @@ enum unix_funcs static inline const char *debugstr_device_desc(struct device_desc *desc) { if (!desc) return "(null)"; - return wine_dbg_sprintf("{vid %04x, pid %04x, version %04x, input %d, uid %08x, is_gamepad %u, is_hidraw %u, bus_type %u}", + return wine_dbg_sprintf("{vid %04x, pid %04x, version %04x, input %d, uid %08x, is_gamepad %u, is_hidraw %u, bus_type %u, class %02x, subclass %02x, protocol %02x}", desc->vid, desc->pid, desc->version, desc->input, desc->uid, - desc->is_gamepad, desc->is_hidraw, desc->bus_type); + desc->is_gamepad, desc->is_hidraw, desc->bus_type, + desc->class, desc->subclass, desc->protocol); } static inline BOOL is_xbox_gamepad(WORD vid, WORD pid) -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11205
This seems risky; if we expose USB compatible IDs it's possible that something will break expecting this to be an actual USB device... -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11205#note_143710
On Fri Jun 19 21:59:14 2026 +0000, Elizabeth Figura wrote:
This seems risky; if we expose USB compatible IDs it's possible that something will break expecting this to be an actual USB device... Fair concern! The change doesn't make the device claim to be USB: its hardware id stays `WINEBUS\VID_xxxx&PID_xxxx` (not `USB\VIDsomething`), and it registers no `GUID_DEVINTERFACE_USB_DEVICE`, so nothing identifies or enumerates it as a USB device. Only its compatible-id fallback list gains the USB\\Class\_\* strings, which are the same ones a HID device's USB interface parent exposes on Windows (and that wineusb.sys already emits for the devices it enumerates).
For binding: it still binds winehid through `WINEBUS\WINE_COMP_HID`, and no INF in the tree matches USB\\Class\_\* (I checked winebus, winehid, input.inf, mouhid, winexinput, but let me know if I missed any), so the new ids never participate in matching. The only in-tree code that consumes a USB\\Class id is an application running its own CM_Get_Parent / SetupDi check against the HID device's parent, which is the case this fixes. Otherwise, I think the heavier alternative is to synthesize a real USB interface PDO so the parent is genuinely a USB node, but that's a larger change to winebus' single-PDO model and I kept this to the compat-id strings the app actually reads. If you'd prefer that direction, or you have a specific consumer the strings would mislead, I'll follow that. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11205#note_143713
Fair concern! The change doesn't make the device claim to be USB: its hardware id stays `WINEBUS\VID_xxxx&PID_xxxx` (not `USB\VIDsomething`), and it registers no `GUID_DEVINTERFACE_USB_DEVICE`, so nothing identifies or enumerates it as a USB device. Only its compatible-id fallback list gains the USB\\Class\_\* strings, which are the same ones a HID device's USB interface parent exposes on Windows (and that wineusb.sys already emits for the devices it enumerates).
No, the compatibility ID *is* what claiming to be a USB device is, at least from the kernel perspective. It means you can throw USB ioctls at it. The device name doesn't matter at all, at least it's not supposed to.
For binding: it still binds winehid through `WINEBUS\WINE_COMP_HID`, and no INF in the tree matches USB\\Class\_\* (I checked winebus, winehid, input.inf, mouhid, winexinput, but let me know if I missed any), so the new ids never participate in matching. The only in-tree code that consumes a USB\\Class id is an application running its own CM_Get_Parent / SetupDi check against the HID device's parent, which is the case this fixes.
Hmm, I guess if the winebus ID takes priority we should always choose winebus as the driver, so this should be safe after all. Although I'm not sure setupapi prioritizes correctly, but I'm less concerned about that for now. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11205#note_143718
On Sat Jun 20 06:22:38 2026 +0000, Elizabeth Figura wrote:
Fair concern! The change doesn't make the device claim to be USB: its hardware id stays `WINEBUS\VID_xxxx&PID_xxxx` (not `USB\VIDsomething`), and it registers no `GUID_DEVINTERFACE_USB_DEVICE`, so nothing identifies or enumerates it as a USB device. Only its compatible-id fallback list gains the USB\\Class\_\* strings, which are the same ones a HID device's USB interface parent exposes on Windows (and that wineusb.sys already emits for the devices it enumerates). No, the compatibility ID *is* what claiming to be a USB device is, at least from the kernel perspective. It means you can throw USB ioctls at it. The device name doesn't matter at all, at least it's not supposed to. For binding: it still binds winehid through `WINEBUS\WINE_COMP_HID`, and no INF in the tree matches USB\\Class\_\* (I checked winebus, winehid, input.inf, mouhid, winexinput, but let me know if I missed any), so the new ids never participate in matching. The only in-tree code that consumes a USB\\Class id is an application running its own CM_Get_Parent / SetupDi check against the HID device's parent, which is the case this fixes. Hmm, I guess if the winebus ID takes priority we should always choose winebus as the driver, so this should be safe after all. Although I'm not sure setupapi prioritizes correctly, but I'm less concerned about that for now. Oh good correction, thanks! And right, setupapi's ranking only matters if something actually matches USB\\Class\_\*, which nothing in the tree does today. If an INF ever adds that match, that's the point to look again.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11205#note_143720
participants (3)
-
Elizabeth Figura (@zfigura) -
Thomas Portal -
Thomas Portal (@OursCodeur)