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

From e2ec976735197797354ad7e8f6c3bc784ad99eb0 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 24 May 2023 22:30:08 -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 | 126 ++++++++++++++++++++++++++++++++++++++--
 1 file changed, 122 insertions(+), 4 deletions(-)

diff --git a/src/hidapi/libusb/hid.c b/src/hidapi/libusb/hid.c
index aa1279a978ba..c0907c4ab82a 100644
--- a/src/hidapi/libusb/hid.c
+++ b/src/hidapi/libusb/hid.c
@@ -492,6 +492,111 @@ 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;
+	usb_string_cache_insert_pos = 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 int usb_string_can_cache(uint16_t vid, uint16_t pid)
+{
+	if (!vid || !pid) {
+		/* We can't cache these, they aren't unique */
+		return 0;
+	}
+
+	if (vid == 0x0f0d && pid == 0x00dc) {
+		/* HORI reuses this VID/PID for many different products */
+		return 0;
+	}
+
+	/* We can cache these strings */
+	return 1;
+}
+
+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;
+}
+
 /**
   Max length of the result: "000-000.000.000.000.000.000.000:000.000" (39 chars).
   64 is used for simplicity/alignment.
@@ -559,6 +664,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;
@@ -685,10 +792,21 @@ static struct hid_device_info * create_device_info_for_device(libusb_device *dev
 		cur_dev->serial_number = 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);
+	const struct usb_string_cache_entry *string_cache;
+	if (usb_string_can_cache(desc->idVendor, desc->idProduct) &&
+	    (string_cache = usb_string_cache_find(desc, handle)) != NULL) {
+		if (string_cache->vendor) {
+			cur_dev->manufacturer_string = wcsdup(string_cache->vendor);
+		}
+		if (string_cache->product) {
+			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);
+	}
 
 	return cur_dev;
 }