[HID] Redesigned support for Portals

- Added hotplug support on Windows
- Added support for XamInputNonControllerGetRawEx & XamInputNonControllerSetRawEx
This commit is contained in:
Gliniak
2026-03-13 22:31:24 +01:00
committed by Radosław Gliński
parent 3b8debcf5b
commit d37c22aad0
24 changed files with 427 additions and 340 deletions

View File

@@ -7,5 +7,5 @@ if(_hid_demo AND _hid_srcs)
list(REMOVE_ITEM _hid_srcs ${_hid_demo})
set_target_properties(xenia-hid PROPERTIES SOURCES "${_hid_srcs}")
endif()
target_link_libraries(xenia-hid PUBLIC xenia-base xenia-hid-skylander)
target_link_libraries(xenia-hid PUBLIC xenia-base xenia-hid-portal)
xe_target_defaults(xenia-hid)

View File

@@ -16,9 +16,8 @@
#include "xenia/hid/input_driver.h"
#include "xenia/kernel/util/shim_utils.h"
#include "xenia/hid/skylander/skylander_emulated.h"
#ifdef XE_PLATFORM_WIN32
#include "xenia/hid/skylander/skylander_hardware.h"
#include "xenia/hid/portal/hardware_portal.h"
#endif // XE_PLATFORM_WIN32
namespace xe {
@@ -34,10 +33,8 @@ DEFINE_double(
"Defines deadzone level for right stick. Allowed range [0.0-1.0].", "HID");
InputSystem::InputSystem(xe::ui::Window* window) : window_(window) {
skylander_portal_ = std::make_unique<SkylanderPortalEmulated>();
#ifdef XE_PLATFORM_WIN32
skylander_portal_ = std::make_unique<SkylanderPortalLibusb>();
portal_ = std::make_unique<HardwarePortal>();
#endif // XE_PLATFORM_WIN32
}

View File

@@ -16,7 +16,7 @@
#include "xenia/base/mutex.h"
#include "xenia/hid/input.h"
#include "xenia/hid/input_driver.h"
#include "xenia/hid/skylander/skylander_portal.h"
#include "xenia/hid/portal/portal.h"
#include "xenia/xbox.h"
namespace xe {
@@ -56,7 +56,7 @@ class InputSystem {
uint32_t GetLastUsedSlot() const { return last_used_slot; }
SkylanderPortal* GetSkylanderPortal() { return skylander_portal_.get(); }
Portal* GetPortal() { return portal_.get(); }
std::unique_lock<xe_unlikely_mutex> lock();
@@ -77,7 +77,7 @@ class InputSystem {
std::vector<std::unique_ptr<InputDriver>> drivers_;
std::unique_ptr<SkylanderPortal> skylander_portal_;
std::unique_ptr<Portal> portal_;
std::bitset<XUserMaxUserCount> connected_slots = {};
std::array<std::pair<joystick_value, joystick_value>, XUserMaxUserCount>

View File

@@ -0,0 +1,13 @@
add_library(xenia-hid-portal STATIC
${CMAKE_CURRENT_SOURCE_DIR}/portal.h
${CMAKE_CURRENT_SOURCE_DIR}/portal.cc
)
if(WIN32)
target_sources(xenia-hid-portal PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/hardware_portal.h
${CMAKE_CURRENT_SOURCE_DIR}/hardware_portal.cc
)
target_link_libraries(xenia-hid-portal PUBLIC libusb)
endif()
xe_target_defaults(xenia-hid-portal)

View File

@@ -0,0 +1,120 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/hid/portal/hardware_portal.h"
#include "xenia/base/logging.h"
namespace xe {
namespace hid {
HardwarePortal::HardwarePortal() : Portal() {
libusb_init(&context_);
OpenDevice();
}
HardwarePortal::~HardwarePortal() {
if (handle_) {
CloseDevice();
}
libusb_exit(context_);
}
void HardwarePortal::OpenDevice() {
if (!context_ || connected_) {
return;
}
libusb_device** devs;
const ssize_t cnt = libusb_get_device_list(context_, &devs);
if (cnt < 0) {
// No device available... It might appear later.
return;
}
for (ssize_t i = 0; i < cnt; ++i) {
libusb_device* dev = devs[i];
libusb_device_descriptor desc;
if (libusb_get_device_descriptor(dev, &desc) == 0) {
// Check if this device matches the target Vendor ID and Product ID.
// Small limitation. We're only supporting one device being connected at
// the time.
for (const auto& entry : kPortalVendorProductIdList) {
if (desc.idVendor == entry.first && desc.idProduct == entry.second) {
if (libusb_open(dev, &handle_) == 0) {
libusb_claim_interface(handle_, 0);
connected_ = true;
// We found device. No need to go thorugh remaining IDs.
break;
}
}
}
}
// We found device. No need to go thorugh remaining devices.
if (connected_) {
break;
}
}
libusb_free_device_list(devs, 0);
}
void HardwarePortal::CloseDevice() {
if (!connected_) {
return;
}
libusb_release_interface(handle_, 0);
libusb_close(handle_);
connected_ = false;
handle_ = nullptr;
}
X_STATUS HardwarePortal::ReadInternal(std::span<uint8_t> data,
int32_t& read_count) {
const int result = libusb_interrupt_transfer(
handle_, read_endpoint, data.data(), static_cast<int>(data.size()),
&read_count, timeout);
switch (result) {
case LIBUSB_ERROR_NO_DEVICE:
return X_ERROR_DEVICE_NOT_CONNECTED;
break;
case LIBUSB_ERROR_TIMEOUT:
return X_ERROR_SUCCESS;
default:
break;
}
if (result < 0) {
XELOGW("Portal[Read] returned error: {:08X}", result);
return X_ERROR_FUNCTION_FAILED;
}
return X_ERROR_SUCCESS;
}
X_STATUS HardwarePortal::WriteInternal(std::span<uint8_t> data) {
const int result = libusb_interrupt_transfer(
handle_, write_endpoint, data.data(), static_cast<int>(data.size()),
nullptr, timeout);
if (result < 0) {
XELOGW("Portal[Write] returned error: {:08X}", result);
return X_ERROR_DEVICE_NOT_CONNECTED;
}
return X_ERROR_SUCCESS;
};
void HardwarePortal::OnDeviceArrival() { OpenDevice(); };
void HardwarePortal::OnDeviceRemoval() { CloseDevice(); };
} // namespace hid
} // namespace xe

View File

@@ -0,0 +1,52 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_HID_PORTAL_HARDWARE_PORTAL_H_
#define XENIA_HID_PORTAL_HARDWARE_PORTAL_H_
#include <array>
#include "xenia/hid/portal/portal.h"
#include "third_party/libusb/libusb/libusb.h"
namespace xe {
namespace hid {
constexpr std::array<std::pair<uint16_t, uint16_t>, 2>
kPortalVendorProductIdList = {
std::pair<uint16_t, uint16_t>{0x1430, 0x1F17},
std::pair<uint16_t, uint16_t>{0x24C6, 0xFA00}};
class HardwarePortal final : public Portal {
public:
HardwarePortal();
~HardwarePortal() override;
virtual void OnDeviceArrival() override;
virtual void OnDeviceRemoval() override;
private:
virtual void OpenDevice() override;
virtual void CloseDevice() override;
virtual X_STATUS ReadInternal(std::span<uint8_t> data,
int32_t& read_count) override;
virtual X_STATUS WriteInternal(std::span<uint8_t> data) override;
const uint8_t read_endpoint = 0x81;
const uint8_t write_endpoint = 0x02;
const uint16_t timeout = 100;
libusb_context* context_ = nullptr;
libusb_device_handle* handle_ = nullptr;
};
} // namespace hid
} // namespace xe
#endif // XENIA_HID_PORTAL_HARDWARE_PORTAL_H_

View File

@@ -0,0 +1,74 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/hid/portal/portal.h"
namespace xe {
namespace hid {
Portal::Portal() {}
Portal::~Portal() {}
bool Portal::IsConnected() {
std::lock_guard<xe_mutex> guard(lock_);
return connected_;
}
X_STATUS Portal::Read(std::span<uint8_t> data, uint32_t& bytes_read,
uint16_t& state) {
std::lock_guard<xe_mutex> guard(lock_);
if (!connected_) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
if (data.size() > kPortalBufferSize) {
return X_ERROR_INVALID_PARAMETER;
}
int32_t read_count = 0;
X_STATUS status = ReadInternal(data, read_count);
if (XSUCCEEDED(status)) {
// According to XAM decompilation logic here is reversed. Difference in
// status should return 1, but these values are taken from internal parts of
// xinput raw implementation, so it might be completely opposite.
state = previous_status_ == status;
bytes_read = read_count;
}
previous_status_ = status;
if (status == X_ERROR_DEVICE_NOT_CONNECTED) {
CloseDevice();
}
return status;
}
X_STATUS Portal::Write(std::span<uint8_t> data) {
std::lock_guard<xe_mutex> guard(lock_);
if (!connected_) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
if (data.size() > kPortalBufferSize) {
return X_ERROR_INVALID_PARAMETER;
}
const X_STATUS status = WriteInternal(data);
if (status == X_ERROR_DEVICE_NOT_CONNECTED) {
CloseDevice();
}
return status;
}
} // namespace hid
} // namespace xe

View File

@@ -0,0 +1,55 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_HID_PORTAL_PORTAL_H_
#define XENIA_HID_PORTAL_PORTAL_H_
#include <span>
#include "xenia/base/mutex.h"
#include "xenia/xbox.h"
namespace xe {
namespace hid {
constexpr uint8_t kPortalBufferSize = 0x20;
class Portal {
public:
Portal();
virtual ~Portal();
bool IsConnected();
virtual X_STATUS Read(std::span<uint8_t> data, uint32_t& bytes_read,
uint16_t& state);
virtual X_STATUS Write(std::span<uint8_t> data);
virtual void OnDeviceArrival() = 0;
virtual void OnDeviceRemoval() = 0;
protected:
bool connected_ = false;
private:
virtual void OpenDevice() = 0;
virtual void CloseDevice() = 0;
virtual X_STATUS ReadInternal(std::span<uint8_t> data,
int32_t& read_count) = 0;
virtual X_STATUS WriteInternal(std::span<uint8_t> data) = 0;
xe_mutex lock_;
X_STATUS previous_status_ = 0;
};
} // namespace hid
} // namespace xe
#endif

View File

@@ -1,14 +0,0 @@
add_library(xenia-hid-skylander STATIC
${CMAKE_CURRENT_SOURCE_DIR}/skylander_portal.h
${CMAKE_CURRENT_SOURCE_DIR}/skylander_portal.cc
${CMAKE_CURRENT_SOURCE_DIR}/skylander_emulated.h
${CMAKE_CURRENT_SOURCE_DIR}/skylander_emulated.cc
)
if(WIN32)
target_sources(xenia-hid-skylander PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/skylander_hardware.h
${CMAKE_CURRENT_SOURCE_DIR}/skylander_hardware.cc
)
target_link_libraries(xenia-hid-skylander PUBLIC libusb)
endif()
xe_target_defaults(xenia-hid-skylander)

View File

@@ -1,32 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/hid/skylander/skylander_emulated.h"
namespace xe {
namespace hid {
SkylanderPortalEmulated::SkylanderPortalEmulated() : SkylanderPortal() {}
SkylanderPortalEmulated::~SkylanderPortalEmulated() {}
bool SkylanderPortalEmulated::init_device() { return false; }
void SkylanderPortalEmulated::destroy_device() {}
X_STATUS SkylanderPortalEmulated::read(std::vector<uint8_t>& data) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
X_STATUS SkylanderPortalEmulated::write(std::vector<uint8_t>& data) {
return X_ERROR_DEVICE_NOT_CONNECTED;
};
} // namespace hid
} // namespace xe

View File

@@ -1,34 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_HID_SKYLANDER_SKYLANDER_PORTAL_EMULATED_H_
#define XENIA_HID_SKYLANDER_SKYLANDER_PORTAL_EMULATED_H_
#include "xenia/hid/skylander/skylander_portal.h"
namespace xe {
namespace hid {
class SkylanderPortalEmulated final : public SkylanderPortal {
public:
SkylanderPortalEmulated();
~SkylanderPortalEmulated() override;
X_STATUS read(std::vector<uint8_t>& data) override;
X_STATUS write(std::vector<uint8_t>& data) override;
private:
bool init_device() override;
void destroy_device() override;
};
} // namespace hid
} // namespace xe
#endif

View File

@@ -1,121 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/hid/skylander/skylander_hardware.h"
namespace xe {
namespace hid {
SkylanderPortalLibusb::SkylanderPortalLibusb() : SkylanderPortal() {
libusb_init(&context_);
}
SkylanderPortalLibusb::~SkylanderPortalLibusb() {
if (handle_) {
destroy_device();
}
libusb_exit(context_);
}
bool SkylanderPortalLibusb::init_device() {
if (!context_) {
return false;
}
libusb_device** devs;
ssize_t cnt = libusb_get_device_list(context_, &devs);
if (cnt < 0) {
// No device available... It might appear later.
return false;
}
bool device_found = false;
for (ssize_t i = 0; i < cnt; ++i) {
libusb_device* dev = devs[i];
struct libusb_device_descriptor desc;
if (libusb_get_device_descriptor(dev, &desc) == 0) {
// Check if this device matches the target Vendor ID and Product ID
if (desc.idVendor == portal_vendor_product_id.first &&
desc.idProduct == portal_vendor_product_id.second) {
libusb_device_handle* handle;
if (libusb_open(dev, &handle) == 0) {
libusb_claim_interface(handle, 0);
handle_ = reinterpret_cast<uintptr_t*>(handle);
device_found = true;
}
}
}
}
libusb_free_device_list(devs, 0);
return device_found;
}
void SkylanderPortalLibusb::destroy_device() {
libusb_release_interface(reinterpret_cast<libusb_device_handle*>(handle_), 0);
libusb_close(reinterpret_cast<libusb_device_handle*>(handle_));
handle_ = nullptr;
}
X_STATUS SkylanderPortalLibusb::read(std::vector<uint8_t>& data) {
if (!is_initialized()) {
if (!init_device()) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
}
if (data.size() > skylander_buffer_size) {
return X_ERROR_INVALID_PARAMETER;
}
const int result = libusb_interrupt_transfer(
reinterpret_cast<libusb_device_handle*>(handle_), read_endpoint,
data.data(), static_cast<int>(data.size()), nullptr, timeout);
if (result == LIBUSB_ERROR_NO_DEVICE) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
if (result < 0) {
destroy_device();
}
return X_ERROR_SUCCESS;
}
X_STATUS SkylanderPortalLibusb::write(std::vector<uint8_t>& data) {
if (!is_initialized()) {
if (!init_device()) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
}
if (data.size() > skylander_buffer_size) {
return X_ERROR_INVALID_PARAMETER;
}
const int result = libusb_interrupt_transfer(
reinterpret_cast<libusb_device_handle*>(handle_), write_endpoint,
data.data(), static_cast<int>(data.size()), nullptr, timeout);
if (result == LIBUSB_ERROR_NO_DEVICE) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
if (result < 0) {
destroy_device();
}
return X_ERROR_SUCCESS;
};
} // namespace hid
} // namespace xe

View File

@@ -1,47 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_HID_SKYLANDER_SKYLANDER_PORTAL_LIBUSB_H_
#define XENIA_HID_SKYLANDER_SKYLANDER_PORTAL_LIBUSB_H_
// Include order is important here. Including skylander_portal.h after libusb
// will cause conflicts.
#include "xenia/hid/skylander/skylander_portal.h"
#include "third_party/libusb/libusb/libusb.h"
namespace xe {
namespace hid {
class SkylanderPortalLibusb final : public SkylanderPortal {
public:
SkylanderPortalLibusb();
~SkylanderPortalLibusb() override;
X_STATUS read(std::vector<uint8_t>& data) override;
X_STATUS write(std::vector<uint8_t>& data) override;
private:
bool init_device() override;
void destroy_device() override;
bool is_initialized() const { return handle_ != nullptr; }
const uint8_t read_endpoint = 0x81;
const uint8_t write_endpoint = 0x02;
const uint16_t timeout = 300;
libusb_context* context_ = nullptr;
uintptr_t* handle_ = nullptr;
};
} // namespace hid
} // namespace xe
#endif

View File

@@ -1,19 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/hid/skylander/skylander_portal.h"
namespace xe {
namespace hid {
SkylanderPortal::SkylanderPortal() {}
SkylanderPortal::~SkylanderPortal() {}
} // namespace hid
} // namespace xe

View File

@@ -1,40 +0,0 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Xenia Canary. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_HID_SKYLANDER_SKYLANDER_PORTAL_H_
#define XENIA_HID_SKYLANDER_SKYLANDER_PORTAL_H_
#include <vector>
#include "xenia/xbox.h"
namespace xe {
namespace hid {
constexpr std::pair<uint16_t, uint16_t> portal_vendor_product_id = {0x1430,
0x1F17};
constexpr uint8_t skylander_buffer_size = 0x20;
class SkylanderPortal {
public:
SkylanderPortal();
virtual ~SkylanderPortal();
virtual X_STATUS read(std::vector<uint8_t>& data) = 0;
virtual X_STATUS write(std::vector<uint8_t>& data) = 0;
private:
virtual bool init_device() = 0;
virtual void destroy_device() = 0;
};
} // namespace hid
} // namespace xe
#endif