From 6d1b0c3004201bf5f9ef2195462a2efa02803d1e Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sun, 9 Aug 2026 21:09:52 +1000 Subject: [PATCH] [client] usbredir: add parser bridge Add a serialized bridge between PureSpice USB redirection channels and libusbredirparser. Claim one channel and stream borrowed fragments directly through the parser without packet allocations. Provide an atomic device plug request for later audio integration. --- client/CMakeLists.txt | 32 ++++ client/src/usbredir.c | 348 ++++++++++++++++++++++++++++++++++++++++++ client/src/usbredir.h | 69 +++++++++ 3 files changed, 449 insertions(+) create mode 100644 client/src/usbredir.c create mode 100644 client/src/usbredir.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 66f45fb6..8a7248b6 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -79,6 +79,25 @@ if(ENABLE_AUDIO AND NOT (ENABLE_PIPEWIRE OR ENABLE_PULSEAUDIO)) endif() add_feature_info(ENABLE_AUDIO ENABLE_AUDIO "Audio support.") +cmake_dependent_option( + ENABLE_USB_AUDIO + "Build with USB audio support" + ON + "ENABLE_AUDIO" + OFF +) + +if(ENABLE_USB_AUDIO) + pkg_check_modules(USBREDIRPARSER QUIET IMPORTED_TARGET + libusbredirparser-0.5>=0.7.1) + if(NOT USBREDIRPARSER_FOUND) + message(STATUS + "libusbredirparser was not found, disabling USB audio support") + set(ENABLE_USB_AUDIO OFF) + endif() +endif() +add_feature_info(ENABLE_USB_AUDIO ENABLE_USB_AUDIO "USB audio support.") + add_compile_options( "-Wall" "-Wextra" @@ -195,6 +214,12 @@ if(ENABLE_AUDIO) ) endif() +if(ENABLE_USB_AUDIO) + list(APPEND SOURCES + src/usbredir.c + ) +endif() + add_subdirectory("${PROJECT_TOP}/resources" "${CMAKE_BINARY_DIR}/resources") add_subdirectory("${PROJECT_TOP}/common" "${CMAKE_BINARY_DIR}/common" ) add_subdirectory("${PROJECT_TOP}/repos/LGMP/lgmp" "${CMAKE_BINARY_DIR}/LGMP" ) @@ -259,6 +284,13 @@ if(ENABLE_AUDIO) ) endif() +if(ENABLE_USB_AUDIO) + target_compile_definitions(looking-glass-client PRIVATE ENABLE_USB_AUDIO) + target_link_libraries(looking-glass-client + PkgConfig::USBREDIRPARSER + ) +endif() + install(TARGETS looking-glass-client RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT binary) diff --git a/client/src/usbredir.c b/client/src/usbredir.c new file mode 100644 index 00000000..c1ab3b45 --- /dev/null +++ b/client/src/usbredir.c @@ -0,0 +1,348 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "usbredir.h" + +#include "common/debug.h" + +#include + +#include +#include +#include + +#define USB_REDIR_CHANNEL_COUNT 256 + +struct LG_USBRedir +{ + const LG_USBRedirDeviceOps * deviceOps; + void * deviceOpaque; + + PSUSBRedirChannel * channels[USB_REDIR_CHANNEL_COUNT]; + PSUSBRedirChannel * channel; + struct usbredirparser * parser; + + const uint8_t * readData; + size_t readSize; + size_t readOffset; + + atomic_bool desiredPlugged; + atomic_bool available; + bool plugged; +}; + +static int readUSBRedir(void * opaque, uint8_t * data, int count) +{ + LG_USBRedir * usbredir = opaque; + const size_t remaining = usbredir->readSize - usbredir->readOffset; + if (remaining < (size_t)count) + count = (int)remaining; + + if (!count) + return 0; + + memcpy(data, usbredir->readData + usbredir->readOffset, count); + usbredir->readOffset += count; + return count; +} + +static int writeUSBRedir(void * opaque, uint8_t * data, int count) +{ + LG_USBRedir * usbredir = opaque; + if (!usbredir->channel || + !purespice_usbRedirWrite(usbredir->channel, data, count)) + return -1; + + return count; +} + +static void logUSBRedir(void * opaque, int level, const char * message) +{ + (void)opaque; + switch (level) + { + case usbredirparser_error: + DEBUG_ERROR("USB redirection: %s", message); + break; + + case usbredirparser_warning: + DEBUG_WARN("USB redirection: %s", message); + break; + + case usbredirparser_info: + DEBUG_INFO("USB redirection: %s", message); + break; + + case usbredirparser_debug: + case usbredirparser_debug_data: + DEBUG_TRACE("USB redirection: %s", message); + break; + + case usbredirparser_none: + break; + } +} + +static void helloUSBRedir(void * opaque, + struct usb_redir_hello_header * hello) +{ + (void)hello; + LG_USBRedir * usbredir = opaque; + atomic_store_explicit(&usbredir->available, true, memory_order_release); +} + +static void unplugDevice(LG_USBRedir * usbredir) +{ + if (!usbredir->plugged) + return; + + usbredir->deviceOps->unplug(usbredir->deviceOpaque); + usbredir->plugged = false; +} + +static void destroyParser(LG_USBRedir * usbredir) +{ + atomic_store_explicit(&usbredir->available, false, memory_order_release); + unplugDevice(usbredir); + + if (!usbredir->parser) + return; + + usbredirparser_destroy(usbredir->parser); + usbredir->parser = NULL; +} + +static bool flushUSBRedir(LG_USBRedir * usbredir) +{ + return !usbredir->parser || + usbredirparser_do_write(usbredir->parser) == 0; +} + +static bool connectChannel(LG_USBRedir * usbredir, + PSUSBRedirChannel * channel) +{ + usbredir->channel = channel; + if (purespice_usbRedirConnect(channel)) + return true; + + DEBUG_WARN("Failed to connect USB redirection channel %u", + purespice_usbRedirChannelId(channel)); + usbredir->channel = NULL; + return false; +} + +static void selectChannel(LG_USBRedir * usbredir) +{ + if (usbredir->channel) + return; + + for (unsigned int i = 0; i < USB_REDIR_CHANNEL_COUNT; ++i) + { + PSUSBRedirChannel * channel = usbredir->channels[i]; + if (!channel || !purespice_usbRedirAvailable(channel)) + continue; + + if (connectChannel(usbredir, channel)) + return; + } +} + +static bool createParser(LG_USBRedir * usbredir) +{ + if (usbredir->parser) + return true; + + usbredir->parser = usbredirparser_create(); + if (!usbredir->parser) + { + DEBUG_ERROR("Failed to create the USB redirection parser"); + return false; + } + + usbredir->deviceOps->setup(usbredir->deviceOpaque, usbredir->parser); + usbredir->parser->priv = usbredir; + usbredir->parser->log_func = logUSBRedir; + usbredir->parser->read_func = readUSBRedir; + usbredir->parser->write_func = writeUSBRedir; + usbredir->parser->hello_func = helloUSBRedir; + + uint32_t caps[USB_REDIR_CAPS_SIZE] = { 0 }; + usbredirparser_caps_set_cap(caps, + usb_redir_cap_connect_device_version); + usbredirparser_caps_set_cap(caps, + usb_redir_cap_ep_info_max_packet_size); + usbredirparser_caps_set_cap(caps, usb_redir_cap_64bits_ids); + usbredirparser_caps_set_cap(caps, usb_redir_cap_32bits_bulk_length); + usbredirparser_init(usbredir->parser, "Looking Glass", caps, + USB_REDIR_CAPS_SIZE, usbredirparser_fl_usb_host); + + if (flushUSBRedir(usbredir)) + return true; + + DEBUG_ERROR("Failed to send the USB redirection hello packet"); + destroyParser(usbredir); + return false; +} + +LG_USBRedir * lgUsbRedir_create( + const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque) +{ + if (!deviceOps || !deviceOps->setup || !deviceOps->plug || + !deviceOps->unplug) + return NULL; + + LG_USBRedir * usbredir = calloc(1, sizeof(*usbredir)); + if (!usbredir) + return NULL; + + usbredir->deviceOps = deviceOps; + usbredir->deviceOpaque = deviceOpaque; + atomic_init(&usbredir->desiredPlugged, false); + atomic_init(&usbredir->available, false); + return usbredir; +} + +void lgUsbRedir_destroy(LG_USBRedir * usbredir) +{ + if (!usbredir) + return; + + destroyParser(usbredir); + free(usbredir); +} + +void lgUsbRedir_setPlugged(LG_USBRedir * usbredir, bool plugged) +{ + atomic_store_explicit(&usbredir->desiredPlugged, plugged, + memory_order_release); +} + +bool lgUsbRedir_available(const LG_USBRedir * usbredir) +{ + return atomic_load_explicit(&usbredir->available, memory_order_acquire); +} + +bool lgUsbRedir_process(LG_USBRedir * usbredir) +{ + if (!usbredir->parser || + !atomic_load_explicit(&usbredir->available, memory_order_acquire)) + return flushUSBRedir(usbredir); + + const bool desired = atomic_load_explicit(&usbredir->desiredPlugged, + memory_order_acquire); + if (desired != usbredir->plugged) + { + if (desired) + { + usbredir->deviceOps->plug( + usbredir->deviceOpaque, usbredir->parser); + usbredir->plugged = true; + } + else + { + unplugDevice(usbredir); + usbredirparser_send_device_disconnect(usbredir->parser); + } + } + + return flushUSBRedir(usbredir); +} + +void lgUsbRedir_state(PSUSBRedirChannel * channel, + PSUSBRedirState state, void * opaque) +{ + LG_USBRedir * usbredir = opaque; + const uint8_t id = purespice_usbRedirChannelId(channel); + + switch (state) + { + case PS_USB_REDIR_AVAILABLE: + usbredir->channels[id] = channel; + selectChannel(usbredir); + break; + + case PS_USB_REDIR_CONNECTED: + if (channel != usbredir->channel) + { + purespice_usbRedirDisconnect(channel); + break; + } + + if (!createParser(usbredir)) + purespice_usbRedirDisconnect(channel); + break; + + case PS_USB_REDIR_DISCONNECTED: + if (channel == usbredir->channel) + destroyParser(usbredir); + break; + + case PS_USB_REDIR_UNAVAILABLE: + if (usbredir->channels[id] == channel) + usbredir->channels[id] = NULL; + + if (channel == usbredir->channel) + { + destroyParser(usbredir); + usbredir->channel = NULL; + selectChannel(usbredir); + } + break; + } +} + +bool lgUsbRedir_data(PSUSBRedirChannel * channel, + const uint8_t * data, size_t size, void * opaque) +{ + LG_USBRedir * usbredir = opaque; + if (channel != usbredir->channel || !usbredir->parser || + usbredir->readData) + return false; + + usbredir->readData = data; + usbredir->readSize = size; + usbredir->readOffset = 0; + + const int result = usbredirparser_do_read(usbredir->parser); + const bool consumed = usbredir->readOffset == usbredir->readSize; + usbredir->readData = NULL; + usbredir->readSize = 0; + usbredir->readOffset = 0; + + if (result == usbredirparser_read_parse_error) + { + DEBUG_ERROR("Invalid USB redirection packet"); + return false; + } + + if (!consumed) + { + DEBUG_ERROR("USB redirection parser did not consume its input"); + return false; + } + + return lgUsbRedir_process(usbredir); +} + +void * lgUsbRedir_device(void * parserOpaque) +{ + LG_USBRedir * usbredir = parserOpaque; + return usbredir->deviceOpaque; +} diff --git a/client/src/usbredir.h b/client/src/usbredir.h new file mode 100644 index 00000000..b9f1ad4c --- /dev/null +++ b/client/src/usbredir.h @@ -0,0 +1,69 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_CLIENT_USBREDIR_ +#define _H_LG_CLIENT_USBREDIR_ + +#include + +#include +#include +#include + +struct usbredirparser; + +typedef struct LG_USBRedir LG_USBRedir; + +typedef struct LG_USBRedirDeviceOps +{ + /* Install the device packet callbacks on a newly-created parser. */ + void (*setup)(void * opaque, struct usbredirparser * parser); + + /* Queue the device descriptors and connection announcement. */ + void (*plug)(void * opaque, struct usbredirparser * parser); + + /* Stop all device activity. The bridge sends the disconnect packet. */ + void (*unplug)(void * opaque); +} +LG_USBRedirDeviceOps; + +LG_USBRedir * lgUsbRedir_create( + const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque); +/* The PureSpice session must be stopped before destroying the bridge. */ +void lgUsbRedir_destroy(LG_USBRedir * usbredir); + +/* This may be called from another thread. The request is applied by + * lgUsbRedir_process on the PureSpice processing thread. */ +void lgUsbRedir_setPlugged(LG_USBRedir * usbredir, bool plugged); +bool lgUsbRedir_available(const LG_USBRedir * usbredir); + +/* Apply pending device state and flush parser output. This must be called on + * the PureSpice processing thread. */ +bool lgUsbRedir_process(LG_USBRedir * usbredir); + +void lgUsbRedir_state(PSUSBRedirChannel * channel, + PSUSBRedirState state, void * opaque); +bool lgUsbRedir_data(PSUSBRedirChannel * channel, + const uint8_t * data, size_t size, void * opaque); + +/* Parser callbacks receive the bridge as their opaque value. */ +void * lgUsbRedir_device(void * parserOpaque); + +#endif