From cb01548ca2dbcdf8e7e467caedfaa4e88ab7feb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Fri, 6 Feb 2026 20:29:52 +0300 Subject: [PATCH 01/35] Wiimote to GunCon3 --- rpcs3/Emu/CMakeLists.txt | 2 + rpcs3/Emu/Io/GunCon3.cpp | 102 +++++++++ rpcs3/Emu/Io/GunCon3.h | 3 + rpcs3/Emu/Io/WiimoteManager.cpp | 259 ++++++++++++++++++++++ rpcs3/Emu/Io/WiimoteManager.h | 70 ++++++ rpcs3/Emu/Io/pad_types.h | 13 ++ rpcs3/Emu/RSX/Overlays/overlay_cursor.h | 3 +- rpcs3/Emu/system_config.h | 1 + rpcs3/rpcs3qt/CMakeLists.txt | 2 + rpcs3/rpcs3qt/emu_settings_type.h | 2 + rpcs3/rpcs3qt/gui_application.cpp | 2 + rpcs3/rpcs3qt/gui_application.h | 3 + rpcs3/rpcs3qt/main_window.cpp | 7 + rpcs3/rpcs3qt/main_window.ui | 6 + rpcs3/rpcs3qt/settings_dialog.cpp | 3 + rpcs3/rpcs3qt/settings_dialog.ui | 7 + rpcs3/rpcs3qt/tooltips.h | 1 + rpcs3/rpcs3qt/wiimote_settings_dialog.cpp | 125 +++++++++++ rpcs3/rpcs3qt/wiimote_settings_dialog.h | 17 ++ rpcs3/rpcs3qt/wiimote_settings_dialog.ui | 179 +++++++++++++++ 20 files changed, 806 insertions(+), 1 deletion(-) create mode 100644 rpcs3/Emu/Io/WiimoteManager.cpp create mode 100644 rpcs3/Emu/Io/WiimoteManager.h create mode 100644 rpcs3/rpcs3qt/wiimote_settings_dialog.cpp create mode 100644 rpcs3/rpcs3qt/wiimote_settings_dialog.h create mode 100644 rpcs3/rpcs3qt/wiimote_settings_dialog.ui diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index edb98a6fa8..6d79159dcb 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -401,6 +401,7 @@ target_sources(rpcs3_emu PRIVATE Io/GameTablet.cpp Io/GHLtar.cpp Io/GunCon3.cpp + Io/WiimoteManager.cpp Io/Infinity.cpp Io/interception.cpp Io/KamenRider.cpp @@ -652,6 +653,7 @@ target_link_libraries(rpcs3_emu 3rdparty::vulkan 3rdparty::glew 3rdparty::libusb + 3rdparty::hidapi 3rdparty::wolfssl 3rdparty::openal 3rdparty::cubeb diff --git a/rpcs3/Emu/Io/GunCon3.cpp b/rpcs3/Emu/Io/GunCon3.cpp index 522369d8a4..f12bda848f 100644 --- a/rpcs3/Emu/Io/GunCon3.cpp +++ b/rpcs3/Emu/Io/GunCon3.cpp @@ -1,11 +1,15 @@ #include "stdafx.h" #include "GunCon3.h" #include "MouseHandler.h" +#include +#include #include "Emu/IdManager.h" #include "Emu/Io/guncon3_config.h" #include "Emu/Cell/lv2/sys_usbd.h" #include "Emu/system_config.h" +#include "WiimoteManager.h" #include "Input/pad_thread.h" +#include "Emu/RSX/Overlays/overlay_cursor.h" LOG_CHANNEL(guncon3_log); @@ -127,6 +131,17 @@ usb_device_guncon3::usb_device_guncon3(u32 controller_index, const std::arraym_controller_index < b->m_controller_index; + }); + } + device = UsbDescriptorNode(USB_DESCRIPTOR_DEVICE, UsbDeviceDescriptor { .bcdUSB = 0x0110, @@ -174,6 +189,11 @@ usb_device_guncon3::usb_device_guncon3(u32 controller_index, const std::arrayget_states(); + + // Determine which Wiimote to use based on our ordinal position among all GunCons + int my_wiimote_index = -1; + { + std::lock_guard lock(s_instances_mutex); + auto it = std::lower_bound(s_instances.begin(), s_instances.end(), this, [](auto* a, auto* b) { + return a->m_controller_index < b->m_controller_index; + }); + // Since we sort by pointer adress/controller_index in add, and search by this ptr + // Actually lower_bound needs a value. std::find is safer for pointer identity. + auto found = std::find(s_instances.begin(), s_instances.end(), this); + if (found != s_instances.end()) + { + my_wiimote_index = std::distance(s_instances.begin(), found); + } + } + + if (my_wiimote_index >= 0 && static_cast(my_wiimote_index) < states.size()) + { + const auto& ws = states[my_wiimote_index]; + + if (ws.buttons & 0x0400) + { + gc.btn_trigger = 1; + } + + // Wiimote to GunCon3 Button Mapping + if (ws.buttons & 0x0800) gc.btn_a1 = 1; // Wiimote A -> A1 + if (ws.buttons & 0x1000) gc.btn_a2 = 1; // Wiimote Minus -> A2 + if (ws.buttons & 0x0010) gc.btn_c1 = 1; // Wiimote Plus -> C1 + if (ws.buttons & 0x0200) gc.btn_b1 = 1; // Wiimote 1 -> B1 + if (ws.buttons & 0x0100) gc.btn_b2 = 1; // Wiimote 2 -> B2 + if (ws.buttons & 0x8000) gc.btn_b3 = 1; // Wiimote Home -> B3 + if (ws.buttons & 0x0001) gc.btn_a3 = 1; // Wiimote Left (D-pad) -> A3 + if (ws.buttons & 0x0002) gc.btn_c2 = 1; // Wiimote Right (D-pad) + if (ws.buttons & 0x0008) gc.btn_b1 = 1; // D-pad Up -> B1 (Alt) + if (ws.buttons & 0x0004) gc.btn_b2 = 1; // D-pad Down -> B2 (Alt) + + if (ws.ir[0].x < 1023) + { + // Only use the primary pointer to avoid jumping between multiple IR points + s32 raw_x = ws.ir[0].x; + s32 raw_y = ws.ir[0].y; + + // Map to GunCon3 range (-32768..32767) + // X calculation (Right = 32767, Left = -32768) + s32 x_res = 32767 - (raw_x * 65535 / 1023); + // Y calculation (Top = 32767, Bottom = -32768) + // Swapping to inverted mapping as per user feedback + s32 y_res = 32767 - (raw_y * 65535 / 767); + + gc.gun_x = static_cast(std::clamp(x_res, -32768, 32767)); + gc.gun_y = static_cast(std::clamp(y_res, -32768, 32767)); + + // Draw the actual GunCon3 output to the overlay + // Mapping GunCon3 range back to virtual_width/height + s16 ax = static_cast((gc.gun_x + 32768) * rsx::overlays::overlay::virtual_width / 65535); + s16 ay = static_cast((32767 - gc.gun_y) * rsx::overlays::overlay::virtual_height / 65535); + + if (g_cfg.io.show_move_cursor) + { + // Use my_wiimote_index for color/cursor selection (0=Red, 1=Green...) + rsx::overlays::set_cursor(rsx::overlays::cursor_offset::cell_gem + my_wiimote_index, ax, ay, { 1.0f, 1.0f, 1.0f, 1.0f }, 100'000, false); + } + + if (ws.ir[1].x < 1023) + { + // Calculate "Z" (distance) based on spread of first two points to emulate depth sensor + s32 dx = static_cast(ws.ir[0].x) - ws.ir[1].x; + s32 dy = static_cast(ws.ir[0].y) - ws.ir[1].y; + gc.gun_z = static_cast(std::sqrt(dx * dx + dy * dy)); + } + } + + guncon3_encode(&gc, buf, m_key.data()); + return; + } + } + if (!is_input_allowed()) { guncon3_encode(&gc, buf, m_key.data()); diff --git a/rpcs3/Emu/Io/GunCon3.h b/rpcs3/Emu/Io/GunCon3.h index df013e4240..daa34415b1 100644 --- a/rpcs3/Emu/Io/GunCon3.h +++ b/rpcs3/Emu/Io/GunCon3.h @@ -14,4 +14,7 @@ public: private: u32 m_controller_index; std::array m_key{}; + + static inline std::vector s_instances; + static inline std::mutex s_instances_mutex; }; diff --git a/rpcs3/Emu/Io/WiimoteManager.cpp b/rpcs3/Emu/Io/WiimoteManager.cpp new file mode 100644 index 0000000000..f49f6155d8 --- /dev/null +++ b/rpcs3/Emu/Io/WiimoteManager.cpp @@ -0,0 +1,259 @@ +#include "stdafx.h" +#include "WiimoteManager.h" +#include "Emu/System.h" +#include "Emu/system_config.h" +#include + +// Nintendo +static constexpr u16 VID_NINTENDO = 0x057e; +static constexpr u16 PID_WIIMOTE = 0x0306; +static constexpr u16 PID_WIIMOTE_PLUS = 0x0330; + +// Mayflash DolphinBar +static constexpr u16 VID_MAYFLASH = 0x0079; +static constexpr u16 PID_DOLPHINBAR_START = 0x1800; +static constexpr u16 PID_DOLPHINBAR_END = 0x1803; + +WiimoteDevice::WiimoteDevice(hid_device_info* info) + : m_path(info->path) + , m_serial(info->serial_number ? info->serial_number : L"") +{ + m_handle = hid_open_path(info->path); + if (m_handle) + { + // 1. Connectivity Test (Matching wiimote_test) + u8 status_req[] = { 0x15, 0x00 }; + if (hid_write(m_handle, status_req, sizeof(status_req)) < 0) + { + hid_close(m_handle); + m_handle = nullptr; + return; + } + + // 2. Full Initialization + if (initialize_ir()) + { + m_state.connected = true; + } + else + { + hid_close(m_handle); + m_handle = nullptr; + } + } +} + +WiimoteDevice::~WiimoteDevice() +{ + if (m_handle) hid_close(m_handle); +} + +bool WiimoteDevice::initialize_ir() +{ + auto write_reg = [&](u32 addr, const std::vector& data) { + u8 buf[22] = {0}; + buf[0] = 0x16; // Write register + buf[1] = 0x04; + buf[2] = (addr >> 16) & 0xFF; + buf[3] = (addr >> 8) & 0xFF; + buf[4] = addr & 0xFF; + buf[5] = static_cast(data.size()); + std::copy(data.begin(), data.end(), &buf[6]); + if (hid_write(m_handle, buf, sizeof(buf)) < 0) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + return true; + }; + + // 1. Enable IR logic / Pixel Clock + u8 ir_on1[] = { 0x13, 0x04 }; + hid_write(m_handle, ir_on1, 2); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + u8 ir_on2[] = { 0x1a, 0x04 }; + hid_write(m_handle, ir_on2, 2); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + // 2. Enable IR Camera (Matching wiimote_test order) + if (!write_reg(0xb00030, {0x08})) return false; + + // 3. Sensitivity Level 3 (Exactly matching wiimote_test) + if (!write_reg(0xb00000, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x00, 0x41})) return false; + if (!write_reg(0xb0001a, {0x40, 0x00})) return false; + + // 4. IR Mode: Extended (3 bytes per point) + if (!write_reg(0xb00033, {0x03})) return false; + + // 5. Reporting mode: Buttons + Accel + IR + u8 mode[] = { 0x12, 0x00, 0x33 }; + if (hid_write(m_handle, mode, sizeof(mode)) < 0) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + return true; +} + +bool WiimoteDevice::update() +{ + if (!m_handle) return false; + + u8 buf[22]; + int res; + + // Fully drain the buffer until empty to ensure we have the most recent data. + // This avoids getting stuck behind a backlog of old reports (e.g. from before IR was enabled). + while ((res = hid_read_timeout(m_handle, buf, sizeof(buf), 0)) > 0) + { + // All data reports (0x30-0x3F) carry buttons in the same location (first 2 bytes). + // We mask out accelerometer LSBs (bits 5,6 of both bytes). + if ((buf[0] & 0xF0) == 0x30) + { + m_state.buttons = (buf[1] | (buf[2] << 8)) & 0x9F1F; + } + + // Mode 0x33: Buttons + Accel + IR (Extended Format) + if (buf[0] == 0x33) + { + // Parse Accelerometer (byte 1: bits 5,6 are X LSBs; byte 2: bit 5 Y LSB, bit 6 Z LSB) + m_state.acc_x = (buf[3] << 2) | ((buf[1] >> 5) & 3); + m_state.acc_y = (buf[4] << 2) | ((buf[2] >> 5) & 1); + m_state.acc_z = (buf[5] << 2) | ((buf[2] >> 6) & 1); + + // Each IR point is 3 bytes in Extended report 0x33. + for (int j = 0; j < 4; j++) + { + u8* ir = &buf[6 + j * 3]; + m_state.ir[j].x = (ir[0] | ((ir[2] & 0x30) << 4)); + m_state.ir[j].y = (ir[1] | ((ir[2] & 0xC0) << 2)); + m_state.ir[j].size = ir[2] & 0x0f; + } + } + } + + // hid_read_timeout returns -1 on error (e.g. device disconnected). + if (res < 0) return false; + + return true; +} + +static WiimoteManager* s_instance = nullptr; + +WiimoteManager::WiimoteManager() +{ + if (!s_instance) + s_instance = this; +} + +WiimoteManager::~WiimoteManager() +{ + stop(); + if (s_instance == this) + s_instance = nullptr; +} + +WiimoteManager* WiimoteManager::get_instance() +{ + return s_instance; +} + +void WiimoteManager::start() +{ + if (m_running) return; + + // Note: hid_init() is not thread-safe. ideally should be called once at app startup. + if (hid_init() != 0) return; + + m_running = true; + m_thread = std::thread(&WiimoteManager::thread_proc, this); +} + +void WiimoteManager::stop() +{ + m_running = false; + if (m_thread.joinable()) m_thread.join(); + hid_exit(); +} + +size_t WiimoteManager::get_device_count() +{ + std::shared_lock lock(m_mutex); + return m_devices.size(); +} + +std::vector WiimoteManager::get_states() +{ + std::shared_lock lock(m_mutex); + std::vector states; + states.reserve(m_devices.size()); + + for (const auto& dev : m_devices) + { + states.push_back(dev->get_state()); + } + return states; +} + + +void WiimoteManager::thread_proc() +{ + u32 counter = 0; + while (m_running) + { + // Scan every 2 seconds + if (counter++ % 200 == 0) + { + auto scan_and_add = [&](u16 vid, u16 pid_start, u16 pid_end) + { + hid_device_info* devs = hid_enumerate(vid, 0); + hid_device_info* cur = devs; + + while (cur) + { + if (cur->product_id >= pid_start && cur->product_id <= pid_end) + { + bool already_owned = false; + { + std::shared_lock lock(m_mutex); + for (const auto& d : m_devices) + { + if (d->get_path() == cur->path) + { + already_owned = true; + break; + } + } + } + + if (!already_owned) + { + auto dev = std::make_unique(cur); + if (dev->get_state().connected) + { + std::unique_lock lock(m_mutex); + m_devices.push_back(std::move(dev)); + } + } + } + cur = cur->next; + } + hid_free_enumeration(devs); + }; + + // Generic Wiimote / DolphinBar Mode 4 (Normal) + scan_and_add(VID_NINTENDO, PID_WIIMOTE, PID_WIIMOTE); + // Wiimote Plus + scan_and_add(VID_NINTENDO, PID_WIIMOTE_PLUS, PID_WIIMOTE_PLUS); + // Mayflash DolphinBar Mode 4 (Custom VID/PIDs) + // Supports up to 4 players (1800, 1801, 1802, 1803) + scan_and_add(VID_MAYFLASH, PID_DOLPHINBAR_START, PID_DOLPHINBAR_END); + } + + // Update all devices at 100Hz + { + std::unique_lock lock(m_mutex); + m_devices.erase(std::remove_if(m_devices.begin(), m_devices.end(), [](const auto& d) + { + return !const_cast(*d).update(); + }), m_devices.end()); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } +} diff --git a/rpcs3/Emu/Io/WiimoteManager.h b/rpcs3/Emu/Io/WiimoteManager.h new file mode 100644 index 0000000000..9decda4dea --- /dev/null +++ b/rpcs3/Emu/Io/WiimoteManager.h @@ -0,0 +1,70 @@ +#pragma once + +#include "util/types.hpp" +#include "Utilities/Thread.h" +#include "Utilities/mutex.h" +#include +#include +#include +#include +#include +#include +#include + +struct WiimoteIRPoint +{ + u16 x = 1023; + u16 y = 1023; + u8 size = 0; +}; + +struct WiimoteState +{ + u16 buttons = 0; + s16 acc_x = 0, acc_y = 0, acc_z = 0; + WiimoteIRPoint ir[4]; + bool connected = false; +}; + +class WiimoteDevice +{ +public: + WiimoteDevice(hid_device_info* info); + ~WiimoteDevice(); + + bool update(); + const WiimoteState& get_state() const { return m_state; } + std::string get_path() const { return m_path; } + std::wstring get_serial() const { return m_serial; } + +private: + hid_device* m_handle = nullptr; + std::string m_path; + std::wstring m_serial; + WiimoteState m_state; + + bool initialize_ir(); +}; + +class WiimoteManager +{ +public: + WiimoteManager(); + ~WiimoteManager(); + + static WiimoteManager* get_instance(); + + void start(); + void stop(); + + std::vector get_states(); + size_t get_device_count(); + +private: + std::thread m_thread; + atomic_t m_running{false}; + std::vector> m_devices; + shared_mutex m_mutex; + + void thread_proc(); +}; diff --git a/rpcs3/Emu/Io/pad_types.h b/rpcs3/Emu/Io/pad_types.h index 5fd9c8973a..90a9a67c2f 100644 --- a/rpcs3/Emu/Io/pad_types.h +++ b/rpcs3/Emu/Io/pad_types.h @@ -458,6 +458,18 @@ struct AnalogSensor {} }; +struct ir_point +{ + u16 x = 1023; + u16 y = 1023; + u16 size = 0; +}; + +struct ir_data +{ + std::array points; +}; + struct VibrateMotor { bool is_large_motor = false; @@ -519,6 +531,7 @@ struct Pad std::vector