From 3dbf85f0d9be6a45baa955d28528b46058426e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=B2=D0=B0=D0=BB=D0=BE=D0=B2=20=D0=90?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20=D0=92=D0=BB?= =?UTF-8?q?=D0=B0=D0=B4=D0=B8=D0=BC=D0=B8=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= Date: Fri, 9 Oct 2026 15:09:48 +0300 Subject: [PATCH] feat(usbd): add native synchronous USB backend --- .gitmodules | 3 + 3rdparty/libusb | 1 + core/libs/prx/libSceUsbd/CMakeLists.txt | 21 +++ core/libs/prx/libSceUsbd/Export.cpp | 202 ++++++++++++++---------- core/libs/tests/GuestUsbd.cpp | 167 +++++++++++++++++--- core/libs/tests/GuestUsbdControl.cpp | 98 ++++++++++++ docs/dev/TechnicalDebt.md | 2 +- 7 files changed, 390 insertions(+), 104 deletions(-) create mode 160000 3rdparty/libusb create mode 100644 core/libs/tests/GuestUsbdControl.cpp diff --git a/.gitmodules b/.gitmodules index fc2e48631c8e..d75f5af46c70 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,3 +31,6 @@ [submodule "3rdparty/libjpeg-turbo"] path = 3rdparty/libjpeg-turbo url = https://github.com/libjpeg-turbo/libjpeg-turbo.git +[submodule "3rdparty/libusb"] + path = 3rdparty/libusb + url = https://github.com/libusb/libusb-cmake.git diff --git a/3rdparty/libusb b/3rdparty/libusb new file mode 160000 index 000000000000..c8477c10ac2a --- /dev/null +++ b/3rdparty/libusb @@ -0,0 +1 @@ +Subproject commit c8477c10ac2ac6b1718d4d498e102b9f18b776f5 diff --git a/core/libs/prx/libSceUsbd/CMakeLists.txt b/core/libs/prx/libSceUsbd/CMakeLists.txt index d7acf60da81c..fe624dd9bc22 100644 --- a/core/libs/prx/libSceUsbd/CMakeLists.txt +++ b/core/libs/prx/libSceUsbd/CMakeLists.txt @@ -1,15 +1,36 @@ +set(LIBUSB_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(LIBUSB_BUILD_TESTING OFF CACHE BOOL "" FORCE) +set(LIBUSB_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(LIBUSB_INSTALL_TARGETS OFF CACHE BOOL "" FORCE) +set(LIBUSB_ENABLE_UDEV OFF CACHE BOOL "" FORCE) +add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/libusb ${CMAKE_CURRENT_BINARY_DIR}/libusb EXCLUDE_FROM_ALL) +set_target_properties(usb-1.0 PROPERTIES POSITION_INDEPENDENT_CODE ON) + add_library(libSceUsbd SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceUsbd PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceUsbd PRIVATE usb-1.0) +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceUsbd PRIVATE LINKER:--exclude-libs,ALL) +endif() configure_windows_unwind(libSceUsbd) if(BUILD_TESTING) add_test_executable(guest_usbd_tests ${LIBS_INCLUDE_DIR}/tests/GuestUsbd.cpp) target_include_directories(guest_usbd_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_include_directories(guest_usbd_tests SYSTEM PRIVATE $) target_link_libraries(guest_usbd_tests PRIVATE libSceUsbd libc) configure_windows_unwind(guest_usbd_tests) add_test(NAME guest_usbd COMMAND guest_usbd_tests) + add_test_executable(guest_usbd_control_tests ${LIBS_INCLUDE_DIR}/tests/GuestUsbdControl.cpp Export.cpp) + target_include_directories(guest_usbd_control_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_usbd_control_tests PRIVATE usb-1.0 libc) + target_link_options(guest_usbd_control_tests PRIVATE LINKER:--wrap=libusb_control_transfer) + configure_windows_unwind(guest_usbd_control_tests) + add_test(NAME guest_usbd_control COMMAND guest_usbd_control_tests) if(WIN32) set_tests_properties(guest_usbd PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_usbd_control PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceUsbd/Export.cpp b/core/libs/prx/libSceUsbd/Export.cpp index c5c238411d89..935ccf5d0e88 100644 --- a/core/libs/prx/libSceUsbd/Export.cpp +++ b/core/libs/prx/libSceUsbd/Export.cpp @@ -1,12 +1,12 @@ #include #include -#include "SceTypes.hpp" +#include +#include +#include +#include +#include #include "prx/libc/include/General.hpp" -#include -#include -#include - namespace { constexpr std::int32_t SCE_USBD_ERROR_INVALID_ARG = static_cast(0x80240002); @@ -16,159 +16,195 @@ struct UsbdTimeval { std::int64_t microseconds; }; -void* g_emptyDeviceList[1] = {nullptr}; +using UsbdTransferCallback = void (APS5_VABI *)(libusb_transfer*); + +std::mutex contextMutex; +std::shared_ptr context; +std::size_t initializationCount = 0; + +std::int32_t ConvertError(int result) { + if (result >= 0) return result; + if (result == LIBUSB_ERROR_OTHER) return static_cast(0x802400ff); + return static_cast(0x80240000u - static_cast(result)); +} + +std::shared_ptr GetContext() { + std::lock_guard lock(contextMutex); + if (!context) throw std::runtime_error("libSceUsbd: library is not initialized"); + return context; +} } extern "C" { std::int32_t APS5_VABI sceUsbdInit() { + std::lock_guard lock(contextMutex); + if (context) { + ++initializationCount; + return 0; + } + libusb_context* created = nullptr; + const int result = libusb_init_context(&created, nullptr, 0); + if (result < 0) return ConvertError(result); + context = std::shared_ptr(created, libusb_exit); + initializationCount = 1; return 0; } void APS5_VABI sceUsbdExit() { + std::shared_ptr released; + { + std::lock_guard lock(contextMutex); + if (initializationCount != 0 && --initializationCount == 0) released.swap(context); + } } -std::int64_t APS5_VABI sceUsbdGetDeviceList(void*** list) { +std::int64_t APS5_VABI sceUsbdGetDeviceList(libusb_device*** list) { if (list == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - *list = g_emptyDeviceList; - return 0; + const auto current = GetContext(); + const auto result = libusb_get_device_list(current.get(), list); + return result < 0 ? ConvertError(static_cast(result)) : result; } -void APS5_VABI sceUsbdFreeDeviceList(void** list, std::int32_t unrefDevices) { - (void)unrefDevices; - if (list != nullptr && list != g_emptyDeviceList) throw std::runtime_error(std::string(__func__) + ": list was not returned by sceUsbdGetDeviceList"); +void APS5_VABI sceUsbdFreeDeviceList(libusb_device** list, std::int32_t unrefDevices) { + libusb_free_device_list(list, unrefDevices); } std::int32_t APS5_VABI sceUsbdHandleEventsTimeout(const UsbdTimeval* timeout) { if (timeout == nullptr || timeout->seconds < 0 || timeout->microseconds < 0 || timeout->microseconds >= 1000000) return SCE_USBD_ERROR_INVALID_ARG; - std::this_thread::sleep_for(std::chrono::seconds(timeout->seconds) + std::chrono::microseconds(timeout->microseconds)); - return 0; + timeval native{}; + if (timeout->seconds > std::numeric_limits::max()) { + throw std::runtime_error("sceUsbdHandleEventsTimeout: timeout exceeds host timeval range"); + } + native.tv_sec = static_cast(timeout->seconds); + native.tv_usec = static_cast(timeout->microseconds); + const auto current = GetContext(); + return ConvertError(libusb_handle_events_timeout(current.get(), &native)); } -int APS5_VABI sceUsbdAllocTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; +libusb_transfer* APS5_VABI sceUsbdAllocTransfer(int isoPackets) { + if (isoPackets < 0) return nullptr; + return libusb_alloc_transfer(isoPackets); } -int APS5_VABI sceUsbdAttachKernelDriver() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdAttachKernelDriver(libusb_device_handle* handle, int interfaceNumber) { + if (handle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_attach_kernel_driver(handle, interfaceNumber)); } -int APS5_VABI sceUsbdCancelTransfer() { +int APS5_VABI sceUsbdCancelTransfer(libusb_transfer* transfer) { + (void)transfer; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbdCheckConnected() { +int APS5_VABI sceUsbdCheckConnected(libusb_device_handle* handle) { + (void)handle; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbdClaimInterface() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdClaimInterface(libusb_device_handle* handle, int interfaceNumber) { + if (handle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_claim_interface(handle, interfaceNumber)); } -int APS5_VABI sceUsbdClose() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceUsbdClose(libusb_device_handle* handle) { + libusb_close(handle); } -int APS5_VABI sceUsbdControlTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdControlTransfer(libusb_device_handle* handle, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, unsigned char* data, std::int32_t length, std::uint32_t timeout) { + if (handle == nullptr || length < 0 || length > UINT16_MAX || (length != 0 && data == nullptr)) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_control_transfer(handle, requestType, request, value, index, data, static_cast(length), timeout)); } -int APS5_VABI sceUsbdEventHandlingOk() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdEventHandlingOk() { + const auto current = GetContext(); + return libusb_event_handling_ok(current.get()); } -int APS5_VABI sceUsbdFillInterruptTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceUsbdFillInterruptTransfer(libusb_transfer* transfer, libusb_device_handle* handle, std::uint8_t endpoint, unsigned char* buffer, int length, UsbdTransferCallback callback, void* userData, std::uint32_t timeout) { + if (transfer == nullptr) APS5_INVALID_ARG_EX; + libusb_fill_interrupt_transfer(transfer, handle, endpoint, buffer, length, reinterpret_cast(callback), userData, timeout); } -int APS5_VABI sceUsbdFreeConfigDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceUsbdFreeConfigDescriptor(libusb_config_descriptor* descriptor) { + libusb_free_config_descriptor(descriptor); } -int APS5_VABI sceUsbdFreeTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceUsbdFreeTransfer(libusb_transfer* transfer) { + libusb_free_transfer(transfer); } -int APS5_VABI sceUsbdGetActiveConfigDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetActiveConfigDescriptor(libusb_device* device, libusb_config_descriptor** config) { + if (device == nullptr || config == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_get_active_config_descriptor(device, config)); } -int APS5_VABI sceUsbdGetBusNumber() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint8_t APS5_VABI sceUsbdGetBusNumber(libusb_device* device) { + if (device == nullptr) APS5_INVALID_ARG_EX; + return libusb_get_bus_number(device); } -int APS5_VABI sceUsbdGetConfigDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetConfigDescriptor(libusb_device* device, std::uint8_t index, libusb_config_descriptor** config) { + if (device == nullptr || config == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_get_config_descriptor(device, index, config)); } -int APS5_VABI sceUsbdGetDeviceAddress() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint8_t APS5_VABI sceUsbdGetDeviceAddress(libusb_device* device) { + if (device == nullptr) APS5_INVALID_ARG_EX; + return libusb_get_device_address(device); } -int APS5_VABI sceUsbdGetDeviceDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetDeviceDescriptor(libusb_device* device, libusb_device_descriptor* descriptor) { + if (device == nullptr || descriptor == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_get_device_descriptor(device, descriptor)); } -int APS5_VABI sceUsbdGetStringDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetStringDescriptor(libusb_device_handle* handle, std::uint8_t index, std::uint16_t language, unsigned char* data, int length) { + if (handle == nullptr || data == nullptr || length < 0 || length > UINT16_MAX) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_get_string_descriptor(handle, index, language, data, length)); } -int APS5_VABI sceUsbdKernelDriverActive() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdKernelDriverActive(libusb_device_handle* handle, int interfaceNumber) { + if (handle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_kernel_driver_active(handle, interfaceNumber)); } -int APS5_VABI sceUsbdOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdOpen(libusb_device* device, libusb_device_handle** handle) { + if (device == nullptr || handle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_open(device, handle)); } -int APS5_VABI sceUsbdRefDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; +libusb_device* APS5_VABI sceUsbdRefDevice(libusb_device* device) { + if (device == nullptr) return nullptr; + return libusb_ref_device(device); } -int APS5_VABI sceUsbdReleaseInterface() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdReleaseInterface(libusb_device_handle* handle, int interfaceNumber) { + if (handle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_release_interface(handle, interfaceNumber)); } -int APS5_VABI sceUsbdResetDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdResetDevice(libusb_device_handle* handle) { + if (handle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_reset_device(handle)); } -int APS5_VABI sceUsbdSetConfiguration() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdSetConfiguration(libusb_device_handle* handle, int configuration) { + if (handle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + return ConvertError(libusb_set_configuration(handle, configuration)); } -int APS5_VABI sceUsbdSubmitTransfer() { +int APS5_VABI sceUsbdSubmitTransfer(libusb_transfer* transfer) { + (void)transfer; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceUsbdUnrefDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceUsbdUnrefDevice(libusb_device* device) { + libusb_unref_device(device); } } diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index 910810ec4c4a..9c29f00d1e6a 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -1,10 +1,11 @@ #include "prx/libc/include/general/VabiMacros.hpp" -#include #include #include #include #include +#include +#include namespace { @@ -13,20 +14,45 @@ struct UsbdTimeval { std::int64_t microseconds; }; +using UsbdTransferCallback = void (APS5_VABI *)(libusb_transfer*); + } extern "C" { std::int32_t APS5_VABI sceUsbdInit(); void APS5_VABI sceUsbdExit(); -std::int64_t APS5_VABI sceUsbdGetDeviceList(void*** list); -void APS5_VABI sceUsbdFreeDeviceList(void** list, std::int32_t unrefDevices); +std::int64_t APS5_VABI sceUsbdGetDeviceList(libusb_device*** list); +void APS5_VABI sceUsbdFreeDeviceList(libusb_device** list, std::int32_t unrefDevices); std::int32_t APS5_VABI sceUsbdHandleEventsTimeout(const UsbdTimeval* timeout); -int APS5_VABI sceUsbdOpen(); +std::int32_t APS5_VABI sceUsbdEventHandlingOk(); +libusb_device* APS5_VABI sceUsbdRefDevice(libusb_device* device); +void APS5_VABI sceUsbdUnrefDevice(libusb_device* device); +std::uint8_t APS5_VABI sceUsbdGetBusNumber(libusb_device* device); +std::uint8_t APS5_VABI sceUsbdGetDeviceAddress(libusb_device* device); +std::int32_t APS5_VABI sceUsbdGetDeviceDescriptor(libusb_device* device, libusb_device_descriptor* descriptor); +std::int32_t APS5_VABI sceUsbdGetConfigDescriptor(libusb_device* device, std::uint8_t index, libusb_config_descriptor** config); +std::int32_t APS5_VABI sceUsbdGetActiveConfigDescriptor(libusb_device* device, libusb_config_descriptor** config); +void APS5_VABI sceUsbdFreeConfigDescriptor(libusb_config_descriptor* descriptor); +std::int32_t APS5_VABI sceUsbdOpen(libusb_device* device, libusb_device_handle** handle); +void APS5_VABI sceUsbdClose(libusb_device_handle* handle); +std::int32_t APS5_VABI sceUsbdAttachKernelDriver(libusb_device_handle* handle, int interfaceNumber); +std::int32_t APS5_VABI sceUsbdClaimInterface(libusb_device_handle* handle, int interfaceNumber); +std::int32_t APS5_VABI sceUsbdReleaseInterface(libusb_device_handle* handle, int interfaceNumber); +std::int32_t APS5_VABI sceUsbdKernelDriverActive(libusb_device_handle* handle, int interfaceNumber); +std::int32_t APS5_VABI sceUsbdResetDevice(libusb_device_handle* handle); +std::int32_t APS5_VABI sceUsbdSetConfiguration(libusb_device_handle* handle, int configuration); +libusb_transfer* APS5_VABI sceUsbdAllocTransfer(int isoPackets); +void APS5_VABI sceUsbdFreeTransfer(libusb_transfer* transfer); +void APS5_VABI sceUsbdFillInterruptTransfer(libusb_transfer* transfer, libusb_device_handle* handle, std::uint8_t endpoint, unsigned char* buffer, int length, UsbdTransferCallback callback, void* userData, std::uint32_t timeout); +int APS5_VABI sceUsbdSubmitTransfer(libusb_transfer* transfer); +int APS5_VABI sceUsbdCancelTransfer(libusb_transfer* transfer); +int APS5_VABI sceUsbdCheckConnected(libusb_device_handle* handle); } namespace { constexpr std::int32_t invalidArgument = static_cast(0x80240002); +constexpr std::int32_t notFound = static_cast(0x80240005); void Require(bool condition, const char* message) { if (!condition) { @@ -35,29 +61,130 @@ void Require(bool condition, const char* message) { } } +template +void RequireUnsupported(TCall call) { + bool threw = false; + try { + call(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "unsupported operation did not throw"); } -int main() { - Require(sceUsbdInit() == 0, "initialization failed"); - void** list = nullptr; - Require(sceUsbdGetDeviceList(&list) == 0, "a device was listed"); - Require(list != nullptr && list[0] == nullptr, "device list is not an empty null-terminated array"); +void APS5_VABI TransferCallback(libusb_transfer* transfer) { + ++*static_cast(transfer->user_data); +} + +void CheckTransferPreparation() { + Require(sceUsbdAllocTransfer(-1) == nullptr, "negative isochronous count accepted"); + for (const int packets : {0, 3}) { + auto* transfer = sceUsbdAllocTransfer(packets); + Require(transfer != nullptr, "transfer allocation failed"); + Require(transfer->flags == 0 && transfer->buffer == nullptr && transfer->length == 0, "transfer was not initialized"); + unsigned char buffer[16]{}; + int callbacks = 0; + sceUsbdFillInterruptTransfer(transfer, nullptr, 0x81, buffer, sizeof(buffer), TransferCallback, &callbacks, 250); + Require(transfer->type == LIBUSB_TRANSFER_TYPE_INTERRUPT && transfer->endpoint == 0x81, "interrupt endpoint not preserved"); + Require(transfer->buffer == buffer && transfer->length == sizeof(buffer) && transfer->timeout == 250, "transfer buffer or timeout not preserved"); + Require(transfer->user_data == &callbacks, "callback user data not preserved"); + auto callback = reinterpret_cast(transfer->callback); + Require(callback == TransferCallback, "guest callback pointer not preserved"); + callback(transfer); + Require(callbacks == 1, "guest callback ABI or user data incorrect"); + sceUsbdFreeTransfer(transfer); + } + sceUsbdFreeTransfer(nullptr); +} + +void CheckDevices() { + libusb_device** list = nullptr; + const auto count = sceUsbdGetDeviceList(&list); + Require(count >= 0 && list != nullptr, "device enumeration failed"); + Require(list[count] == nullptr, "device list is not null-terminated"); + for (std::int64_t i = 0; i < count; ++i) { + auto* device = list[i]; + Require(device != nullptr, "null entry before device list terminator"); + Require(sceUsbdRefDevice(device) == device, "ref changed the device pointer"); + sceUsbdUnrefDevice(device); + libusb_device_descriptor descriptor{}; + Require(sceUsbdGetDeviceDescriptor(device, &descriptor) == 0, "cached device descriptor unavailable"); + Require(descriptor.bLength == LIBUSB_DT_DEVICE_SIZE && descriptor.bDescriptorType == LIBUSB_DT_DEVICE, "invalid device descriptor"); + Require(sceUsbdGetDeviceAddress(device) <= 127, "invalid USB device address"); + libusb_config_descriptor* config = nullptr; + Require(sceUsbdGetConfigDescriptor(device, descriptor.bNumConfigurations, &config) == notFound, "missing config did not map to NOT_FOUND"); + Require(config == nullptr, "missing config returned a descriptor"); + if (descriptor.bNumConfigurations != 0 && sceUsbdGetConfigDescriptor(device, 0, &config) == 0) { + Require(config != nullptr && config->bDescriptorType == LIBUSB_DT_CONFIG, "invalid config descriptor"); + sceUsbdFreeConfigDescriptor(config); + } + config = nullptr; + if (sceUsbdGetActiveConfigDescriptor(device, &config) == 0) { + Require(config != nullptr && config->bDescriptorType == LIBUSB_DT_CONFIG, "invalid active config descriptor"); + sceUsbdFreeConfigDescriptor(config); + } + } sceUsbdFreeDeviceList(list, 1); + list = nullptr; + const auto retainedCount = sceUsbdGetDeviceList(&list); + Require(retainedCount >= 0 && list != nullptr, "second enumeration failed"); + const std::vector retained(list, list + retainedCount); + sceUsbdFreeDeviceList(list, 0); + for (auto* device : retained) { + libusb_device_descriptor descriptor{}; + Require(sceUsbdGetDeviceDescriptor(device, &descriptor) == 0, "device reference was not retained"); + sceUsbdUnrefDevice(device); + } + std::printf("USBD: enumerated %lld host devices\n", static_cast(count)); +} + +void CheckInvalidArguments() { Require(sceUsbdGetDeviceList(nullptr) == invalidArgument, "null list accepted"); Require(sceUsbdHandleEventsTimeout(nullptr) == invalidArgument, "null timeout accepted"); - const UsbdTimeval invalid{0, 1000000}; - Require(sceUsbdHandleEventsTimeout(&invalid) == invalidArgument, "out-of-range microseconds accepted"); + for (const UsbdTimeval invalid : {UsbdTimeval{-1, 0}, UsbdTimeval{0, -1}, UsbdTimeval{0, 1000000}}) { + Require(sceUsbdHandleEventsTimeout(&invalid) == invalidArgument, "invalid timeout accepted"); + } + libusb_device_descriptor descriptor{}; + libusb_config_descriptor* config = nullptr; + libusb_device_handle* handle = nullptr; + Require(sceUsbdGetDeviceDescriptor(nullptr, &descriptor) == invalidArgument, "null device accepted for descriptor"); + Require(sceUsbdGetConfigDescriptor(nullptr, 0, &config) == invalidArgument, "null device accepted for config"); + Require(sceUsbdGetActiveConfigDescriptor(nullptr, &config) == invalidArgument, "null device accepted for active config"); + Require(sceUsbdOpen(nullptr, &handle) == invalidArgument, "null device opened"); + Require(sceUsbdAttachKernelDriver(nullptr, 0) == invalidArgument, "null handle accepted for attach"); + Require(sceUsbdClaimInterface(nullptr, 0) == invalidArgument, "null handle accepted for claim"); + Require(sceUsbdReleaseInterface(nullptr, 0) == invalidArgument, "null handle accepted for release"); + Require(sceUsbdKernelDriverActive(nullptr, 0) == invalidArgument, "null handle accepted for driver query"); + Require(sceUsbdResetDevice(nullptr) == invalidArgument, "null handle accepted for reset"); + Require(sceUsbdSetConfiguration(nullptr, 0) == invalidArgument, "null handle accepted for configuration"); + Require(sceUsbdRefDevice(nullptr) == nullptr, "null device ref returned a pointer"); + sceUsbdUnrefDevice(nullptr); + sceUsbdClose(nullptr); + sceUsbdFreeConfigDescriptor(nullptr); + sceUsbdFreeDeviceList(nullptr, 1); + RequireUnsupported([] { sceUsbdSubmitTransfer(nullptr); }); + RequireUnsupported([] { sceUsbdCancelTransfer(nullptr); }); + RequireUnsupported([] { sceUsbdCheckConnected(nullptr); }); +} + +} + +int main() { + Require(sceUsbdInit() == 0, "initialization failed"); + Require(sceUsbdInit() == 0, "repeated initialization failed"); + sceUsbdExit(); + CheckDevices(); + CheckTransferPreparation(); + CheckInvalidArguments(); const UsbdTimeval timeout{0, 50000}; - const auto start = std::chrono::steady_clock::now(); Require(sceUsbdHandleEventsTimeout(&timeout) == 0, "event handling failed"); - Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(45), "event handling returned before the timeout"); - bool threw = false; - try { - sceUsbdOpen(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "opening a device did not throw"); + Require(sceUsbdEventHandlingOk() == 1, "event handling unavailable"); + sceUsbdExit(); + RequireUnsupported([] { sceUsbdEventHandlingOk(); }); + Require(sceUsbdInit() == 0, "reinitialization failed"); + libusb_device** list = nullptr; + Require(sceUsbdGetDeviceList(&list) >= 0, "enumeration after reinitialization failed"); + sceUsbdFreeDeviceList(list, 1); sceUsbdExit(); std::puts("USBD tests passed"); return 0; diff --git a/core/libs/tests/GuestUsbdControl.cpp b/core/libs/tests/GuestUsbdControl.cpp new file mode 100644 index 000000000000..21471b92ed57 --- /dev/null +++ b/core/libs/tests/GuestUsbdControl.cpp @@ -0,0 +1,98 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int32_t APS5_VABI sceUsbdControlTransfer(libusb_device_handle*, std::uint8_t, std::uint8_t, std::uint16_t, std::uint16_t, unsigned char*, std::int32_t, std::uint32_t); +std::int32_t APS5_VABI sceUsbdGetStringDescriptor(libusb_device_handle*, std::uint8_t, std::uint16_t, unsigned char*, int); +} + +namespace { + +struct ControlCall { + libusb_device_handle* handle; + std::uint8_t requestType; + std::uint8_t request; + std::uint16_t value; + std::uint16_t index; + unsigned char* data; + std::uint16_t length; + unsigned int timeout; +}; + +ControlCall lastCall{}; +int nativeResult = 0; +int nativeCalls = 0; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "USBD control: %s\n", message); + std::abort(); + } +} + +} + +extern "C" int LIBUSB_CALL __wrap_libusb_control_transfer(libusb_device_handle* handle, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, unsigned char* data, std::uint16_t length, unsigned int timeout) { + lastCall = {handle, requestType, request, value, index, data, length, timeout}; + ++nativeCalls; + if (nativeResult > 0) { + Require(nativeResult <= length, "test backend result exceeds transfer length"); + for (int i = 0; i < nativeResult; ++i) data[i] = static_cast(0x20 + i); + } + return nativeResult; +} + +int main() { + unsigned char handleStorage{}; + auto* handle = reinterpret_cast(&handleStorage); + unsigned char buffer[10]; + std::memset(buffer, 0x5a, sizeof(buffer)); + nativeResult = 5; + Require(sceUsbdControlTransfer(handle, 0xc1, 0x37, 0x1234, 0xabcd, buffer + 1, 8, 1200) == 5, "byte count not preserved"); + Require(lastCall.handle == handle && lastCall.requestType == 0xc1 && lastCall.request == 0x37, "handle or request changed"); + Require(lastCall.value == 0x1234 && lastCall.index == 0xabcd && lastCall.timeout == 1200, "setup fields or timeout changed"); + Require(lastCall.data == buffer + 1 && lastCall.length == 8, "buffer or length changed"); + for (int i = 0; i < 5; ++i) Require(buffer[i + 1] == 0x20 + i, "native output did not reach guest buffer"); + Require(buffer[0] == 0x5a && buffer[6] == 0x5a && buffer[9] == 0x5a, "buffer canary overwritten"); + + for (int error = -1; error >= -12; --error) { + nativeResult = error; + const auto expected = static_cast(0x80240000u - static_cast(error)); + Require(sceUsbdControlTransfer(handle, 0x80, 0, 0, 0, buffer, sizeof(buffer), 0) == expected, "USB error mapping incorrect"); + } + nativeResult = LIBUSB_ERROR_OTHER; + Require(sceUsbdControlTransfer(handle, 0x80, 0, 0, 0, buffer, sizeof(buffer), 0) == static_cast(0x802400ff), "OTHER error mapping incorrect"); + nativeResult = 0; + Require(sceUsbdControlTransfer(handle, 0x40, 0, 0, 0, nullptr, 0, 0) == 0, "zero length transfer rejected"); + Require(lastCall.data == nullptr && lastCall.length == 0, "zero length transfer changed"); + std::vector maximumBuffer(UINT16_MAX); + Require(sceUsbdControlTransfer(handle, 0x80, 0, 0, 0, maximumBuffer.data(), maximumBuffer.size(), 0) == 0, "maximum USB control length rejected"); + Require(lastCall.length == UINT16_MAX, "maximum USB control length truncated"); + + nativeResult = 4; + Require(sceUsbdGetStringDescriptor(handle, 7, 0x409, buffer + 1, 8) == 4, "string descriptor byte count changed"); + Require(lastCall.handle == handle && lastCall.requestType == 0x80 && lastCall.request == LIBUSB_REQUEST_GET_DESCRIPTOR, "string descriptor request incorrect"); + Require(lastCall.value == 0x307 && lastCall.index == 0x409 && lastCall.length == 8 && lastCall.data == buffer + 1, "string descriptor setup incorrect"); + nativeResult = LIBUSB_ERROR_PIPE; + Require(sceUsbdGetStringDescriptor(handle, 7, 0x409, buffer, 8) == static_cast(0x80240009), "string descriptor error not mapped"); + + const int beforeInvalidCalls = nativeCalls; + constexpr auto invalidArgument = static_cast(0x80240002); + Require(sceUsbdControlTransfer(nullptr, 0, 0, 0, 0, buffer, 8, 0) == invalidArgument, "null handle accepted"); + Require(sceUsbdControlTransfer(handle, 0, 0, 0, 0, nullptr, 1, 0) == invalidArgument, "null nonempty buffer accepted"); + Require(sceUsbdControlTransfer(handle, 0, 0, 0, 0, buffer, -1, 0) == invalidArgument, "negative transfer length accepted"); + Require(sceUsbdControlTransfer(handle, 0, 0, 0, 0, buffer, 65536, 0) == invalidArgument, "oversized transfer length accepted"); + Require(sceUsbdGetStringDescriptor(nullptr, 0, 0, buffer, 8) == invalidArgument, "null string descriptor handle accepted"); + Require(sceUsbdGetStringDescriptor(handle, 0, 0, nullptr, 8) == invalidArgument, "null string descriptor buffer accepted"); + Require(sceUsbdGetStringDescriptor(handle, 0, 0, buffer, -1) == invalidArgument, "negative descriptor length accepted"); + Require(sceUsbdGetStringDescriptor(handle, 0, 0, buffer, 65536) == invalidArgument, "oversized descriptor length accepted"); + Require(nativeCalls == beforeInvalidCalls, "invalid request reached native USB backend"); + std::puts("USBD control tests passed"); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8f2178189d5b..d91fbf2967fc 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -21,7 +21,6 @@ Throughout the project, every function at every stage either **does exactly what - [libSceMbus](../../core/libs/prx/libSceMbus/Export.cpp) implements only the no-device path: initialization and termination succeed, no device events are reported and device queries return no entries - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing -- [libSceUsbd](../../core/libs/prx/libSceUsbd/Export.cpp) implements only the no-device path: `sceUsbdInit` and `sceUsbdExit` succeed without a host USB context, the device list is always empty and event handling waits for its timeout; calls that need a device throw - [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer`, `sceSystemServiceReenableMusicPlayer`, `sceSystemServiceDisableMediaPlay` and `sceSystemServiceReenableMediaPlay` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify, no system music player to pause or resume and no system media playback to disable. PPSA01521 calls `sceSystemServiceDisableMediaPlay` during start-up, and its wrappers for both pass no arguments - [libSceBluetoothHid](../../core/libs/prx/libSceBluetoothHid/Export.cpp) implements only the no-device path: initialization and callback and device registration succeed, the callback is never called because no device connects, and calls that need a connected device throw - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling @@ -242,6 +241,7 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [libSceUsbd](../../core/libs/prx/libSceUsbd/Export.cpp) uses host libusb 1.0.30 for enumeration, descriptors and synchronous device I/O. Signatures, descriptor layouts and error conversion follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/usbd/usbd.h) and its usbd.cpp; console lifecycle and invalid-argument error precedence remain unverified. Host USB permissions and driver support apply. Asynchronous submit/cancel and `sceUsbdCheckConnected` still throw; transfer preparation preserves the guest callback pointer but does not invoke it through host libusb. Callers must close devices and release their lists and descriptors before the final exit. Timeouts outside the host timeval range throw. No game or homebrew has been tested with this backend. - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Wide-character conversions](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `wcsrtombs` and [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still pass guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, they misread and miswrite guest strings. The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits