Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cb01548
Wiimote to GunCon3
SomeoneIsWorking Feb 6, 2026
e20e74e
Wiimote to GunCon3 remapping support
SomeoneIsWorking Feb 6, 2026
17b72ae
Wiimote to GunCon3 PR review updates
SomeoneIsWorking Feb 7, 2026
0376d7c
Wiimote to GunCon3 apply (https://wiibrew.org/wiki/Wiimote#IR_Camera)…
SomeoneIsWorking Feb 7, 2026
6b68a70
Wiimote to GunCon3 removed 2 extra spaces
SomeoneIsWorking Feb 7, 2026
c4c93f0
Wiimote to GunCon3 removed fake wiimote_continuous_scanning
SomeoneIsWorking Feb 7, 2026
2579a76
Wiimote to GunCon3 compile error
SomeoneIsWorking Feb 7, 2026
f50ed87
Wiimote to GunCon3: removed bogus mapping remnant
SomeoneIsWorking Feb 7, 2026
81bc25d
Wiimote to GunCon3: rework disconnect/reconnect logic
SomeoneIsWorking Feb 7, 2026
da55ae9
Wiimote to GunCon3: IR healthchecks to determine connection state
SomeoneIsWorking Feb 7, 2026
234a128
Merge branch 'RPCS3:master' into wiimote-to-guncon3
SomeoneIsWorking Feb 9, 2026
607e045
Wiimote to GunCon3: revert pad_types.h
SomeoneIsWorking Feb 9, 2026
e274375
Wiimote to GunCon3: Address review comments
SomeoneIsWorking Feb 10, 2026
53717ad
Wiimote to GunCon3: Address review comments (Untested)
SomeoneIsWorking Feb 10, 2026
e74432d
Merge branch 'master' into wiimote-to-guncon3
SomeoneIsWorking Feb 10, 2026
b600979
Wiimote to GunCon3: retry github actions
SomeoneIsWorking Feb 10, 2026
caa0be0
Rename WiimoteManager to wiimote_handler, attempt to fix the structur…
SomeoneIsWorking Feb 10, 2026
b2b35fc
Attempt to get Windows build working
SomeoneIsWorking Feb 10, 2026
840df4e
Wiimote to GunCon3: GunCon3: use constants more
SomeoneIsWorking Feb 10, 2026
8bd7d3c
Attempt to resolve threading issues
SomeoneIsWorking Feb 10, 2026
d8cc077
Remove unnecessary Apple-specific HID API include from hid_pad_handle…
SomeoneIsWorking Feb 10, 2026
1369699
unused usings
SomeoneIsWorking Feb 10, 2026
bb9d12b
Address review comments
SomeoneIsWorking Feb 11, 2026
7e325a3
Merge branch 'master' into wiimote-to-guncon3
Megamouse Feb 11, 2026
d009e89
Address review comments
SomeoneIsWorking Feb 11, 2026
91b238e
Address review comments
SomeoneIsWorking Feb 11, 2026
c78f4fc
fix and utillize hid_instance::enumerate_devices
SomeoneIsWorking Feb 11, 2026
b7eeaac
Refactor wiimote_settings_dialog: Improve variable naming and initial…
SomeoneIsWorking Feb 14, 2026
02fb9b5
Refactor wiimote handling: Replace hardcoded values with MAX_WIIMOTES…
SomeoneIsWorking Feb 14, 2026
f9e8fd5
Wiimotes: Actually we have two constants MAX_WIIMOTES and MAX_WIIMOTE…
SomeoneIsWorking Feb 14, 2026
6828bec
Enhance Wiimote support for GunCon emulation: Add configuration optio…
SomeoneIsWorking Feb 14, 2026
6dde741
Refactor update_list: Extract repeated strings into variables for cla…
SomeoneIsWorking Feb 14, 2026
4b53c17
Refactor update_state: Simplify condition checks for Wiimote state re…
SomeoneIsWorking Feb 14, 2026
7a3e056
Fix update_list: Improve connection status label updates for Wiimote …
SomeoneIsWorking Feb 14, 2026
8c7a277
Fix GunCon3: Change type casting from int16_t to s16 for gun coordinates
SomeoneIsWorking Feb 14, 2026
2af6fe0
Refactor Wiimote configuration: Move types to a new header and update…
SomeoneIsWorking Feb 14, 2026
6a3a77f
Refactor Wiimote and GunCon3 integration: Update button mapping and c…
SomeoneIsWorking Feb 15, 2026
ff0e27d
Merge branch 'master' into wiimote-to-guncon3
SomeoneIsWorking Feb 15, 2026
0bb3983
Refactor wiimote_handler: Replace raw pointer with unique_ptr for ins…
SomeoneIsWorking Feb 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions rpcs3/Emu/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ target_sources(rpcs3_emu PRIVATE
Io/GHLtar.cpp
Io/GunCon3.cpp
Io/Infinity.cpp
Io/wiimote_config.cpp
Io/interception.cpp
Io/KamenRider.cpp
Io/KeyboardHandler.cpp
Expand Down Expand Up @@ -652,6 +653,7 @@ target_link_libraries(rpcs3_emu
3rdparty::vulkan
3rdparty::glew
3rdparty::libusb
3rdparty::hidapi
3rdparty::wolfssl
3rdparty::openal
3rdparty::cubeb
Expand Down
104 changes: 104 additions & 0 deletions rpcs3/Emu/Io/GunCon3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
#include "MouseHandler.h"
#include "Emu/IdManager.h"
#include "Emu/Io/guncon3_config.h"
#include "Emu/Io/wiimote_config.h"
#include "Emu/Cell/lv2/sys_usbd.h"
#include "Emu/system_config.h"
#include "Emu/RSX/Overlays/overlay_cursor.h"
#include "Input/wiimote_handler.h"
#include "Input/pad_thread.h"
#include <cmath>
#include <climits>
#include <algorithm>

LOG_CHANNEL(guncon3_log);

Expand Down Expand Up @@ -127,6 +133,18 @@ usb_device_guncon3::usb_device_guncon3(u32 controller_index, const std::array<u8
: usb_device_emulated(location)
, m_controller_index(controller_index)
{
{
std::lock_guard lock(s_instances_mutex);
s_instances.push_back(this);
// Sort instances by controller index (P1 < P2 < P3...)
// This ensures that the first available GunCon (e.g. at P3) takes the first Wiimote,
// and the second (e.g. at P4) takes the second Wiimote.
std::sort(s_instances.begin(), s_instances.end(), [](auto* a, auto* b)
{
return a->m_controller_index < b->m_controller_index;
});
}

device = UsbDescriptorNode(USB_DESCRIPTOR_DEVICE,
UsbDeviceDescriptor {
.bcdUSB = 0x0110,
Expand Down Expand Up @@ -174,6 +192,11 @@ usb_device_guncon3::usb_device_guncon3(u32 controller_index, const std::array<u8

usb_device_guncon3::~usb_device_guncon3()
{
std::lock_guard lock(s_instances_mutex);
if (auto it = std::find(s_instances.begin(), s_instances.end(), this); it != s_instances.end())
{
s_instances.erase(it);
}
}

void usb_device_guncon3::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer)
Expand All @@ -188,6 +211,81 @@ void usb_device_guncon3::control_transfer(u8 bmRequestType, u8 bRequest, u16 wVa

extern bool is_input_allowed();

bool usb_device_guncon3::handle_wiimote(GunCon3_data& gc)
{
if (!get_wiimote_config().use_for_guncon.get())
return false;

auto* wm = wiimote_handler::get_instance();
auto states = wm->get_states();

// Determine which Wiimote to use based on our ordinal position among all GunCons
s64 my_wiimote_index = -1;
{
std::lock_guard lock(s_instances_mutex);
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<usz>(my_wiimote_index) >= states.size())
return false;

const auto& ws = states[my_wiimote_index];
if (!ws.connected)
return false;

const auto& map = get_wiimote_config().guncon_mapping;
const auto is_pressed = [&](wiimote_button btn) { return (ws.buttons & static_cast<u16>(btn)) != 0; };

if (is_pressed(map.trigger.get())) gc.btn_trigger = 1;

// Wiimote to GunCon3 Button Mapping
if (is_pressed(map.a1.get())) gc.btn_a1 = 1;
if (is_pressed(map.a2.get())) gc.btn_a2 = 1;
if (is_pressed(map.a3.get())) gc.btn_a3 = 1;
if (is_pressed(map.b1.get())) gc.btn_b1 = 1;
if (is_pressed(map.b2.get())) gc.btn_b2 = 1;
if (is_pressed(map.b3.get())) gc.btn_b3 = 1;
if (is_pressed(map.c1.get())) gc.btn_c1 = 1;
if (is_pressed(map.c2.get())) gc.btn_c2 = 1;

// Secondary / Hardcoded Alts
if (is_pressed(map.b1_alt.get())) gc.btn_b1 = 1;
if (is_pressed(map.b2_alt.get())) gc.btn_b2 = 1;

if (ws.ir[0].x < 1023)
{
// Map Wiimote IR (0..1023) to GunCon3 range (-32768..32767)
const s32 raw_x = ws.ir[0].x;
const s32 raw_y = ws.ir[0].y;

const s32 x_res = SHRT_MAX - (raw_x * USHRT_MAX / 1023);
const s32 y_res = SHRT_MAX - (raw_y * USHRT_MAX / 767);

gc.gun_x = static_cast<s16>(std::clamp(x_res, SHRT_MIN, SHRT_MAX));
gc.gun_y = static_cast<s16>(std::clamp(y_res, SHRT_MIN, SHRT_MAX));

if (g_cfg.io.show_move_cursor)
{
const s16 ax = static_cast<s16>((gc.gun_x + SHRT_MAX + 1) * rsx::overlays::overlay::virtual_width / USHRT_MAX);
const s16 ay = static_cast<s16>((SHRT_MAX - gc.gun_y) * rsx::overlays::overlay::virtual_height / USHRT_MAX);
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)
{
const s32 dx = static_cast<s32>(ws.ir[0].x) - ws.ir[1].x;
const s32 dy = static_cast<s32>(ws.ir[0].y) - ws.ir[1].y;
gc.gun_z = static_cast<s16>(std::sqrt(dx * dx + dy * dy));
}
}

return true;
}

void usb_device_guncon3::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer)
{
transfer->fake = true;
Expand All @@ -213,6 +311,12 @@ void usb_device_guncon3::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint,
return;
}

if (handle_wiimote(gc))
{
guncon3_encode(&gc, buf, m_key.data());
return;
}

if (g_cfg.io.mouse == mouse_handler::null)
{
guncon3_log.warning("GunCon3 requires a Mouse Handler enabled");
Expand Down
7 changes: 7 additions & 0 deletions rpcs3/Emu/Io/GunCon3.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once

#include "Emu/Io/usb_device.h"
#include <vector>
#include <mutex>

class usb_device_guncon3 : public usb_device_emulated
{
Expand All @@ -14,4 +16,9 @@ class usb_device_guncon3 : public usb_device_emulated
private:
u32 m_controller_index;
std::array<u8, 8> m_key{};

bool handle_wiimote(struct GunCon3_data& gc);

static inline std::vector<usb_device_guncon3*> s_instances;
static inline std::mutex s_instances_mutex;
};
58 changes: 58 additions & 0 deletions rpcs3/Emu/Io/wiimote_config.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include "stdafx.h"
#include "wiimote_config.h"
#include "Utilities/File.h"

LOG_CHANNEL(wiimote_log, "Wiimote");

template <>
void fmt_class_string<wiimote_button>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](wiimote_button value)
{
switch (value)
{
case wiimote_button::None: return "None";
case wiimote_button::Left: return "Left";
case wiimote_button::Right: return "Right";
case wiimote_button::Down: return "Down";
case wiimote_button::Up: return "Up";
case wiimote_button::Plus: return "Plus";
case wiimote_button::Two: return "Two";
case wiimote_button::One: return "One";
case wiimote_button::B: return "B";
case wiimote_button::A: return "A";
case wiimote_button::Minus: return "Minus";
case wiimote_button::Home: return "Home";
}
return unknown;
});
}

cfg_wiimote& get_wiimote_config()
{
static cfg_wiimote instance;
return instance;
}

cfg_wiimote::cfg_wiimote()
: cfg::node()
, path(fs::get_config_dir(true) + "wiimote.yml")
{
}

bool cfg_wiimote::load()
{
if (fs::file f{path, fs::read})
{
return cfg::node::from_string(f.to_string());
}
return false;
}

void cfg_wiimote::save() const
{
if (!cfg::node::save(path))
{
wiimote_log.error("Failed to save wiimote config to '%s'", path);
}
}
35 changes: 35 additions & 0 deletions rpcs3/Emu/Io/wiimote_config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include "Utilities/Config.h"
#include "Input/wiimote_types.h"

struct cfg_wiimote : cfg::node
{
cfg_wiimote();
bool load();
void save() const;

cfg::_bool use_for_guncon{ this, "UseForGunCon", true };

struct node_guncon_mapping : cfg::node
{
node_guncon_mapping(cfg::node* _parent) : cfg::node(_parent, "GunCon Mapping") {}

cfg::_enum<wiimote_button> trigger{ this, "Trigger", wiimote_button::B };
cfg::_enum<wiimote_button> a1{ this, "A1", wiimote_button::A };
cfg::_enum<wiimote_button> a2{ this, "A2", wiimote_button::Minus };
cfg::_enum<wiimote_button> a3{ this, "A3", wiimote_button::Left };
cfg::_enum<wiimote_button> b1{ this, "B1", wiimote_button::One };
cfg::_enum<wiimote_button> b2{ this, "B2", wiimote_button::Two };
cfg::_enum<wiimote_button> b3{ this, "B3", wiimote_button::Home };
cfg::_enum<wiimote_button> c1{ this, "C1", wiimote_button::Plus };
cfg::_enum<wiimote_button> c2{ this, "C2", wiimote_button::Right };

cfg::_enum<wiimote_button> b1_alt{ this, "B1_Alt", wiimote_button::Up };
cfg::_enum<wiimote_button> b2_alt{ this, "B2_Alt", wiimote_button::Down };
} guncon_mapping{ this };

const std::string path;
};

cfg_wiimote& get_wiimote_config();
3 changes: 2 additions & 1 deletion rpcs3/Emu/RSX/Overlays/overlay_cursor.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ namespace rsx
enum cursor_offset : u32
{
cell_gem = 0, // CELL_GEM_MAX_NUM = 4 Move controllers
last = 4
wiimote_ir = 4, // 4 points per Wiimote * 4 Wiimotes (up to 16 points)
last = 20
};

class cursor_item
Expand Down
104 changes: 104 additions & 0 deletions rpcs3/Input/hid_instance.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#include "stdafx.h"
#include "hid_instance.h"
#include "util/logs.hpp"
#include "Emu/System.h"
#include "Utilities/Thread.h"

#if defined(__APPLE__)
#include "3rdparty/hidapi/hidapi/mac/hidapi_darwin.h"
#endif

LOG_CHANNEL(hid_log, "HID");

std::mutex g_hid_mutex;

hid_instance::~hid_instance()
{
std::lock_guard lock(m_hid_mutex);

// Only exit HIDAPI once on exit. HIDAPI uses a global state internally...
if (m_initialized)
{
hid_log.notice("Exiting HIDAPI...");

if (hid_exit() != 0)
{
hid_log.error("hid_exit failed!");
}
}
}

bool hid_instance::initialize()
{
std::lock_guard lock(m_hid_mutex);

// Only init HIDAPI once. HIDAPI uses a global state internally...
if (m_initialized)
{
return true;
}

hid_log.notice("Initializing HIDAPI ...");

#if defined(__APPLE__)
int error_code = 0;
Emu.BlockingCallFromMainThread([&error_code]()
{
error_code = hid_init();
hid_darwin_set_open_exclusive(0);
}, false);
#else
const int error_code = hid_init();
#endif
if (error_code != 0)
{
hid_log.fatal("hid_init error %d: %s", error_code, hid_error(nullptr));
return false;
}

m_initialized = true;
return true;
}

void hid_instance::enumerate_devices(u16 vid, u16 pid, std::function<void(hid_device_info*)> callback)
{
std::lock_guard lock(g_hid_mutex);
#if defined(__APPLE__)
Emu.BlockingCallFromMainThread([&]()
{
hid_device_info* devs = hid_enumerate(vid, pid);
callback(devs);
hid_free_enumeration(devs);
}, false);
#else
hid_device_info* devs = hid_enumerate(vid, pid);
callback(devs);
hid_free_enumeration(devs);
#endif
}

hid_device* hid_instance::open_path(const char* path)
{
std::lock_guard lock(g_hid_mutex);
#if defined(__APPLE__)
if (!thread_ctrl::is_main())
{
hid_device* dev = nullptr;
Emu.BlockingCallFromMainThread([&]() { dev = hid_open_path(path); }, false);
return dev;
}
#endif
return hid_open_path(path);
}

void hid_instance::close(hid_device* dev)
{
if (!dev) return;

std::lock_guard lock(g_hid_mutex);
#if defined(__APPLE__)
Emu.BlockingCallFromMainThread([&]() { hid_close(dev); }, false);
#else
hid_close(dev);
#endif
}
Loading