SDL: hidapi/libusb: maintain in-memory cache of vendor/product strings

From 17d8479d980ebd37707c03e4d9e86aa0438f0ab9 Mon Sep 17 00:00:00 2001
From: Steven Noonan <[EMAIL REDACTED]>
Date: Thu, 22 Apr 2021 15:44:01 -0700
Subject: [PATCH] hidapi/libusb: maintain in-memory cache of vendor/product
 strings

The get_usb_string call is rather expensive on some USB devices, so we
cache the vendor/product strings for future lookups (e.g. when
hid_enumerate is invoked again later).

This way, we only need to ask libusb for strings for devices we haven't
seen since before we started.

Signed-off-by: Steven Noonan <steven@valvesoftware.com>
Signed-off-by: Sam Lantinga <slouken@libsdl.org>
---
 src/hidapi/libusb/hid.c | 111 +++++++++++++++++++++++++++++++++++++---
 1 file changed, 105 insertions(+), 6 deletions(-)

diff --git a/src/hidapi/libusb/hid.c b/src/hidapi/libusb/hid.c
index 3fd95e061..1175da99c 100644
--- a/src/hidapi/libusb/hid.c
+++ b/src/hidapi/libusb/hid.c
@@ -440,6 +440,94 @@ static wchar_t *get_usb_string(libusb_device_handle *dev, uint8_t idx)
 	return str;
 }
 
+struct usb_string_cache_entry {
+	uint16_t vid;
+	uint16_t pid;
+	wchar_t *vendor;
+	wchar_t *product;
+};
+
+static struct usb_string_cache_entry *usb_string_cache = NULL;
+static size_t usb_string_cache_size = 0;
+static size_t usb_string_cache_insert_pos = 0;
+
+static int usb_string_cache_grow()
+{
+	struct usb_string_cache_entry *new_cache;
+	size_t allocSize;
+	size_t new_cache_size;
+
+	new_cache_size = usb_string_cache_size + 8;
+	allocSize = sizeof(struct usb_string_cache_entry) * new_cache_size;
+	new_cache = (struct usb_string_cache_entry *)realloc(usb_string_cache, allocSize);
+	if (!new_cache)
+		return -1;
+
+	usb_string_cache = new_cache;
+	usb_string_cache_size = new_cache_size;
+
+	return 0;
+}
+
+static void usb_string_cache_destroy()
+{
+	size_t i;
+	for (i = 0; i < usb_string_cache_insert_pos; i++) {
+		free(usb_string_cache[i].vendor);
+		free(usb_string_cache[i].product);
+	}
+	free(usb_string_cache);
+
+	usb_string_cache = NULL;
+	usb_string_cache_size = 0;
+}
+
+static struct usb_string_cache_entry *usb_string_cache_insert()
+{
+	struct usb_string_cache_entry *new_entry = NULL;
+	if (usb_string_cache_insert_pos >= usb_string_cache_size) {
+		if (usb_string_cache_grow() < 0)
+			return NULL;
+	}
+	new_entry = &usb_string_cache[usb_string_cache_insert_pos];
+	usb_string_cache_insert_pos++;
+	return new_entry;
+}
+
+static const struct usb_string_cache_entry *usb_string_cache_find(struct libusb_device_descriptor *desc, struct libusb_device_handle *handle)
+{
+	struct usb_string_cache_entry *entry = NULL;
+	size_t i;
+
+	/* Search for existing string cache entry */
+	for (i = 0; i < usb_string_cache_insert_pos; i++) {
+		entry = &usb_string_cache[i];
+		if (entry->vid != desc->idVendor)
+			continue;
+		if (entry->pid != desc->idProduct)
+			continue;
+		return entry;
+	}
+
+	/* Not found, create one. */
+	entry = usb_string_cache_insert();
+	if (!entry)
+		return NULL;
+
+	entry->vid = desc->idVendor;
+	entry->pid = desc->idProduct;
+	if (desc->iManufacturer > 0)
+		entry->vendor = get_usb_string(handle, desc->iManufacturer);
+	else
+		entry->vendor = NULL;
+	if (desc->iProduct > 0)
+		entry->product = get_usb_string(handle, desc->iProduct);
+	else
+		entry->product = NULL;
+
+	return entry;
+}
+
 static char *make_path(libusb_device *dev, int interface_number)
 {
 	char str[64];
@@ -473,6 +561,8 @@ int HID_API_EXPORT hid_init(void)
 
 int HID_API_EXPORT hid_exit(void)
 {
+	usb_string_cache_destroy();
+
 	if (usb_context) {
 		libusb_exit(usb_context);
 		usb_context = NULL;
@@ -617,6 +707,7 @@ struct hid_device_info  HID_API_EXPORT *hid_enumerate(unsigned short vendor_id,
 
 							if (res >= 0) {
 								struct hid_device_info *tmp;
+								const struct usb_string_cache_entry *string_cache;
 
 								/* VID/PID match. Create the record. */
 								tmp = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info));
@@ -638,12 +729,20 @@ struct hid_device_info  HID_API_EXPORT *hid_enumerate(unsigned short vendor_id,
 										get_usb_string(handle, desc.iSerialNumber);
 
 								/* Manufacturer and Product strings */
-								if (desc.iManufacturer > 0)
-									cur_dev->manufacturer_string =
-										get_usb_string(handle, desc.iManufacturer);
-								if (desc.iProduct > 0)
-									cur_dev->product_string =
-										get_usb_string(handle, desc.iProduct);
+								if (dev_vid && dev_pid) {
+									string_cache = usb_string_cache_find(&desc, handle);
+									if (string_cache) {
+										cur_dev->manufacturer_string = wcsdup(string_cache->vendor);
+										cur_dev->product_string = wcsdup(string_cache->product);
+									}
+								} else {
+									if (desc.iManufacturer > 0)
+										cur_dev->manufacturer_string =
+											get_usb_string(handle, desc.iManufacturer);
+									if (desc.iProduct > 0)
+										cur_dev->product_string =
+											get_usb_string(handle, desc.iProduct);
+								}
 
 #ifdef INVASIVE_GET_USAGE
 {