1
0
mirror of https://github.com/RPCS3/rpcs3.git synced 2024-11-22 02:32:36 +01:00

cellGem: implement real ps move handler

This commit is contained in:
Megamouse 2024-07-08 20:17:21 +02:00
parent e7faec6b0e
commit b89cc9b973
34 changed files with 3628 additions and 130 deletions

View File

@ -6,12 +6,15 @@
#include "Emu/Cell/PPUModule.h"
#include "Emu/Cell/timers.hpp"
#include "Emu/Io/MouseHandler.h"
#include "Emu/Io/PadHandler.h"
#include "Emu/Io/gem_config.h"
#include "Emu/system_config.h"
#include "Emu/System.h"
#include "Emu/IdManager.h"
#include "Emu/RSX/Overlays/overlay_cursor.h"
#include "Input/pad_thread.h"
#include "Input/ps_move_config.h"
#include "Input/ps_move_tracker.h"
#ifdef HAVE_LIBEVDEV
#include "Input/evdev_gun_handler.h"
@ -174,7 +177,8 @@ using gun_thread = named_thread<gun_handler>;
#endif
cfg_gems g_cfg_gem;
cfg_gems g_cfg_gem_real;
cfg_fake_gems g_cfg_gem_fake;
struct gem_config_data
{
@ -232,6 +236,7 @@ public:
u32 hue = 0; // Tracking hue of the motion controller
f32 distance{1500.0f}; // Distance from the camera in mm
f32 radius{10.0f}; // Radius of the sphere in camera pixels
bool radius_valid = true; // If the radius and distance of the sphere was computed.
bool is_calibrating{false}; // Whether or not we are currently calibrating
u64 calibration_start_us{0}; // The start timestamp of the calibration in microseconds
@ -252,7 +257,7 @@ public:
std::array<gem_controller, CELL_GEM_MAX_NUM> controllers;
u32 connected_controllers = 0;
atomic_t<bool> video_conversion_in_progress{false};
atomic_t<bool> update_started{false};
atomic_t<bool> updating{false};
u32 camera_frame{};
u32 memory_ptr{};
@ -298,6 +303,26 @@ public:
switch (g_cfg.io.move)
{
case move_handler::real:
{
connected_controllers = 0;
std::lock_guard lock(pad::g_pad_mutex);
const auto handler = pad::get_current_handler();
for (u32 i = 0; i < std::min<u32>(attribute.max_connect, CELL_GEM_MAX_NUM); i++)
{
const auto& pad = ::at32(handler->GetPads(), pad_num(i));
if (pad && pad->m_pad_handler == pad_handler::move && (pad->m_port_status & CELL_PAD_STATUS_CONNECTED))
{
connected_controllers++;
if (gem_num == i)
{
is_connected = true;
}
}
}
break;
}
case move_handler::fake:
{
connected_controllers = 0;
@ -368,9 +393,14 @@ public:
gem_config_data()
{
if (!g_cfg_gem.load())
if (!g_cfg_gem_real.load())
{
cellGem.notice("Could not load gem config. Using defaults.");
cellGem.notice("Could not load real gem config. Using defaults.");
}
if (!g_cfg_gem_fake.load())
{
cellGem.notice("Could not load fake gem config. Using defaults.");
}
cellGem.notice("Gem config=\n", g_cfg_gem.to_string());
@ -390,7 +420,7 @@ public:
[[maybe_unused]] const s32 version = GET_OR_USE_SERIALIZATION_VERSION(ar.is_writing(), cellGem);
ar(attribute, vc_attribute, status_flags, enable_pitch_correction, inertial_counter, controllers
, connected_controllers, update_started, camera_frame, memory_ptr, start_timestamp_us);
, connected_controllers, updating, camera_frame, memory_ptr, start_timestamp_us);
}
gem_config_data(utils::serial& ar)
@ -400,15 +430,24 @@ public:
if (ar.is_writing())
return;
if (!g_cfg_gem.load())
if (!g_cfg_gem_real.load())
{
cellGem.notice("Could not load gem config. Using defaults.");
cellGem.notice("Could not load real gem config. Using defaults.");
}
cellGem.notice("Gem config=\n", g_cfg_gem.to_string());
if (!g_cfg_gem_fake.load())
{
cellGem.notice("Could not load fake gem config. Using defaults.");
}
cellGem.notice("Real gem config=\n", g_cfg_gem_real.to_string());
cellGem.notice("Fake gem config=\n", g_cfg_gem_fake.to_string());
}
};
extern std::pair<u32, u32> get_video_resolution(const CellCameraInfoEx& info);
extern u32 get_buffer_size_by_format(s32 format, s32 width, s32 height);
static inline int32_t cellGemGetVideoConvertSize(s32 output_format)
{
switch (output_format)
@ -605,6 +644,207 @@ void gem_config_data::operator()()
using gem_config = named_thread<gem_config_data>;
class gem_tracker
{
public:
gem_tracker()
{
}
bool is_busy()
{
return m_busy;
}
void wake_up()
{
m_wake_up.release(1);
m_wake_up.notify_one();
}
void wait_for_result()
{
if (!m_done)
{
m_done.wait(0);
m_done.release(0);
}
}
bool set_image(u32 addr)
{
if (!addr)
return false;
auto& g_camera = g_fxo->get<camera_thread>();
std::lock_guard lock(g_camera.mutex);
m_camera_info = g_camera.info;
if (m_camera_info.buffer.addr() != addr && m_camera_info.pbuf[0].addr() != addr && m_camera_info.pbuf[1].addr() != addr)
{
cellGem.error("gem_tracker: unexcepted image address: addr=0x%x, expected one of: 0x%x, 0x%x, 0x%x", addr, m_camera_info.buffer.addr(), m_camera_info.pbuf[0].addr(), m_camera_info.pbuf[1].addr());
return false;
}
// Copy image data for further processing
const auto& [width, height] = get_video_resolution(m_camera_info);
const u32 expected_size = get_buffer_size_by_format(m_camera_info.format, width, height);
if (!m_camera_info.bytesize || m_camera_info.bytesize != expected_size)
{
cellGem.error("gem_tracker: unexcepted image size: size=%d, expected=%d", m_camera_info.bytesize, expected_size);
return false;
}
if (!m_camera_info.bytesize)
{
cellGem.error("gem_tracker: unexcepted image size: %d", m_camera_info.bytesize);
return false;
}
m_tracker.set_image_data(m_camera_info.buffer.get_ptr(), m_camera_info.bytesize, m_camera_info.width, m_camera_info.height, m_camera_info.format);
return true;
}
bool hue_is_trackable(u32 hue)
{
if (g_cfg.io.move != move_handler::real)
{
return 1; // potentially true if less than 20 pixels have the hue
}
return hue < m_hues.size() && m_hues[hue] < 20; // potentially true if less than 20 pixels have the hue
}
ps_move_info& get_info(u32 gem_num)
{
return ::at32(m_info, gem_num);
}
gem_tracker& operator=(thread_state)
{
wake_up();
return *this;
}
void operator()()
{
if (g_cfg.io.move != move_handler::real)
{
return;
}
if (!g_cfg_move.load())
{
cellGem.notice("Could not load PS Move config. Using defaults.");
}
auto& gem = g_fxo->get<gem_config>();
while (thread_ctrl::state() != thread_state::aborting)
{
// Check if we have a new frame
if (!m_wake_up)
{
m_wake_up.wait(0);
m_wake_up.release(0);
if (thread_ctrl::state() == thread_state::aborting)
{
break;
}
}
m_busy.release(true);
// Update PS Move LED colors
{
std::lock_guard lock(pad::g_pad_mutex);
const auto handler = pad::get_current_handler();
auto& handlers = handler->get_handlers();
if (auto it = handlers.find(pad_handler::move); it != handlers.end())
{
for (auto& binding : it->second->bindings())
{
if (!binding.device) continue;
// last 4 out of 7 ports (6,5,4,3). index starts at 0
const s32 gem_num = std::abs(binding.device->player_id - CELL_PAD_MAX_PORT_NUM) - 1;
if (gem_num < 0 || gem_num >= CELL_GEM_MAX_NUM) continue;
const cfg_ps_move* config = ::at32(g_cfg_move.move, gem_num);
binding.device->color_override_active = true;
binding.device->color_override.r = config->r.get();
binding.device->color_override.g = config->g.get();
binding.device->color_override.b = config->b.get();
}
}
}
// Update tracker config
for (u32 gem_num = 0; gem_num < CELL_GEM_MAX_NUM; gem_num++)
{
const auto& controller = gem.controllers[gem_num];
const cfg_ps_move* config = g_cfg_move.move[gem_num];
m_tracker.set_active(gem_num, controller.enabled_tracking && controller.status == CELL_GEM_STATUS_READY);
m_tracker.set_hue(gem_num, config->hue);
m_tracker.set_hue_threshold(gem_num, config->hue_threshold);
m_tracker.set_saturation_threshold(gem_num, config->saturation_threshold);
}
m_tracker.set_min_radius(static_cast<f32>(g_cfg_move.min_radius.get() / g_cfg_move.min_radius.max));
m_tracker.set_max_radius(static_cast<f32>(g_cfg_move.max_radius.get() / g_cfg_move.max_radius.max));
// Process camera image
m_tracker.process_image();
// Update cellGem with results
{
std::lock_guard lock(mutex);
m_hues = m_tracker.hues();
m_info = m_tracker.info();
for (u32 gem_num = 0; gem_num < CELL_GEM_MAX_NUM; gem_num++)
{
const ps_move_info& info = m_info[gem_num];
auto& controller = gem.controllers[gem_num];
controller.radius_valid = info.valid;
if (info.valid)
{
// Only set new radius and distance if the radius is valid
controller.radius = info.radius;
controller.distance = info.distance;
}
}
}
// Notify that we are finished with this frame
m_done.release(1);
m_done.notify_one();
m_busy.release(false);
}
}
static constexpr auto thread_name = "GemUpdateThread"sv;
shared_mutex mutex;
private:
atomic_t<u32> m_wake_up = 0;
atomic_t<u32> m_done = 1;
atomic_t<bool> m_busy = false;
ps_move_tracker<false> m_tracker{};
CellCameraInfoEx m_camera_info{};
std::array<u32, 360> m_hues{};
std::array<ps_move_info, CELL_GEM_MAX_NUM> m_info{};
};
/**
* \brief Verifies that a Move controller id is valid
* \param gem_num Move controler ID to verify
@ -739,45 +979,53 @@ static void ds3_input_to_pad(const u32 gem_num, be_t<u16>& digital_buttons, be_t
return;
}
const auto& cfg = ::at32(g_cfg_gem.players, gem_num);
cfg->handle_input(pad, true, [&](gem_btn btn, u16 value, bool pressed)
{
if (!pressed)
return;
const auto handle_input = [&](gem_btn btn, u16 value, bool pressed)
{
if (!pressed)
return;
switch (btn)
{
case gem_btn::start:
digital_buttons |= CELL_GEM_CTRL_START;
break;
case gem_btn::select:
digital_buttons |= CELL_GEM_CTRL_SELECT;
break;
case gem_btn::square:
digital_buttons |= CELL_GEM_CTRL_SQUARE;
break;
case gem_btn::cross:
digital_buttons |= CELL_GEM_CTRL_CROSS;
break;
case gem_btn::circle:
digital_buttons |= CELL_GEM_CTRL_CIRCLE;
break;
case gem_btn::triangle:
digital_buttons |= CELL_GEM_CTRL_TRIANGLE;
break;
case gem_btn::move:
digital_buttons |= CELL_GEM_CTRL_MOVE;
break;
case gem_btn::t:
digital_buttons |= CELL_GEM_CTRL_T;
analog_t = std::max<u16>(analog_t, value);
break;
case gem_btn::x_axis:
case gem_btn::y_axis:
case gem_btn::count:
break;
}
});
switch (btn)
{
case gem_btn::start:
digital_buttons |= CELL_GEM_CTRL_START;
break;
case gem_btn::select:
digital_buttons |= CELL_GEM_CTRL_SELECT;
break;
case gem_btn::square:
digital_buttons |= CELL_GEM_CTRL_SQUARE;
break;
case gem_btn::cross:
digital_buttons |= CELL_GEM_CTRL_CROSS;
break;
case gem_btn::circle:
digital_buttons |= CELL_GEM_CTRL_CIRCLE;
break;
case gem_btn::triangle:
digital_buttons |= CELL_GEM_CTRL_TRIANGLE;
break;
case gem_btn::move:
digital_buttons |= CELL_GEM_CTRL_MOVE;
break;
case gem_btn::t:
digital_buttons |= CELL_GEM_CTRL_T;
analog_t = std::max<u16>(analog_t, value);
break;
case gem_btn::x_axis:
case gem_btn::y_axis:
case gem_btn::count:
break;
}
};
if (g_cfg.io.move == move_handler::real)
{
::at32(g_cfg_gem_real.players, gem_num)->handle_input(pad, true, handle_input);
}
else
{
::at32(g_cfg_gem_fake.players, gem_num)->handle_input(pad, true, handle_input);
}
}
constexpr u16 ds3_max_x = 255;
@ -788,7 +1036,7 @@ static inline void ds3_get_stick_values(u32 gem_num, const std::shared_ptr<Pad>&
x_pos = 0;
y_pos = 0;
const auto& cfg = ::at32(g_cfg_gem.players, gem_num);
const auto& cfg = ::at32(g_cfg_gem_fake.players, gem_num);
cfg->handle_input(pad, true, [&](gem_btn btn, u16 value, bool pressed)
{
if (!pressed)
@ -839,6 +1087,37 @@ static void ds3_pos_to_gem_state(u32 gem_num, const gem_config::gem_controller&
}
}
template <typename T>
static void ps_move_pos_to_gem_state(u32 gem_num, const gem_config::gem_controller& controller, T& gem_state)
{
if (!gem_state || !is_input_allowed())
{
return;
}
std::lock_guard lock(pad::g_pad_mutex);
const auto handler = pad::get_current_handler();
const auto& pad = ::at32(handler->GetPads(), pad_num(gem_num));
if (pad->m_pad_handler != pad_handler::move || !(pad->m_port_status & CELL_PAD_STATUS_CONNECTED))
{
return;
}
auto& tracker = g_fxo->get<named_thread<gem_tracker>>(); // Let's not lock the mutex. This not really important here
const ps_move_info& info = tracker.get_info(gem_num);
if constexpr (std::is_same_v<T, vm::ptr<CellGemState>>)
{
pos_to_gem_state(gem_num, controller, gem_state, info.x_pos, info.y_pos, info.x_max, info.y_max);
}
else if constexpr (std::is_same_v<T, vm::ptr<CellGemImageState>>)
{
pos_to_gem_image_state(gem_num, controller, gem_state, info.x_pos, info.y_pos, info.x_max, info.y_max);
}
}
/**
* \brief Maps external Move controller data to DS3 input. (This can be input from any physical pad, not just the DS3)
* Implementation detail: CellGemExtPortData's digital/analog fields map the same way as
@ -847,7 +1126,7 @@ static void ds3_pos_to_gem_state(u32 gem_num, const gem_config::gem_controller&
* \param ext External data to modify
* \return true on success, false if controller is disconnected
*/
static void ds3_input_to_ext(const u32 gem_num, const gem_config::gem_controller& controller, CellGemExtPortData& ext)
static void ds3_input_to_ext(u32 gem_num, gem_config::gem_controller& controller, CellGemExtPortData& ext)
{
ext = {};
@ -866,23 +1145,58 @@ static void ds3_input_to_ext(const u32 gem_num, const gem_config::gem_controller
return;
}
ext.status = 0; // CELL_GEM_EXT_CONNECTED | CELL_GEM_EXT_EXT0 | CELL_GEM_EXT_EXT1
ext.analog_left_x = pad->m_analog_left_x; // HACK: these pad members are actually only set in cellPad
ext.analog_left_y = pad->m_analog_left_y;
ext.analog_right_x = pad->m_analog_right_x;
ext.analog_right_y = pad->m_analog_right_y;
ext.digital1 = pad->m_digital_1;
ext.digital2 = pad->m_digital_2;
const auto& move_data = pad->move_data;
if (controller.ext_id == SHARP_SHOOTER_DEVICE_ID)
controller.ext_status = move_data.external_device_connected ? CELL_GEM_EXT_CONNECTED : 0; // TODO: | CELL_GEM_EXT_EXT0 | CELL_GEM_EXT_EXT1
controller.ext_id = move_data.external_device_connected ? move_data.external_device_id : 0;
ext.status = controller.ext_status;
for (const AnalogStick& stick : pad->m_sticks)
{
// TODO set custom[0] bits as follows:
// 1xxxxxxx: RL reload button is pressed.
// x1xxxxxx: T button trigger is pressed.
// xxxxx001: Firing mode selector is in position 1.
// xxxxx010: Firing mode selector is in position 2.
// xxxxx100: Firing mode selector is in position 3.
switch (stick.m_offset)
{
case CELL_PAD_BTN_OFFSET_ANALOG_LEFT_X: ext.analog_left_x = stick.m_value; break;
case CELL_PAD_BTN_OFFSET_ANALOG_LEFT_Y: ext.analog_left_y = stick.m_value; break;
case CELL_PAD_BTN_OFFSET_ANALOG_RIGHT_X: ext.analog_right_x = stick.m_value; break;
case CELL_PAD_BTN_OFFSET_ANALOG_RIGHT_Y: ext.analog_right_y = stick.m_value; break;
default: break;
}
}
for (const Button& button : pad->m_buttons)
{
if (!button.m_pressed)
continue;
switch (button.m_offset)
{
case CELL_PAD_BTN_OFFSET_DIGITAL1: ext.digital1 |= button.m_outKeyCode; break;
case CELL_PAD_BTN_OFFSET_DIGITAL2: ext.digital2 |= button.m_outKeyCode; break;
default: break;
}
}
if (!move_data.external_device_connected)
{
return;
}
// The sharpshooter only sets the custom bytes as follows:
// custom[0] (0x01): Firing mode selector is in position 1.
// custom[0] (0x02): Firing mode selector is in position 2.
// custom[0] (0x04): Firing mode selector is in position 3.
// custom[0] (0x40): T button trigger is pressed.
// custom[0] (0x80): RL reload button is pressed.
// The racing wheel sets the custom bytes as follows:
// custom[0] 0-255: Throttle
// custom[1] 0-255: L2
// custom[2] 0-255: R2
// custom[3] (0x01): Left paddle
// custom[3] (0x02): Right paddle
std::memcpy(ext.custom, move_data.external_device_data.data(), 5);
}
/**
@ -892,7 +1206,7 @@ static void ds3_input_to_ext(const u32 gem_num, const gem_config::gem_controller
* \param analog_t Analog value of Move's Trigger.
* \return true on success, false if mouse_no is invalid
*/
static bool mouse_input_to_pad(const u32 mouse_no, be_t<u16>& digital_buttons, be_t<u16>& analog_t)
static bool mouse_input_to_pad(u32 mouse_no, be_t<u16>& digital_buttons, be_t<u16>& analog_t)
{
digital_buttons = 0;
analog_t = 0;
@ -954,7 +1268,7 @@ static bool mouse_input_to_pad(const u32 mouse_no, be_t<u16>& digital_buttons, b
}
template <typename T>
static void mouse_pos_to_gem_state(const u32 mouse_no, const gem_config::gem_controller& controller, T& gem_state)
static void mouse_pos_to_gem_state(u32 mouse_no, const gem_config::gem_controller& controller, T& gem_state)
{
if (!gem_state || !is_input_allowed())
{
@ -986,7 +1300,7 @@ static void mouse_pos_to_gem_state(const u32 mouse_no, const gem_config::gem_con
}
#ifdef HAVE_LIBEVDEV
static bool gun_input_to_pad(const u32 gem_no, be_t<u16>& digital_buttons, be_t<u16>& analog_t)
static bool gun_input_to_pad(u32 gem_no, be_t<u16>& digital_buttons, be_t<u16>& analog_t)
{
digital_buttons = 0;
analog_t = 0;
@ -1027,7 +1341,7 @@ static bool gun_input_to_pad(const u32 gem_no, be_t<u16>& digital_buttons, be_t<
}
template <typename T>
static void gun_pos_to_gem_state(const u32 gem_no, const gem_config::gem_controller& controller, T& gem_state)
static void gun_pos_to_gem_state(u32 gem_no, const gem_config::gem_controller& controller, T& gem_state)
{
if (!gem_state || !is_input_allowed())
return;
@ -1271,6 +1585,11 @@ error_code cellGemEnd(ppu_thread& ppu)
return CELL_OK;
}
auto& tracker = g_fxo->get<named_thread<gem_tracker>>();
tracker.wait_for_result();
gem.updating = false;
return CELL_GEM_ERROR_UNINITIALIZED;
}
@ -1368,9 +1687,12 @@ error_code cellGemGetAllTrackableHues(vm::ptr<u8> hues)
return CELL_GEM_ERROR_INVALID_PARAMETER;
}
for (u32 i = 0; i < 360; i++)
auto& tracker = g_fxo->get<named_thread<gem_tracker>>();
std::lock_guard lock(tracker.mutex);
for (u32 hue = 0; hue < 360; hue++)
{
hues[i] = true;
hues[hue] = tracker.hue_is_trackable(hue);
}
return CELL_OK;
@ -1445,7 +1767,10 @@ error_code cellGemGetHuePixels(vm::cptr<void> camera_frame, u32 hue, vm::ptr<u8>
std::memset(pixels.get_ptr(), 0, 640 * 480 * sizeof(u8));
// TODO
if (g_cfg.io.move == move_handler::real)
{
// TODO: get pixels from tracker
}
return CELL_OK;
}
@ -1478,10 +1803,13 @@ error_code cellGemGetImageState(u32 gem_num, vm::ptr<CellGemImageState> gem_imag
gem_image_state->r = controller.radius; // Radius in camera pixels
gem_image_state->distance = controller.distance; // 1.5 meters away from camera
gem_image_state->visible = gem.is_controller_ready(gem_num);
gem_image_state->r_valid = true;
gem_image_state->r_valid = controller.radius_valid;
switch (g_cfg.io.move)
{
case move_handler::real:
ps_move_pos_to_gem_state(gem_num, controller, gem_image_state);
break;
case move_handler::fake:
ds3_pos_to_gem_state(gem_num, controller, gem_image_state);
break;
@ -1537,6 +1865,24 @@ error_code cellGemGetInertialState(u32 gem_num, u32 state_flag, u64 timestamp, v
switch (g_cfg.io.move)
{
case move_handler::real:
{
// Get temperature
{
std::lock_guard lock(pad::g_pad_mutex);
const auto handler = pad::get_current_handler();
const auto& pad = ::at32(handler->GetPads(), pad_num(gem_num));
if (pad && pad->m_pad_handler == pad_handler::move && (pad->m_port_status & CELL_PAD_STATUS_CONNECTED))
{
inertial_state->temperature = pad->move_data.temperature;
}
}
ds3_input_to_pad(gem_num, inertial_state->pad.digitalbuttons, inertial_state->pad.analog_T);
break;
}
case move_handler::fake:
ds3_input_to_pad(gem_num, inertial_state->pad.digitalbuttons, inertial_state->pad.analog_T);
break;
@ -1575,10 +1921,9 @@ error_code cellGemGetInfo(vm::ptr<CellGemInfo> info)
return CELL_GEM_ERROR_INVALID_PARAMETER;
}
// TODO: Support connecting PlayStation Move controllers
switch (g_cfg.io.move)
{
case move_handler::real:
case move_handler::fake:
{
gem.connected_controllers = 0;
@ -1590,8 +1935,9 @@ error_code cellGemGetInfo(vm::ptr<CellGemInfo> info)
{
const auto& pad = ::at32(handler->GetPads(), pad_num(i));
const bool connected = (pad && (pad->m_port_status & CELL_PAD_STATUS_CONNECTED) && i < gem.attribute.max_connect);
const bool is_real_move = g_cfg.io.move != move_handler::real || pad->m_pad_handler == pad_handler::move;
if (connected)
if (connected && is_real_move)
{
gem.connected_controllers++;
gem.controllers[i].status = CELL_GEM_STATUS_READY;
@ -1758,7 +2104,7 @@ error_code cellGemGetState(u32 gem_num, u32 flag, u64 time_parameter, vm::ptr<Ce
return CELL_GEM_TIME_OUT_OF_RANGE;
}
const auto& controller = gem.controllers[gem_num];
auto& controller = gem.controllers[gem_num];
*gem_state = {};
@ -1766,18 +2112,33 @@ error_code cellGemGetState(u32 gem_num, u32 flag, u64 time_parameter, vm::ptr<Ce
{
ds3_input_to_ext(gem_num, controller, gem_state->ext);
u32 tracking_flags = CELL_GEM_TRACKING_FLAG_VISIBLE;
if (controller.enabled_tracking)
tracking_flags |= CELL_GEM_TRACKING_FLAG_POSITION_TRACKED;
{
gem_state->tracking_flags |= CELL_GEM_TRACKING_FLAG_POSITION_TRACKED;
gem_state->tracking_flags |= CELL_GEM_TRACKING_FLAG_VISIBLE;
}
gem_state->tracking_flags = tracking_flags;
gem_state->timestamp = (get_guest_system_time() - gem.start_timestamp_us);
gem_state->camera_pitch_angle = 0.f;
gem_state->quat[3] = 1.f;
switch (g_cfg.io.move)
{
case move_handler::real:
{
auto& tracker = g_fxo->get<named_thread<gem_tracker>>(); // Let's not lock the mutex. This not really important here
const ps_move_info& info = tracker.get_info(gem_num);
ds3_input_to_pad(gem_num, gem_state->pad.digitalbuttons, gem_state->pad.analog_T);
ps_move_pos_to_gem_state(gem_num, controller, gem_state);
if (info.valid)
gem_state->tracking_flags |= CELL_GEM_TRACKING_FLAG_VISIBLE;
else
gem_state->tracking_flags &= ~CELL_GEM_TRACKING_FLAG_VISIBLE;
break;
}
case move_handler::fake:
ds3_input_to_pad(gem_num, gem_state->pad.digitalbuttons, gem_state->pad.analog_T);
ds3_pos_to_gem_state(gem_num, controller, gem_state);
@ -1966,7 +2327,7 @@ error_code cellGemInit(ppu_thread& ppu, vm::cptr<CellGemAttribute> attribute)
gem.memory_ptr = 0;
}
gem.update_started = false;
gem.updating = false;
gem.camera_frame = 0;
gem.status_flags = 0;
gem.attribute = *attribute;
@ -2027,7 +2388,10 @@ s32 cellGemIsTrackableHue(u32 hue)
return CELL_GEM_ERROR_INVALID_PARAMETER;
}
return 1; // potentially true if less than 20 pixels have the hue
auto& tracker = g_fxo->get<named_thread<gem_tracker>>();
std::lock_guard lock(tracker.mutex);
return tracker.hue_is_trackable(hue);
}
error_code cellGemPrepareCamera(s32 max_exposure, f32 image_quality)
@ -2105,7 +2469,7 @@ error_code cellGemPrepareVideoConvert(vm::cptr<CellGemVideoConvertAttribute> vc_
error_code cellGemReadExternalPortDeviceInfo(u32 gem_num, vm::ptr<u32> ext_id, vm::ptr<u8[CELL_GEM_EXTERNAL_PORT_DEVICE_INFO_SIZE]> ext_info)
{
cellGem.todo("cellGemReadExternalPortDeviceInfo(gem_num=%d, ext_id=*0x%x, ext_info=%s)", gem_num, ext_id, ext_info);
cellGem.warning("cellGemReadExternalPortDeviceInfo(gem_num=%d, ext_id=*0x%x, ext_info=%s)", gem_num, ext_id, ext_info);
auto& gem = g_fxo->get<gem_config>();
@ -2124,12 +2488,65 @@ error_code cellGemReadExternalPortDeviceInfo(u32 gem_num, vm::ptr<u32> ext_id, v
return CELL_GEM_NOT_CONNECTED;
}
if (!(gem.controllers[gem_num].ext_status & CELL_GEM_EXT_CONNECTED))
const u64 start = get_system_time();
auto& controller = gem.controllers[gem_num];
if (g_cfg.io.move != move_handler::null)
{
// Get external device status
CellGemExtPortData ext_port_data{};
ds3_input_to_ext(gem_num, controller, ext_port_data);
}
if (!(controller.ext_status & CELL_GEM_EXT_CONNECTED))
{
return CELL_GEM_NO_EXTERNAL_PORT_DEVICE;
}
*ext_id = gem.controllers[gem_num].ext_id;
*ext_id = controller.ext_id;
if (ext_info && g_cfg.io.move == move_handler::real)
{
bool read_requested = false;
while (true)
{
{
std::lock_guard lock(pad::g_pad_mutex);
const auto handler = pad::get_current_handler();
const auto& pad = ::at32(handler->GetPads(), pad_num(gem_num));
if (pad->m_pad_handler != pad_handler::move || !(pad->m_port_status & CELL_PAD_STATUS_CONNECTED))
{
return CELL_GEM_NOT_CONNECTED;
}
if (!read_requested)
{
pad->move_data.external_device_read_requested = true;
read_requested = true;
}
if (!pad->move_data.external_device_read_requested)
{
*ext_id = controller.ext_id = pad->move_data.external_device_id;
std::memcpy(pad->move_data.external_device_read.data(), ext_info.get_ptr(), CELL_GEM_EXTERNAL_PORT_OUTPUT_SIZE);
break;
}
}
// We wait for 300ms at most
if (const u64 elapsed_us = get_system_time() - start; elapsed_us > 300'000)
{
cellGem.warning("cellGemReadExternalPortDeviceInfo(gem_num=%d): timeout", gem_num);
break;
}
// TODO: sleep ?
}
}
return CELL_OK;
}
@ -2229,6 +2646,8 @@ error_code cellGemTrackHues(vm::cptr<u32> req_hues, vm::ptr<u32> res_hues)
gem.controllers[i].enabled_tracking = true;
gem.controllers[i].enabled_LED = true;
// TODO: set hue based on tracker data
switch (i)
{
default:
@ -2272,6 +2691,8 @@ error_code cellGemTrackHues(vm::cptr<u32> req_hues, vm::ptr<u32> res_hues)
gem.controllers[i].enabled_LED = true;
gem.controllers[i].hue = req_hues[i];
// TODO: set hue of tracker
if (res_hues)
{
res_hues[i] = gem.controllers[i].hue;
@ -2297,11 +2718,16 @@ error_code cellGemUpdateFinish()
std::scoped_lock lock(gem.mtx);
if (!gem.update_started.exchange(false))
if (!gem.updating)
{
return CELL_GEM_ERROR_UPDATE_NOT_STARTED;
}
auto& tracker = g_fxo->get<named_thread<gem_tracker>>();
tracker.wait_for_result();
gem.updating = false;
if (!gem.camera_frame)
{
return not_an_error(CELL_GEM_NO_VIDEO);
@ -2321,10 +2747,17 @@ error_code cellGemUpdateStart(vm::cptr<void> camera_frame, u64 timestamp)
return CELL_GEM_ERROR_UNINITIALIZED;
}
auto& tracker = g_fxo->get<named_thread<gem_tracker>>();
if (tracker.is_busy())
{
return CELL_GEM_ERROR_UPDATE_NOT_FINISHED;
}
std::scoped_lock lock(gem.mtx);
// Update is starting even when camera_frame is null
if (gem.update_started.exchange(true))
if (gem.updating.exchange(true))
{
return CELL_GEM_ERROR_UPDATE_NOT_FINISHED;
}
@ -2335,17 +2768,20 @@ error_code cellGemUpdateStart(vm::cptr<void> camera_frame, u64 timestamp)
}
gem.camera_frame = camera_frame.addr();
if (!camera_frame)
if (!tracker.set_image(gem.camera_frame))
{
return not_an_error(CELL_GEM_NO_VIDEO);
}
tracker.wake_up();
return CELL_OK;
}
error_code cellGemWriteExternalPort(u32 gem_num, vm::ptr<u8[CELL_GEM_EXTERNAL_PORT_OUTPUT_SIZE]> data)
{
cellGem.todo("cellGemWriteExternalPort(gem_num=%d, data=%s)", gem_num, data);
cellGem.warning("cellGemWriteExternalPort(gem_num=%d, data=%s)", gem_num, data);
auto& gem = g_fxo->get<gem_config>();
@ -2364,9 +2800,25 @@ error_code cellGemWriteExternalPort(u32 gem_num, vm::ptr<u8[CELL_GEM_EXTERNAL_PO
return CELL_GEM_NOT_CONNECTED;
}
if (false) // TODO: check if this is still writing to the external port
if (data && g_cfg.io.move == move_handler::real)
{
return CELL_GEM_ERROR_WRITE_NOT_FINISHED;
std::lock_guard lock(pad::g_pad_mutex);
const auto handler = pad::get_current_handler();
const auto& pad = ::at32(handler->GetPads(), pad_num(gem_num));
if (pad->m_pad_handler != pad_handler::move || !(pad->m_port_status & CELL_PAD_STATUS_CONNECTED))
{
return CELL_GEM_NOT_CONNECTED;
}
if (pad->move_data.external_device_write_requested)
{
return CELL_GEM_ERROR_WRITE_NOT_FINISHED;
}
std::memcpy(pad->move_data.external_device_write.data(), data.get_ptr(), CELL_GEM_EXTERNAL_PORT_OUTPUT_SIZE);
pad->move_data.external_device_write_requested = true;
}
return CELL_OK;

View File

@ -161,7 +161,8 @@ enum CellGemVideoConvertFormatEnum : s32
// External device IDs (types)
enum
{
SHARP_SHOOTER_DEVICE_ID = 0x8081
SHARP_SHOOTER_DEVICE_ID = 0x8081,
RACING_WHEEL_DEVICE_ID = 0x8101
};
struct CellGemAttribute

View File

@ -313,9 +313,10 @@ void clear_pad_buffer(const std::shared_ptr<Pad>& pad)
pad->m_press_triangle = pad->m_press_circle = pad->m_press_cross = pad->m_press_square = 0;
pad->m_press_L1 = pad->m_press_L2 = pad->m_press_R1 = pad->m_press_R2 = 0;
// ~399 on sensor y is a level non moving controller
pad->m_sensor_y = 399;
pad->m_sensor_x = pad->m_sensor_z = pad->m_sensor_g = 512;
pad->m_sensor_x = DEFAULT_MOTION_X;
pad->m_sensor_y = DEFAULT_MOTION_Y;
pad->m_sensor_z = DEFAULT_MOTION_Z;
pad->m_sensor_g = DEFAULT_MOTION_G;
}
error_code cellPadClearBuf(u32 port_no)

View File

@ -247,6 +247,21 @@ PadHandlerBase::connection PadHandlerBase::get_next_button_press(const std::stri
return status;
}
if (m_type == pad_handler::move)
{
// Keep the pad cached to reduce expensive one time requests
if (!m_pad_for_pad_settings || m_pad_for_pad_settings->m_pad_handler != m_type)
{
const pad_preview_values preview_values{};
m_pad_for_pad_settings = std::make_shared<Pad>(m_type, 0, 0, 0, 0);
m_pad_for_pad_settings->m_sensors.resize(preview_values.size(), AnalogSensor(0, 0, 0, 0, 0));
}
// Get extended device ID
pad_ensemble binding{m_pad_for_pad_settings, device, nullptr};
get_extended_info(binding);
}
// Get the current button values
auto data = get_button_values(device);
@ -348,14 +363,20 @@ void PadHandlerBase::get_motion_sensors(const std::string& pad_id, const motion_
return;
}
// Keep the pad cached to reduce expensive one time requests
if (!m_pad_for_pad_settings || m_pad_for_pad_settings->m_pad_handler != m_type)
{
m_pad_for_pad_settings = std::make_shared<Pad>(m_type, 0, 0, 0, 0);
m_pad_for_pad_settings->m_sensors.resize(preview_values.size(), AnalogSensor(0, 0, 0, 0, 0));
}
// Get the current motion values
std::shared_ptr<Pad> pad = std::make_shared<Pad>(m_type, 0, 0, 0, 0);
pad_ensemble binding{pad, device, nullptr};
pad_ensemble binding{m_pad_for_pad_settings, device, nullptr};
get_extended_info(binding);
for (usz i = 0; i < preview_values.size(); i++)
{
preview_values[i] = pad->m_sensors[i].m_value;
preview_values[i] = m_pad_for_pad_settings->m_sensors[i].m_value;
}
callback(pad_id, std::move(preview_values));

View File

@ -29,6 +29,15 @@ public:
std::set<u64> trigger_code_right{};
std::array<std::set<u64>, 4> axis_code_left{};
std::array<std::set<u64>, 4> axis_code_right{};
struct color
{
u8 r{};
u8 g{};
u8 b{};
};
color color_override{};
bool color_override_active{};
};
struct pad_ensemble
@ -151,6 +160,8 @@ protected:
std::unordered_map<u32, u16> min_button_values;
std::set<u32> blacklist;
std::shared_ptr<Pad> m_pad_for_pad_settings;
static std::set<u32> narrow_set(const std::set<u64>& src);
// Search an unordered map for a string value and return found keycode

View File

@ -32,6 +32,13 @@ public:
camera_handler_state get_state() const { return m_state.load(); };
bool mirrored() const { return m_mirrored; };
s32 format() const { return m_format; };
u32 bytesize() const { return m_bytesize; };
u32 width() const { return m_width; };
u32 height() const { return m_height; };
u32 frame_rate() const { return m_frame_rate; };
protected:
std::mutex m_mutex;
atomic_t<camera_handler_state> m_state = camera_handler_state::closed;

View File

@ -20,9 +20,9 @@ enum class gem_btn
count
};
struct cfg_gem final : public emulated_pad_config<gem_btn>
struct cfg_fake_gem final : public emulated_pad_config<gem_btn>
{
cfg_gem(node* owner, const std::string& name) : emulated_pad_config(owner, name) {}
cfg_fake_gem(node* owner, const std::string& name) : emulated_pad_config(owner, name) {}
cfg_pad_btn<gem_btn> start{ this, "Start", gem_btn::start, pad_button::start };
cfg_pad_btn<gem_btn> select{ this, "Select", gem_btn::select, pad_button::select };
@ -36,9 +36,29 @@ struct cfg_gem final : public emulated_pad_config<gem_btn>
cfg_pad_btn<gem_btn> y_axis{ this, "Y-Axis", gem_btn::y_axis, pad_button::ls_y };
};
struct cfg_gems final : public emulated_pads_config<cfg_gem, 4>
struct cfg_fake_gems final : public emulated_pads_config<cfg_fake_gem, 4>
{
cfg_gems() : emulated_pads_config<cfg_gem, 4>("gem") {};
cfg_fake_gems() : emulated_pads_config<cfg_fake_gem, 4>("gem") {};
};
extern cfg_gems g_cfg_gem;
struct cfg_gem final : public emulated_pad_config<gem_btn>
{
cfg_gem(node* owner, const std::string& name) : emulated_pad_config(owner, name) {}
cfg_pad_btn<gem_btn> start{ this, "Start", gem_btn::start, pad_button::start };
cfg_pad_btn<gem_btn> select{ this, "Select", gem_btn::select, pad_button::select };
cfg_pad_btn<gem_btn> triangle{ this, "Triangle", gem_btn::triangle, pad_button::triangle };
cfg_pad_btn<gem_btn> circle{ this, "Circle", gem_btn::circle, pad_button::circle };
cfg_pad_btn<gem_btn> cross{ this, "Cross", gem_btn::cross, pad_button::cross };
cfg_pad_btn<gem_btn> square{ this, "Square", gem_btn::square, pad_button::square };
cfg_pad_btn<gem_btn> move{ this, "Move", gem_btn::move, pad_button::R1 };
cfg_pad_btn<gem_btn> t{ this, "T", gem_btn::t, pad_button::R2 };
};
struct cfg_gems final : public emulated_pads_config<cfg_gem, 4>
{
cfg_gems() : emulated_pads_config<cfg_gem, 4>("gem_real") {};
};
extern cfg_gems g_cfg_gem_real;
extern cfg_fake_gems g_cfg_gem_fake;

View File

@ -14,6 +14,7 @@ void fmt_class_string<pad_handler>::format(std::string& out, u64 arg)
case pad_handler::ds4: return "DualShock 4";
case pad_handler::dualsense: return "DualSense";
case pad_handler::skateboard: return "Skateboard";
case pad_handler::move: return "PS Move";
#ifdef _WIN32
case pad_handler::xinput: return "XInput";
case pad_handler::mm: return "MMJoystick";

View File

@ -10,6 +10,7 @@ enum class pad_handler
ds4,
dualsense,
skateboard,
move,
#ifdef _WIN32
xinput,
mm,

View File

@ -337,8 +337,9 @@ struct CellPadData
be_t<u16> button[CELL_PAD_MAX_CODES];
};
static constexpr u16 MOTION_ONE_G = 113;
static constexpr u16 DEFAULT_MOTION_X = 512;
static constexpr u16 DEFAULT_MOTION_Y = 399;
static constexpr u16 DEFAULT_MOTION_Y = 399; // 512 - 113 (113 is 1G gravity)
static constexpr u16 DEFAULT_MOTION_Z = 512;
static constexpr u16 DEFAULT_MOTION_G = 512;
@ -455,6 +456,25 @@ struct VibrateMotor
{}
};
struct ps_move_data
{
bool external_device_connected = false;
u32 external_device_id = 0;
std::array<u8, 5> external_device_data{};
std::array<u8, 38> external_device_read{}; // CELL_GEM_EXTERNAL_PORT_DEVICE_INFO_SIZE
std::array<u8, 40> external_device_write{}; // CELL_GEM_EXTERNAL_PORT_OUTPUT_SIZE
bool external_device_read_requested = false;
bool external_device_write_requested = false;
s16 accelerometer_x = 0;
s16 accelerometer_y = 0;
s16 accelerometer_z = 0;
s16 gyro_x = 0;
s16 gyro_y = 0;
s16 gyro_z = 0;
s16 temperature = 0;
};
struct Pad
{
const pad_handler m_pad_handler;
@ -519,7 +539,6 @@ struct Pad
u16 m_press_R2{0};
// Except for these...0-1023
// ~399 on sensor y is a level non moving controller
u16 m_sensor_x{DEFAULT_MOTION_X};
u16 m_sensor_y{DEFAULT_MOTION_Y};
u16 m_sensor_z{DEFAULT_MOTION_Z};
@ -530,6 +549,8 @@ struct Pad
bool is_fake_pad = false;
ps_move_data move_data{};
explicit Pad(pad_handler handler, u32 player_id, u32 port_status, u32 device_capability, u32 device_type)
: m_pad_handler(handler)
, m_player_id(player_id)

View File

@ -437,6 +437,7 @@ void fmt_class_string<move_handler>::format(std::string& out, u64 arg)
switch (value)
{
case move_handler::null: return "Null";
case move_handler::real: return "Real";
case move_handler::fake: return "Fake";
case move_handler::mouse: return "Mouse";
case move_handler::raw_mouse: return "Raw Mouse";

View File

@ -138,6 +138,7 @@ enum class fake_camera_type
enum class move_handler
{
null,
real,
fake,
mouse,
raw_mouse,

View File

@ -198,6 +198,7 @@ std::shared_ptr<PadHandlerBase> gui_pad_thread::GetHandler(pad_handler type)
{
case pad_handler::null:
case pad_handler::keyboard:
case pad_handler::move:
// Makes no sense to use this if we are in the GUI anyway
return nullptr;
case pad_handler::ds3:

View File

@ -3,6 +3,7 @@
#include "ds4_pad_handler.h"
#include "dualsense_pad_handler.h"
#include "skateboard_pad_handler.h"
#include "ps_move_handler.h"
#include "util/logs.hpp"
#include "Utilities/Timer.h"
#include "Emu/System.h"
@ -77,6 +78,13 @@ void HidDevice::close()
hid_close(hidDevice);
hidDevice = nullptr;
}
#ifdef _WIN32
if (bt_device)
{
hid_close(bt_device);
bt_device = nullptr;
}
#endif
}
template <class Device>
@ -185,9 +193,19 @@ void hid_pad_handler<Device>::enumerate_devices()
hid_log.error("Skipping enumeration of device with empty path.");
continue;
}
device_paths.insert(dev_info->path);
serials[dev_info->path] = dev_info->serial_number ? std::wstring(dev_info->serial_number) : std::wstring();
dev_info = dev_info->next;
const std::string path = dev_info->path;
device_paths.insert(path);
#ifdef _WIN32
// Only add serials for col01 ps move device
if (m_type == pad_handler::move && path.find("&Col01#") != umax)
#endif
{
serials[path] = dev_info->serial_number ? std::wstring(dev_info->serial_number) : std::wstring();
}
dev_info = dev_info->next;
}
hid_free_enumeration(head);
}
@ -196,6 +214,29 @@ void hid_pad_handler<Device>::enumerate_devices()
std::lock_guard lock(m_enumeration_mutex);
m_new_enumerated_devices = device_paths;
m_enumerated_serials = std::move(serials);
#ifdef _WIN32
if (m_type == pad_handler::move)
{
// Windows enumerates 3 ps move devices: Col01, Col02, and Col03.
// We use Col01 for data and Col02 for bluetooth.
// Filter paths. We only want the Col01 paths.
std::set<std::string> col01_paths;
for (const std::string& path : m_new_enumerated_devices)
{
hid_log.trace("Found ps move device: %s", path);
if (path.find("&Col01#") != umax)
{
col01_paths.insert(path);
}
}
m_new_enumerated_devices = std::move(col01_paths);
}
#endif
}
template <class Device>
@ -235,8 +276,15 @@ void hid_pad_handler<Device>::update_devices()
if (std::any_of(m_controllers.cbegin(), m_controllers.cend(), [&path](const auto& c) { return c.second && c.second->path == path; }))
continue;
hid_device* dev = hid_open_path(path.c_str());
if (dev)
#ifdef _WIN32
if (m_type == pad_handler::move)
{
check_add_device(nullptr, path, m_enumerated_serials[path]);
continue;
}
#endif
if (hid_device* dev = hid_open_path(path.c_str()))
{
if (const hid_device_info* info = hid_get_device_info(dev))
{
@ -314,3 +362,4 @@ template class hid_pad_handler<ds3_device>;
template class hid_pad_handler<DS4Device>;
template class hid_pad_handler<DualSenseDevice>;
template class hid_pad_handler<skateboard_device>;
template class hid_pad_handler<ps_move_device>;

View File

@ -33,6 +33,9 @@ public:
void close();
hid_device* hidDevice{nullptr};
#ifdef _WIN32
hid_device* bt_device{nullptr}; // Used in ps move handler
#endif
std::string path;
bool enable_player_leds{false};
u8 led_delay_on{0};

View File

@ -5,6 +5,7 @@
#include "ds4_pad_handler.h"
#include "dualsense_pad_handler.h"
#include "skateboard_pad_handler.h"
#include "ps_move_handler.h"
#ifdef _WIN32
#include "xinput_pad_handler.h"
#include "mm_joystick_handler.h"
@ -198,7 +199,7 @@ void pad_thread::Init()
}
}
pad->is_fake_pad = (g_cfg.io.move == move_handler::fake && i >= (static_cast<u32>(CELL_PAD_MAX_PORT_NUM) - static_cast<u32>(CELL_GEM_MAX_NUM)))
pad->is_fake_pad = ((g_cfg.io.move == move_handler::real || g_cfg.io.move == move_handler::fake) && i >= (static_cast<u32>(CELL_PAD_MAX_PORT_NUM) - static_cast<u32>(CELL_GEM_MAX_NUM)))
|| (pad->m_class_type >= CELL_PAD_FAKE_TYPE_FIRST && pad->m_class_type < CELL_PAD_FAKE_TYPE_LAST);
connect_usb_controller(i, input::get_product_by_vid_pid(pad->m_vendor_id, pad->m_product_id));
}
@ -622,6 +623,8 @@ std::shared_ptr<PadHandlerBase> pad_thread::GetHandler(pad_handler type)
return std::make_shared<dualsense_pad_handler>();
case pad_handler::skateboard:
return std::make_shared<skateboard_pad_handler>();
case pad_handler::move:
return std::make_shared<ps_move_handler>();
#ifdef _WIN32
case pad_handler::xinput:
return std::make_shared<xinput_pad_handler>();

View File

@ -0,0 +1,40 @@
#include "stdafx.h"
#include "ps_move_config.h"
LOG_CHANNEL(ps_move);
cfg_ps_moves g_cfg_move;
cfg_ps_moves::cfg_ps_moves()
: cfg::node()
#ifdef _WIN32
, path(fs::get_config_dir() + "config/ps_move.yml")
#else
, path(fs::get_config_dir() + "ps_move.yml")
#endif
{
}
bool cfg_ps_moves::load()
{
ps_move.notice("Loading PS Move config from '%s'", path);
if (fs::file cfg_file{ path, fs::read })
{
return from_string(cfg_file.to_string());
}
ps_move.notice("PS Move config missing. Using default settings. Path: %s", path);
from_default();
return false;
}
void cfg_ps_moves::save() const
{
ps_move.notice("Saving PS Move config to '%s'", path);
if (!cfg::node::save(path))
{
ps_move.error("Failed to save PS Move config to '%s' (error=%s)", path, fs::g_tls_error);
}
}

View File

@ -0,0 +1,40 @@
#pragma once
#include "Utilities/Config.h"
#include <array>
struct cfg_ps_move final : cfg::node
{
cfg_ps_move() : cfg::node() {}
cfg_ps_move(cfg::node* owner, std::string name) : cfg::node(owner, name) {}
cfg::uint<0, 255> r{ this, "Color R", 0, true };
cfg::uint<0, 255> g{ this, "Color G", 0, true };
cfg::uint<0, 255> b{ this, "Color B", 0, true };
cfg::uint<0, 359> hue{ this, "Hue", 0, true };
cfg::uint<0, 180> hue_threshold{ this, "Hue Threshold", 10, true };
cfg::uint<0, 255> saturation_threshold{ this, "Saturation Threshold", 10, true };
};
struct cfg_ps_moves final : cfg::node
{
cfg_ps_moves();
cfg_ps_move move1{ this, "PS Move 1" };
cfg_ps_move move2{ this, "PS Move 2" };
cfg_ps_move move3{ this, "PS Move 3" };
cfg_ps_move move4{ this, "PS Move 4" };
cfg::_float<0, 100> min_radius{ this, "Minimum Radius", 1.0f, true }; // Percentage of image width
cfg::_float<0, 100> max_radius{ this, "Maximum Radius", 10.0f, true }; // Percentage of image width
std::array<cfg_ps_move*, 4> move{ &move1, &move2, &move3, &move4 };
const std::string path;
bool load();
void save() const;
};
extern cfg_ps_moves g_cfg_move;

View File

@ -0,0 +1,797 @@
#include "stdafx.h"
#include "ps_move_handler.h"
#include "Emu/Io/pad_config.h"
#include "Emu/System.h"
#include "Emu/system_config.h"
#include "Emu/Cell/Modules/cellGem.h"
#include "Input/ps_move_config.h"
LOG_CHANNEL(move_log, "Move");
using namespace reports;
namespace
{
constexpr id_pair MOVE_ID_ZCM1 = {0x054C, 0x03D5};
constexpr id_pair MOVE_ID_ZCM2 = {0x054C, 0x0c5e};
enum button_flags : u16
{
select = 0x01,
start = 0x08,
triangle = 0x10,
circle = 0x20,
cross = 0x40,
square = 0x80,
ps = 0x0001,
move = 0x4008,
t = 0x8010,
ext_dev = 0x1000,
// Sharpshooter
ss_firing_mode_1 = 0x01,
ss_firing_mode_2 = 0x02,
ss_firing_mode_3 = 0x04,
ss_trigger = 0x40,
ss_reload = 0x80,
// Racing Wheel
rw_d_pad_up = 0x10,
rw_d_pad_right = 0x20,
rw_d_pad_down = 0x40,
rw_d_pad_left = 0x80,
rw_l1 = 0x04,
rw_r1 = 0x08,
rw_paddle_l = 0x01,
rw_paddle_r = 0x02,
};
enum battery_status : u8
{
charge_empty = 0x00,
charge_1 = 0x01,
charge_2 = 0x02,
charge_3 = 0x03,
charge_4 = 0x04,
charge_full = 0x05,
usb_charging = 0xEE,
usb_charged = 0xEF,
};
enum
{
zero_shift = 0x8000,
};
}
const ps_move_input_report_common& ps_move_device::input_report_common() const
{
switch (model)
{
default:
case ps_move_model::ZCM1:
{
return input_report_ZCM1.common;
}
case ps_move_model::ZCM2:
{
return input_report_ZCM2.common;
}
}
}
ps_move_handler::ps_move_handler()
: hid_pad_handler<ps_move_device>(pad_handler::move, { MOVE_ID_ZCM1, MOVE_ID_ZCM2 })
{
// Unique names for the config files and our pad settings dialog
button_list =
{
{ ps_move_key_codes::none, "" },
{ ps_move_key_codes::cross, "Cross" },
{ ps_move_key_codes::square, "Square" },
{ ps_move_key_codes::circle, "Circle" },
{ ps_move_key_codes::triangle, "Triangle" },
{ ps_move_key_codes::start, "Start" },
{ ps_move_key_codes::select, "Select" },
{ ps_move_key_codes::ps, "PS" },
{ ps_move_key_codes::move, "Move" },
{ ps_move_key_codes::t, "T" },
{ ps_move_key_codes::firing_mode_1, "Firing Mode 1" },
{ ps_move_key_codes::firing_mode_2, "Firing Mode 2" },
{ ps_move_key_codes::firing_mode_3, "Firing Mode 3" },
{ ps_move_key_codes::reload, "Reload" },
{ ps_move_key_codes::dpad_up, "D-Pad Up" },
{ ps_move_key_codes::dpad_down, "D-Pad Down" },
{ ps_move_key_codes::dpad_left, "D-Pad Left" },
{ ps_move_key_codes::dpad_right, "D-Pad Right" },
{ ps_move_key_codes::L1, "L1" },
{ ps_move_key_codes::R1, "R1" },
{ ps_move_key_codes::L2, "L2" },
{ ps_move_key_codes::R2, "R2" },
{ ps_move_key_codes::throttle, "Throttle" },
{ ps_move_key_codes::paddle_left, "Paddle Left" },
{ ps_move_key_codes::paddle_right, "Paddle Right" },
};
init_configs();
// Define border values
thumb_max = 255;
trigger_min = 0;
trigger_max = 255;
// Set capabilities
b_has_config = true;
b_has_rumble = true;
b_has_motion = true;
b_has_deadzones = true;
b_has_led = true;
b_has_rgb = true;
b_has_player_led = false;
b_has_battery = true;
b_has_battery_led = false;
b_has_pressure_intensity_button = false;
m_name_string = "PS Move #";
m_max_devices = 4; // CELL_GEM_MAX_NUM
m_trigger_threshold = trigger_max / 2;
m_thumb_threshold = thumb_max / 2;
}
ps_move_handler::~ps_move_handler()
{
}
void ps_move_handler::init_config(cfg_pad* cfg)
{
if (!cfg) return;
// Set default button mapping
cfg->ls_left.def = ::at32(button_list, ps_move_key_codes::none);
cfg->ls_down.def = ::at32(button_list, ps_move_key_codes::none);
cfg->ls_right.def = ::at32(button_list, ps_move_key_codes::none);
cfg->ls_up.def = ::at32(button_list, ps_move_key_codes::none);
cfg->rs_left.def = ::at32(button_list, ps_move_key_codes::none);
cfg->rs_down.def = ::at32(button_list, ps_move_key_codes::none);
cfg->rs_right.def = ::at32(button_list, ps_move_key_codes::none);
cfg->rs_up.def = ::at32(button_list, ps_move_key_codes::none);
cfg->start.def = ::at32(button_list, ps_move_key_codes::start);
cfg->select.def = ::at32(button_list, ps_move_key_codes::select);
cfg->ps.def = ::at32(button_list, ps_move_key_codes::ps);
cfg->square.def = ::at32(button_list, ps_move_key_codes::square);
cfg->cross.def = ::at32(button_list, ps_move_key_codes::cross);
cfg->circle.def = ::at32(button_list, ps_move_key_codes::circle);
cfg->triangle.def = ::at32(button_list, ps_move_key_codes::triangle);
cfg->left.def = ::at32(button_list, ps_move_key_codes::none);
cfg->down.def = ::at32(button_list, ps_move_key_codes::none);
cfg->right.def = ::at32(button_list, ps_move_key_codes::none);
cfg->up.def = ::at32(button_list, ps_move_key_codes::none);
cfg->r1.def = ::at32(button_list, ps_move_key_codes::move);
cfg->r2.def = ::at32(button_list, ps_move_key_codes::t);
cfg->r3.def = ::at32(button_list, ps_move_key_codes::none);
cfg->l1.def = ::at32(button_list, ps_move_key_codes::none);
cfg->l2.def = ::at32(button_list, ps_move_key_codes::none);
cfg->l3.def = ::at32(button_list, ps_move_key_codes::none);
// Set default misc variables
cfg->lstickdeadzone.def = 40; // between 0 and 255
cfg->rstickdeadzone.def = 40; // between 0 and 255
cfg->ltriggerthreshold.def = 0; // between 0 and 255
cfg->rtriggerthreshold.def = 0; // between 0 and 255
// apply defaults
cfg->from_default();
}
#ifdef _WIN32
hid_device* ps_move_handler::connect_move_device(ps_move_device* device, std::string_view path)
{
if (!device)
{
return nullptr;
}
// Windows enumerates 3 ps move devices: Col01, Col02, and Col03.
// We use Col01 for data and Col02 for bluetooth.
// Our enumerated paths are filtered and only contain Col01.
// We open Col02 first, and then Col01. Col02 is unused for now.
static const std::string col01 = "&Col01#";
static const std::string number = "&0000#";
std::string col02_path { path };
col02_path.replace(path.find(col01), col01.size(), "&Col02#");
col02_path.replace(path.find(number), number.size(), "&0001#");
// Open Col02
device->bt_device = hid_open_path(col02_path.c_str());
if (!device->bt_device)
{
move_log.error("%s hid_open_path failed! error='%s', path='%s'", m_type, hid_error(device->bt_device), col02_path);
return nullptr;
}
if (const hid_device_info* info = hid_get_device_info(device->bt_device))
{
move_log.notice("%s adding bt device: vid=0x%x, pid=0x%x, path='%s'", m_type, info->vendor_id, info->product_id, col02_path);
}
else
{
move_log.warning("%s adding bt device: vid=N/A, pid=N/A, path='%s', error='%s'", m_type, col02_path, hid_error(device->bt_device));
}
if (hid_set_nonblocking(device->bt_device, 1) == -1)
{
move_log.error("check_add_device: hid_set_nonblocking failed! Reason: %s", hid_error(device->bt_device));
device->close();
return nullptr;
}
// Open Col01
device->hidDevice = hid_open_path(path.data());
if (!device->hidDevice)
{
move_log.error("%s hid_open_path failed! error='%s', path='%s'", m_type, hid_error(device->bt_device), path);
device->close();
return nullptr;
}
if (hid_set_nonblocking(device->hidDevice, 1) == -1)
{
move_log.error("check_add_device: hid_set_nonblocking failed! Reason: %s", hid_error(device->hidDevice));
device->close();
return nullptr;
}
if (const hid_device_info* info = hid_get_device_info(device->hidDevice))
{
move_log.notice("%s adding device: vid=0x%x, pid=0x%x, path='%s'", m_type, info->vendor_id, info->product_id, col02_path);
switch (info->product_id)
{
default:
case MOVE_ID_ZCM1.m_pid:
device->model = ps_move_model::ZCM1;
break;
case MOVE_ID_ZCM2.m_pid:
device->model = ps_move_model::ZCM2;
break;
}
}
else
{
move_log.warning("%s adding device: vid=N/A, pid=N/A, path='%s', error='%s'", m_type, col02_path, hid_error(device->hidDevice));
device->model = ps_move_model::ZCM1;
}
return device->hidDevice;
}
#endif
void ps_move_handler::check_add_device(hid_device* hidDevice, std::string_view path, std::wstring_view wide_serial)
{
#ifndef _WIN32
if (!hidDevice)
{
return;
}
#endif
ps_move_device* device = nullptr;
for (auto& controller : m_controllers)
{
ensure(controller.second);
if (!controller.second->hidDevice)
{
device = controller.second.get();
break;
}
}
if (!device)
{
return;
}
#ifdef _WIN32
hidDevice = connect_move_device(device, path);
if (!hidDevice)
{
device->close();
return;
}
#else
if (hid_set_nonblocking(hidDevice, 1) == -1)
{
move_log.error("check_add_device: hid_set_nonblocking failed! Reason: %s", hid_error(hidDevice));
hid_close(hidDevice);
return;
}
#endif
device->hidDevice = hidDevice;
device->path = path;
// Activate
if (send_output_report(device) == -1)
{
move_log.error("check_add_device: send_output_report failed! Reason: %s", hid_error(hidDevice));
}
std::string serial;
for (wchar_t ch : wide_serial)
serial += static_cast<uchar>(ch);
move_log.success("Added device: serial='%s', path='%s'", serial, device->path);
}
ps_move_handler::DataStatus ps_move_handler::get_data(ps_move_device* device)
{
if (!device)
return DataStatus::ReadError;
constexpr u8 reportId = 0x01;
void* report = nullptr;
usz report_size = 0;
switch (device->model)
{
case ps_move_model::ZCM1:
report = &device->input_report_ZCM1;
device->input_report_ZCM1.common.report_id = reportId;
report_size = sizeof(ps_move_input_report_ZCM1);
break;
case ps_move_model::ZCM2:
report = &device->input_report_ZCM2;
device->input_report_ZCM2.common.report_id = reportId;
report_size = sizeof(ps_move_input_report_ZCM2);
break;
}
std::vector<u8> buf(report_size);
int res = hid_read(device->hidDevice, buf.data(), report_size);
if (res < 0)
{
// looks like controller disconnected or read error
move_log.error("get_data: hid_read 0x%02x failed! result=%d, buf[0]=0x%x, error=%s", reportId, res, buf[0], hid_error(device->hidDevice));
return DataStatus::ReadError;
}
if (res != static_cast<int>(report_size))
return DataStatus::NoNewData;
if (std::memcmp(report, buf.data(), report_size) == 0)
return DataStatus::NoNewData;
// Get the new data
std::memcpy(report, buf.data(), report_size);
//move_log.error("%s", fmt::buf_to_hexstring(buf.data(), buf.size(), 64));
return DataStatus::NewData;
}
PadHandlerBase::connection ps_move_handler::update_connection(const std::shared_ptr<PadDevice>& device)
{
ps_move_device* move_device = static_cast<ps_move_device*>(device.get());
if (!move_device || move_device->path.empty())
return connection::disconnected;
if (move_device->hidDevice == nullptr)
{
// try to reconnect
#ifdef _WIN32
if (hid_device* dev = connect_move_device(move_device, move_device->path))
{
move_device->hidDevice = dev;
}
#else
if (hid_device* dev = hid_open_path(move_device->path.c_str()))
{
if (hid_set_nonblocking(dev, 1) == -1)
{
move_log.error("Reconnecting Device %s: hid_set_nonblocking failed with error %s", move_device->path, hid_error(dev));
}
move_device->hidDevice = dev;
}
#endif
else
{
// nope, not there
move_log.error("Device %s: disconnected", move_device->path);
return connection::disconnected;
}
}
if (get_data(move_device) == DataStatus::ReadError)
{
// this also can mean disconnected, either way deal with it on next loop and reconnect
move_device->close();
return connection::no_data;
}
return connection::connected;
}
void ps_move_handler::handle_external_device(const pad_ensemble& binding)
{
const auto& device = binding.device;
const auto& pad = binding.pad;
ps_move_device* dev = static_cast<ps_move_device*>(device.get());
if (!dev || !pad)
return;
auto& move_data = pad->move_data;
if (dev->model != ps_move_model::ZCM1)
{
move_data.external_device_read_requested = false;
move_data.external_device_write_requested = false;
return;
}
const ps_move_input_report_common& input = dev->input_report_common();
const u16 extra_buttons = input.sequence_number << 8 | input.buttons_3;
move_data.external_device_connected = !!(extra_buttons & button_flags::ext_dev);
if (!move_data.external_device_connected)
{
dev->external_device_id = move_data.external_device_id = 0;
std::memset(move_data.external_device_data.data(), 0, move_data.external_device_data.size());
move_data.external_device_read_requested = false;
move_data.external_device_write_requested = false;
return;
}
std::memcpy(move_data.external_device_data.data(), dev->input_report_ZCM1.ext_device_data.data(), dev->input_report_ZCM1.ext_device_data.size());
if (move_data.external_device_read_requested || move_data.external_device_id == 0)
{
bool success = false;
std::array<u8, 49> ext_buf{};
ext_buf[0x00] = 0xE0; // Report ID
ext_buf[0x01] = 0x01; // Read flag
ext_buf[0x02] = 0xA0; // Target extension device's I²C slave address
ext_buf[0x03] = 0x00; // Offset
ext_buf[0x04] = 0xFF; // Length
if (int res = hid_send_feature_report(dev->hidDevice, ext_buf.data(), ext_buf.size()); res != static_cast<int>(ext_buf.size()))
{
move_log.error("get_extended_info: hid_send_feature_report 0xE0 (external_device) failed! result=%d, ext_buf[0]=0x%x, error=%s", res, ext_buf[0], hid_error(dev->hidDevice));
}
else if (res = hid_get_feature_report(dev->hidDevice, ext_buf.data(), ext_buf.size()); res < 0)
{
move_log.error("get_extended_info: hid_get_feature_report 0xE0 (external_device) failed! result=%d, ext_buf[0]=0x%x, error=%s", res, ext_buf[0], hid_error(dev->hidDevice));
}
else if (ext_buf[0x01] != 0) // The result will hold an error flag at pos 0x01
{
move_log.error("get_extended_info: hid_get_feature_report 0xE0 (external_device) returned error: ext_buf[0x01]=0x%x, error=%s", ext_buf[0x01], hid_error(dev->hidDevice));
}
else
{
move_log.trace("get_extended_info: hid_get_feature_report 0xE0 got result: %s", fmt::buf_to_hexstring(ext_buf.data(), ext_buf.size(), 64));
success = true;
}
// Get device ID
const u32 old_id = dev->external_device_id;
// The result will be stored starting at pos 0x09
dev->external_device_id = move_data.external_device_id = (ext_buf[0x09] << 8) | ext_buf[0x0A];
if (dev->external_device_id != 0 && dev->external_device_id != old_id)
{
move_log.notice("get_extended_info: external device with ID 0x%x found", dev->external_device_id);
}
if (move_data.external_device_read_requested)
{
auto& dst = move_data.external_device_read;
if (success)
{
// Copy everything except device ID starting at pos 0x0B
ensure(ext_buf.size() == dst.size() + 0x0B);
std::memcpy(dst.data(), &ext_buf[0x0B], dst.size());
}
else
{
std::memset(dst.data(), 0, dst.size());
}
}
}
if (move_data.external_device_write_requested)
{
const auto& src = move_data.external_device_write;
std::array<u8, 49> ext_buf{};
ext_buf[0x00] = 0xE0; // Report ID
ext_buf[0x01] = 0x00; // Read flag
ext_buf[0x02] = 0xA0; // Target extension device's I²C slave address
ext_buf[0x03] = src[0]; // Control Byte
ext_buf[0x04] = static_cast<u8>(src.size() - 1); // Length
std::memcpy(&ext_buf[0x09], &src[1], src.size() - 1);
move_log.trace("ps_move_handler: trying to send data to external device: %s", fmt::buf_to_hexstring(ext_buf.data(), ext_buf.size(), 64));
if (const int res = hid_send_feature_report(dev->hidDevice, ext_buf.data(), ext_buf.size()); res < 0)
{
move_log.error("get_extended_info: hid_send_feature_report 0xE0 (external_device) failed! result=%d, ext_buf[0]=0x%x, error=%s", res, ext_buf[0], hid_error(dev->hidDevice));
}
}
move_data.external_device_read_requested = false;
move_data.external_device_write_requested = false;
}
bool ps_move_handler::get_is_left_trigger(const std::shared_ptr<PadDevice>& /*device*/, u64 keyCode)
{
// We also report the T button as left trigger
return keyCode == ps_move_key_codes::L2 || keyCode == ps_move_key_codes::t;
}
bool ps_move_handler::get_is_right_trigger(const std::shared_ptr<PadDevice>& /*device*/, u64 keyCode)
{
// We also report the Throttle button as right trigger
return keyCode == ps_move_key_codes::R2 || keyCode == ps_move_key_codes::throttle;
}
std::unordered_map<u64, u16> ps_move_handler::get_button_values(const std::shared_ptr<PadDevice>& device)
{
std::unordered_map<u64, u16> key_buf;
ps_move_device* dev = static_cast<ps_move_device*>(device.get());
if (!dev)
return key_buf;
const ps_move_input_report_common& input = dev->input_report_common();
key_buf[ps_move_key_codes::cross] = (input.buttons_2 & button_flags::cross) ? 255 : 0;
key_buf[ps_move_key_codes::square] = (input.buttons_2 & button_flags::square) ? 255 : 0;
key_buf[ps_move_key_codes::circle] = (input.buttons_2 & button_flags::circle) ? 255 : 0;
key_buf[ps_move_key_codes::triangle] = (input.buttons_2 & button_flags::triangle) ? 255 : 0;
key_buf[ps_move_key_codes::start] = (input.buttons_1 & button_flags::start) ? 255 : 0;
key_buf[ps_move_key_codes::select] = (input.buttons_1 & button_flags::select) ? 255 : 0;
const u16 extra_buttons = input.sequence_number << 8 | input.buttons_3;
key_buf[ps_move_key_codes::ps] = (extra_buttons & button_flags::ps) ? 255 : 0;
key_buf[ps_move_key_codes::move] = (extra_buttons & button_flags::move) ? 255 : 0;
key_buf[ps_move_key_codes::t] = (extra_buttons & button_flags::t) ? input.trigger_2 : 0;
dev->battery_level = input.battery_level;
// Handle external data
if (dev->model == ps_move_model::ZCM1)
{
const bool external_device_connected = !!(extra_buttons & button_flags::ext_dev);
if (external_device_connected)
{
const std::array<u8, 5>& ext_data = dev->input_report_ZCM1.ext_device_data;
switch (dev->external_device_id)
{
case SHARP_SHOOTER_DEVICE_ID:
key_buf[ps_move_key_codes::firing_mode_1] = (ext_data[0] & button_flags::ss_firing_mode_1) ? 255 : 0;
key_buf[ps_move_key_codes::firing_mode_2] = (ext_data[0] & button_flags::ss_firing_mode_2) ? 255 : 0;
key_buf[ps_move_key_codes::firing_mode_3] = (ext_data[0] & button_flags::ss_firing_mode_3) ? 255 : 0;
key_buf[ps_move_key_codes::reload] = (ext_data[0] & button_flags::ss_reload) ? 255 : 0;
//key_buf[ps_move_key_codes::t] = (ext_data[0] & button_flags::ss_trigger) ? 255 : 0; // This is already reported as normal trigger
break;
case RACING_WHEEL_DEVICE_ID:
key_buf[ps_move_key_codes::dpad_up] = (input.buttons_1 & button_flags::rw_d_pad_up) ? 255 : 0;
key_buf[ps_move_key_codes::dpad_right] = (input.buttons_1 & button_flags::rw_d_pad_right) ? 255 : 0;
key_buf[ps_move_key_codes::dpad_down] = (input.buttons_1 & button_flags::rw_d_pad_down) ? 255 : 0;
key_buf[ps_move_key_codes::dpad_left] = (input.buttons_1 & button_flags::rw_d_pad_left) ? 255 : 0;
key_buf[ps_move_key_codes::L1] = (input.buttons_2 & button_flags::rw_l1) ? 255 : 0;
key_buf[ps_move_key_codes::R1] = (input.buttons_2 & button_flags::rw_r1) ? 255 : 0;
key_buf[ps_move_key_codes::throttle] = ext_data[0];
key_buf[ps_move_key_codes::L2] = ext_data[1];
key_buf[ps_move_key_codes::R2] = ext_data[2];
key_buf[ps_move_key_codes::paddle_left] = (ext_data[3] & button_flags::rw_paddle_l) ? 255 : 0;
key_buf[ps_move_key_codes::paddle_right] = (ext_data[3] & button_flags::rw_paddle_r) ? 255 : 0;
break;
default:
break;
}
}
}
return key_buf;
}
void ps_move_handler::get_extended_info(const pad_ensemble& binding)
{
const auto& device = binding.device;
const auto& pad = binding.pad;
ps_move_device* dev = static_cast<ps_move_device*>(device.get());
if (!dev || !pad)
return;
const ps_move_input_report_common& input = dev->input_report_common();
constexpr f32 MOVE_ONE_G = 4096.0f; // This is just a rough estimate and probably depends on the device
// The default position is flat on the ground, pointing forward.
// The accelerometers constantly measure G forces.
// The gyros measure changes in orientation and will reset when the device isn't moved anymore.
s16 accel_x = input.accel_x_2; // Increases if the device is rolled to the left
s16 accel_y = input.accel_y_2; // Increases if the device is pitched upwards
s16 accel_z = input.accel_z_2; // Increases if the device is moved upwards
s16 gyro_x = input.gyro_x_2; // Increases if the device is pitched upwards
s16 gyro_y = input.gyro_y_2; // Increases if the device is rolled to the right
s16 gyro_z = input.gyro_z_2; // Increases if the device is yawed to the left
if (dev->model == ps_move_model::ZCM1)
{
accel_x -= zero_shift;
accel_y -= zero_shift;
accel_z -= zero_shift;
gyro_x -= zero_shift;
gyro_y -= zero_shift;
gyro_z -= zero_shift;
}
pad->m_sensors[0].m_value = Clamp0To1023(512.0f + (MOTION_ONE_G * (accel_x / MOVE_ONE_G) * -1.0f));
pad->m_sensors[1].m_value = Clamp0To1023(512.0f + (MOTION_ONE_G * (accel_z / MOVE_ONE_G) * -1.0f));
pad->m_sensors[2].m_value = Clamp0To1023(512.0f + (MOTION_ONE_G * (accel_y / MOVE_ONE_G)));
pad->m_sensors[3].m_value = Clamp0To1023(512.0f + (MOTION_ONE_G * (gyro_z / MOVE_ONE_G) * -1.0f));
pad->move_data.accelerometer_x = accel_x;
pad->move_data.accelerometer_y = accel_y;
pad->move_data.accelerometer_z = accel_z;
pad->move_data.gyro_x = gyro_x;
pad->move_data.gyro_y = gyro_y;
pad->move_data.gyro_z = gyro_z;
pad->move_data.temperature = ((input.temperature << 4) | ((input.magnetometer_x & 0xF0) >> 4));
handle_external_device(binding);
}
pad_preview_values ps_move_handler::get_preview_values(const std::unordered_map<u64, u16>& data)
{
return {
std::max(::at32(data, ps_move_key_codes::L2), ::at32(data, ps_move_key_codes::t)),
std::max(::at32(data, ps_move_key_codes::R2), ::at32(data, ps_move_key_codes::throttle)),
0,
0,
0,
0
};
}
int ps_move_handler::send_output_report(ps_move_device* device)
{
if (!device || !device->hidDevice)
return -2;
const cfg_pad* config = device->config;
if (config == nullptr)
return -2; // hid_write returns -1 on error
device->output_report.type = 0x06;
device->output_report.rumble = device->large_motor;
// Override color if necessary (for example while we actually use the PS Move with cellGem)
if (device->color_override_active)
{
device->output_report.r = device->color_override.r;
device->output_report.g = device->color_override.g;
device->output_report.b = device->color_override.b;
}
else
{
device->output_report.r = config->colorR;
device->output_report.g = config->colorG;
device->output_report.b = config->colorB;
}
const auto now = steady_clock::now();
const auto elapsed = now - device->last_output_report_time;
// Update LED at an interval or it will be disabled automatically
if (elapsed >= 4000ms)
{
device->new_output_data = true;
}
else
{
// Use LED update rate of 120ms
if (elapsed < 120ms)
{
return -3;
}
device->new_output_data = std::memcmp(&device->output_report, &device->last_output_report, sizeof(ps_move_output_report));
if (!device->new_output_data)
{
return -3;
}
}
device->last_output_report_time = now;
device->last_output_report = device->output_report;
return hid_write(device->hidDevice, reinterpret_cast<u8*>(&device->output_report), sizeof(ps_move_output_report));
}
void ps_move_handler::apply_pad_data(const pad_ensemble& binding)
{
const auto& device = binding.device;
const auto& pad = binding.pad;
ps_move_device* dev = static_cast<ps_move_device*>(device.get());
if (!dev || !dev->hidDevice || !dev->config || !pad)
return;
cfg_pad* config = dev->config;
const int idx_l = config->switch_vibration_motors ? 1 : 0;
const u8 speed_large = config->enable_vibration_motor_large ? pad->m_vibrateMotors[idx_l].m_value : 0;
dev->large_motor = speed_large;
if (send_output_report(dev) >= 0)
{
dev->new_output_data = false;
}
}
void ps_move_handler::SetPadData(const std::string& padId, u8 player_id, u8 large_motor, u8 small_motor, s32 r, s32 g, s32 b, bool /*player_led*/, bool /*battery_led*/, u32 /*battery_led_brightness*/)
{
std::shared_ptr<ps_move_device> device = get_hid_device(padId);
if (device == nullptr || device->hidDevice == nullptr)
return;
device->large_motor = large_motor;
device->small_motor = small_motor;
device->player_id = player_id;
device->config = get_config(padId);
ensure(device->config);
if (r >= 0 && g >= 0 && b >= 0 && r <= 255 && g <= 255 && b <= 255)
{
device->config->colorR.set(r);
device->config->colorG.set(g);
device->config->colorB.set(b);
}
if (send_output_report(device.get()) >= 0)
{
device->new_output_data = false;
}
}
u32 ps_move_handler::get_battery_level(const std::string& padId)
{
const std::shared_ptr<ps_move_device> device = get_hid_device(padId);
if (!device || !device->hidDevice)
{
return 0;
}
switch (device->battery_level)
{
case battery_status::usb_charging:
case battery_status::usb_charged:
return 100;
default:
break;
}
// 0 to 5
return std::clamp<u32>(device->battery_level * 20, 0, 100);
}

View File

@ -0,0 +1,166 @@
#pragma once
#include "hid_pad_handler.h"
#include <unordered_map>
namespace reports
{
// NOTE: The 1st half-frame contains slightly older data than the 2nd half-frame
#pragma pack(push, 1)
struct ps_move_input_report_common
{
// ID Size Description
u8 report_id{}; // 0x00 1 HID Report ID (always 0x01)
u8 buttons_1{}; // 0x01 1 Buttons 1 (Start, Select)
u8 buttons_2{}; // 0x02 1 Buttons 2 (X, Square, Circle, Triangle)
u8 buttons_3{}; // 0x03 1+ Buttons 3 (PS, Move, T) and EXT
u8 sequence_number{}; // 0x04 1- Sequence number
u8 trigger_1{}; // 0x05 1 T button values (1st half-frame)
u8 trigger_2{}; // 0x06 1 T button values (2nd half-frame)
u32 magic{}; // 0x07 4 always 0x7F7F7F7F
u8 timestamp_upper{}; // 0x0B 1 Timestamp (upper byte)
u8 battery_level{}; // 0x0C 1 Battery level. 0x05 = max, 0xEE = USB charging
s16 accel_x_1{}; // 0x0D 2 X-axis accelerometer (1st half-frame)
s16 accel_y_1{}; // 0x0F 2 Z-axis accelerometer (1st half-frame)
s16 accel_z_1{}; // 0x11 2 Y-axis accelerometer (1st half-frame)
s16 accel_x_2{}; // 0x13 2 X-axis accelerometer (2nd half-frame)
s16 accel_y_2{}; // 0x15 2 Z-axis accelerometer (2nd half-frame)
s16 accel_z_2{}; // 0x17 2 Y-axis accelerometer (2nd half-frame)
s16 gyro_x_1{}; // 0x19 2 X-axis gyroscope (1st half-frame)
s16 gyro_y_1{}; // 0x1B 2 Z-axis gyroscope (1st half-frame)
s16 gyro_z_1{}; // 0x1D 2 Y-axis gyroscope (1st half-frame)
s16 gyro_x_2{}; // 0x1F 2 X-axis gyroscope (2nd half-frame)
s16 gyro_y_2{}; // 0x21 2 Z-axis gyroscope (2nd half-frame)
s16 gyro_z_2{}; // 0x23 2 Y-axis gyroscope (2nd half-frame)
u8 temperature{}; // 0x25 1+ Temperature
u8 magnetometer_x{}; // 0x26 1+ Temperature + X-axis magnetometer
};
#pragma pack(pop)
#pragma pack(push, 1)
struct ps_move_input_report_ZCM1
{
ps_move_input_report_common common{};
// ID Size Description
u8 magnetometer_x2{}; // 0x27 1- X-axis magnetometer
u8 magnetometer_y{}; // 0x28 1+ Z-axis magnetometer
u16 magnetometer_z{}; // 0x29 1- Y-axis magnetometer
u8 timestamp_lower{}; // 0x2B 1 Timestamp (lower byte)
std::array<u8, 5> ext_device_data{}; // 0x2C 5 External device data
};
#pragma pack(pop)
#pragma pack(push, 1)
struct ps_move_input_report_ZCM2
{
ps_move_input_report_common common{};
// ID Size Description
u16 timestamp2; // 0x27 2 same as common timestamp
u16 unk; // 0x29 2 Unknown
u8 timestamp_lower; // 0x2B 1 Timestamp (lower byte)
};
#pragma pack(pop)
struct ps_move_output_report
{
u8 type{};
u8 zero{};
u8 r{};
u8 g{};
u8 b{};
u8 zero2{};
u8 rumble{};
u8 padding[2];
};
struct ps_move_feature_report
{
std::array<u8, 4> data{}; // TODO
};
}
enum class ps_move_model
{
ZCM1, // PS3
ZCM2, // PS4
};
class ps_move_device : public HidDevice
{
public:
ps_move_model model = ps_move_model::ZCM1;
reports::ps_move_input_report_ZCM1 input_report_ZCM1{};
reports::ps_move_input_report_ZCM2 input_report_ZCM2{};
reports::ps_move_output_report output_report{};
reports::ps_move_output_report last_output_report{};
steady_clock::time_point last_output_report_time;
u32 external_device_id = 0;
const reports::ps_move_input_report_common& input_report_common() const;
};
class ps_move_handler final : public hid_pad_handler<ps_move_device>
{
enum ps_move_key_codes
{
none = 0,
cross,
square,
circle,
triangle,
start,
select,
ps,
move,
t,
// Available through external sharpshooter
firing_mode_1,
firing_mode_2,
firing_mode_3,
reload,
// Available through external racing wheel
dpad_up,
dpad_down,
dpad_left,
dpad_right,
L1,
R1,
L2,
R2,
throttle,
paddle_left,
paddle_right,
};
public:
ps_move_handler();
~ps_move_handler();
void SetPadData(const std::string& padId, u8 player_id, u8 large_motor, u8 small_motor, s32 r, s32 g, s32 b, bool player_led, bool battery_led, u32 battery_led_brightness) override;
u32 get_battery_level(const std::string& padId) override;
void init_config(cfg_pad* cfg) override;
private:
#ifdef _WIN32
hid_device* connect_move_device(ps_move_device* device, std::string_view path);
#endif
DataStatus get_data(ps_move_device* device) override;
void check_add_device(hid_device* hidDevice, std::string_view path, std::wstring_view wide_serial) override;
int send_output_report(ps_move_device* device) override;
bool get_is_left_trigger(const std::shared_ptr<PadDevice>& device, u64 keyCode) override;
bool get_is_right_trigger(const std::shared_ptr<PadDevice>& device, u64 keyCode) override;
PadHandlerBase::connection update_connection(const std::shared_ptr<PadDevice>& device) override;
std::unordered_map<u64, u16> get_button_values(const std::shared_ptr<PadDevice>& device) override;
pad_preview_values get_preview_values(const std::unordered_map<u64, u16>& data) override;
void get_extended_info(const pad_ensemble& binding) override;
void apply_pad_data(const pad_ensemble& binding) override;
void handle_external_device(const pad_ensemble& binding);
};

View File

@ -0,0 +1,545 @@
#include "stdafx.h"
#include "Emu/Cell/Modules/cellCamera.h"
#include "Emu/Cell/Modules/cellGem.h"
#include "ps_move_tracker.h"
#include <cmath>
#include <opencv2/photo.hpp>
LOG_CHANNEL(ps_move);
namespace gem
{
extern bool convert_image_format(CellCameraFormat input_format, CellGemVideoConvertFormatEnum output_format,
const std::vector<u8>& video_data_in, u32 width, u32 height,
u8* video_data_out, u32 video_data_out_size);
}
template class ps_move_tracker<false>;
template class ps_move_tracker<true>;
template <bool DiagnosticsEnabled>
ps_move_tracker<DiagnosticsEnabled>::ps_move_tracker()
{
init_workers();
}
template <bool DiagnosticsEnabled>
ps_move_tracker<DiagnosticsEnabled>::~ps_move_tracker()
{
for (u32 index = 0; index < CELL_GEM_MAX_NUM; index++)
{
if (auto& worker = m_workers[index])
{
auto& thread = *worker;
thread = thread_state::aborting;
m_wake_up_workers[index].release(1);
m_wake_up_workers[index].notify_one();
thread();
}
}
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::set_image_data(const void* buf, u64 size, u32 width, u32 height, s32 format)
{
if (!buf || !size || !width || !height || !format)
{
ps_move.error("ps_move_tracker got unexpected input: buf=*0x%x, size=%d, width=%d, height=%d, format=%d", buf, size, width, height, format);
return;
}
m_width = width;
m_height = height;
m_format = format;
m_image_data.resize(size);
std::memcpy(m_image_data.data(), buf, size);
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::set_active(u32 index, bool active)
{
ps_move_config& config = ::at32(m_config, index);
config.active = active;
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::ps_move_config::calculate_values()
{
// The hue is a "circle", so we use modulo 360.
max_hue = (hue + hue_threshold) % 360;
min_hue = hue - hue_threshold;
if (min_hue < 0)
{
min_hue += 360;
}
else
{
min_hue %= 360;
}
saturation_threshold = saturation_threshold_u8 / 255.0f;
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::set_hue(u32 index, u16 hue)
{
ps_move_config& config = ::at32(m_config, index);
config.hue = hue;
config.calculate_values();
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::set_hue_threshold(u32 index, u16 threshold)
{
ps_move_config& config = ::at32(m_config, index);
config.hue_threshold = threshold;
config.calculate_values();
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::set_saturation_threshold(u32 index, u16 threshold)
{
ps_move_config& config = ::at32(m_config, index);
config.saturation_threshold_u8 = threshold;
config.calculate_values();
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::init_workers()
{
for (u32 index = 0; index < CELL_GEM_MAX_NUM; index++)
{
if (m_workers[index])
{
continue;
}
m_workers[index] = std::make_unique<named_thread<std::function<void()>>>(fmt::format("PS Move Worker %d", index), [this, index]()
{
while (thread_ctrl::state() != thread_state::aborting)
{
// Notify that all work is done
m_workers_finished[index].release(1);
m_workers_finished[index].notify_one();
// Wait for work
m_wake_up_workers[index].wait(0);
m_wake_up_workers[index].release(0);
if (thread_ctrl::state() == thread_state::aborting)
{
break;
}
// Find contours
ps_move_info& info = m_info[index];
ps_move_info new_info{};
process_contours(new_info, index);
if (new_info.valid)
{
info = std::move(new_info);
}
else
{
info.valid = false;
}
}
// Notify one last time that all work is done
m_workers_finished[index].release(1);
m_workers_finished[index].notify_one();
});
}
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::process_image()
{
// Convert image to RGBA
convert_image(CELL_GEM_RGBA_640x480);
// Calculate hues
process_hues();
// Get active devices
std::vector<u32> active_devices;
for (u32 index = 0; index < CELL_GEM_MAX_NUM; index++)
{
ps_move_config& config = m_config[index];
if (config.active)
{
active_devices.push_back(index);
}
else
{
ps_move_info& info = m_info[index];
info.valid = false;
}
}
// Find contours on worker threads
for (u32 index : active_devices)
{
// Clear old state
m_workers_finished[index].release(0);
// Wake up worker
m_wake_up_workers[index].release(1);
m_wake_up_workers[index].notify_one();
}
// Wait for worker threads (a couple of seconds so we don't deadlock)
for (u32 index : active_devices)
{
// Wait for worker
m_workers_finished[index].wait(0, atomic_wait_timeout{5000000000});
m_workers_finished[index].release(0);
}
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::convert_image(s32 output_format)
{
const u32 width = m_width;
const u32 height = m_height;
const u32 size = height * width;
m_image_rgba.resize(size * 4);
m_image_hsv.resize(size * 3);
for (u32 index = 0; index < CELL_GEM_MAX_NUM; index++)
{
m_image_binary[index].resize(size);
}
if (gem::convert_image_format(CellCameraFormat{m_format}, CellGemVideoConvertFormatEnum{output_format}, m_image_data, width, height, m_image_rgba.data(), ::size32(m_image_rgba)))
{
ps_move.trace("Converted video frame of format %s to %s", CellCameraFormat{m_format}, CellGemVideoConvertFormatEnum{output_format});
}
if constexpr (DiagnosticsEnabled)
{
m_image_gray.resize(size);
m_image_rgba_contours.resize(size * 4);
std::memcpy(m_image_rgba_contours.data(), m_image_rgba.data(), m_image_rgba.size());
}
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::process_hues()
{
const u32 width = m_width;
const u32 height = m_height;
static const double sqrt3 = sqrt(3);
if constexpr (DiagnosticsEnabled)
{
std::fill(m_hues.begin(), m_hues.end(), 0);
}
u8* gray = nullptr;
for (u32 y = 0; y < height; y++)
{
const u8* rgba = &m_image_rgba[y * width * 4];
u8* hsv = &m_image_hsv[y * width * 3];
if constexpr (DiagnosticsEnabled)
{
gray = &m_image_gray[y * width];
}
for (u32 x = 0; x < width; x++, rgba += 4, hsv += 3)
{
const float r = rgba[0] / 255.0f;
const float g = rgba[1] / 255.0f;
const float b = rgba[2] / 255.0f;
const auto [hue, saturation, value] = rgb_to_hsv(r, g, b);
hsv[0] = static_cast<u8>(hue / 2);
hsv[1] = static_cast<u8>(saturation * 255.0f);
hsv[2] = static_cast<u8>(value * 255.0f);
if constexpr (DiagnosticsEnabled)
{
*gray++ = static_cast<u8>(std::clamp((0.299f * r + 0.587f * g + 0.114f * b) * 255.0f, 0.0f, 255.0f));
++m_hues[hue];
}
}
}
}
static bool is_circular_contour(const std::vector<cv::Point>& contour, float& area)
{
std::vector<cv::Point> approx;
cv::approxPolyDP(contour, approx, 0.01 * cv::arcLength(contour, true), true);
if (approx.size() < 8ULL) return false;
area = static_cast<float>(cv::contourArea(contour));
if (area < 30.0f) return false;
cv::Point2f center;
float radius;
cv::minEnclosingCircle(contour, center, radius);
if (radius < 5.0f) return false;
return true;
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::process_contours(ps_move_info& info, u32 index)
{
const ps_move_config& config = ::at32(m_config, index);
const std::vector<u8>& image_hsv = m_image_hsv;
std::vector<u8>& image_binary = ::at32(m_image_binary, index);
const u32 width = m_width;
const u32 height = m_height;
const bool wrapped_hue = config.min_hue > config.max_hue; // e.g. min=355, max=5 (red)
info.valid = false;
info.x_max = width;
info.y_max = height;
// Map memory
cv::Mat binary(cv::Size(width, height), CV_8UC1, image_binary.data(), 0);
// Filter image
for (u32 y = 0; y < height; y++)
{
const u8* src = &image_hsv[y * width * 3];
u8* dst = &image_binary[y * width];
for (u32 x = 0; x < width; x++, src += 3)
{
const u16 hue = src[0] * 2;
const u8 saturation = src[1];
const u8 value = src[2];
// Simply drop dark and colorless pixels as well as pixels that don't match our hue
if ((wrapped_hue ? (hue < config.min_hue && hue > config.max_hue) : (hue < config.min_hue || hue > config.max_hue)) ||
saturation < config.saturation_threshold_u8 || saturation > 200 ||
value < 150 || value > 255)
{
dst[x] = 0;
}
else
{
dst[x] = 255;
}
}
}
// Remove all small outer contours
if (m_filter_small_contours)
{
std::vector<std::vector<cv::Point>> contours;
cv::findContours(binary, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);
for (auto it = contours.begin(); it != contours.end();)
{
float area;
if (is_circular_contour(*it, area))
{
it = contours.erase(it);
continue;
}
it++;
}
if (!contours.empty())
{
cv::drawContours(binary, contours, -1, 0, cv::FILLED);
}
}
// Find best contour
std::vector<std::vector<cv::Point>> all_contours;
cv::findContours(binary, all_contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
if (all_contours.empty())
return;
std::vector<std::vector<cv::Point>> contours;
contours.reserve(all_contours.size());
std::vector<cv::Point2f> centers;
centers.reserve(all_contours.size());
std::vector<float> radii;
radii.reserve(all_contours.size());
const f32 min_radius = m_min_radius * width;
const f32 max_radius = m_max_radius * width;
usz best_index = umax;
float best_area = 0.0f;
for (usz i = 0; i < all_contours.size(); i++)
{
const std::vector<cv::Point>& contour = all_contours[i];
float area;
if (!is_circular_contour(contour, area))
continue;
// Get center and radius
cv::Point2f center;
float radius;
cv::minEnclosingCircle(contour, center, radius);
// Filter radius
if (radius < min_radius || radius > max_radius)
continue;
contours.push_back(std::move(all_contours[i]));
centers.push_back(std::move(center));
radii.push_back(std::move(radius));
if (area > best_area)
{
best_area = area;
best_index = contours.size() - 1;
}
}
if (best_index == umax)
return;
info.valid = true;
info.distance = 1500.0f;
info.radius = radii[best_index];
info.x_pos = std::clamp(static_cast<u32>(centers[best_index].x), 0u, width);
info.y_pos = std::clamp(static_cast<u32>(centers[best_index].y), 0u, height);
if constexpr (!DiagnosticsEnabled)
return;
if (!m_draw_contours && !m_draw_overlays) [[likely]]
return;
// Map memory
cv::Mat rgba(cv::Size(width, height), CV_8UC4, m_image_rgba_contours.data(), 0);
if (!m_show_all_contours) [[likely]]
{
std::vector<cv::Point> contour = std::move(contours[best_index]);
contours = { std::move(contour) };
centers = { centers[best_index] };
radii = { radii[best_index] };
}
static const cv::Scalar contour_color(255, 0, 0, 255);
static const cv::Scalar circle_color(0, 255, 0, 255);
static const cv::Scalar center_color(0, 0, 255, 255);
// Draw contours
if (m_draw_contours)
{
cv::drawContours(rgba, contours, -1, contour_color, cv::FILLED);
}
// Draw overlays
if (m_draw_overlays)
{
for (usz i = 0; i < centers.size(); i++)
{
const cv::Point2f& center = centers[i];
const float radius = radii[i];
cv::circle(rgba, center, static_cast<int>(radius), circle_color, 1);
cv::line(rgba, center + cv::Point2f(-2, 0), center + cv::Point2f(2, 0), center_color, 1);
cv::line(rgba, center + cv::Point2f(0, -2), center + cv::Point2f(0, 2), center_color, 1);
}
}
}
template <bool DiagnosticsEnabled>
std::tuple<u8, u8, u8> ps_move_tracker<DiagnosticsEnabled>::hsv_to_rgb(u16 hue, float saturation, float value)
{
const float h = hue / 60.0f;
const float chroma = value * saturation;
const float x = chroma * (1.0f - std::abs(std::fmod(h, 2.0f) - 1.0f));
const float m = value - chroma;
float r = 0.0f;
float g = 0.0f;
float b = 0.0f;
switch (static_cast<int>(std::ceil(h)))
{
case 0:
case 1:
r = chroma + m;
g = x + m;
b = 0 + m;
break;
case 2:
r = x + m;
g = chroma + m;
b = 0 + m;
break;
case 3:
r = 0 + m;
g = chroma + m;
b = x + m;
break;
case 4:
r = 0 + m;
g = x + m;
b = chroma + m;
break;
case 5:
r = x + m;
g = 0 + m;
b = chroma + m;
break;
case 6:
r = chroma + m;
g = 0 + m;
b = x + m;
break;
default:
break;
}
const u8 red = static_cast<u8>(std::clamp(std::round(r * 255.0f), 0.0f, 255.0f));
const u8 green = static_cast<u8>(std::clamp(std::round(g * 255.0f), 0.0f, 255.0f));
const u8 blue = static_cast<u8>(std::clamp(std::round(b * 255.0f), 0.0f, 255.0f));
return { red, green, blue };
}
template <bool DiagnosticsEnabled>
std::tuple<s16, float, float> ps_move_tracker<DiagnosticsEnabled>::rgb_to_hsv(float r, float g, float b)
{
const float cmax = std::max({r, g, b}); // V (of HSV)
const float cmin = std::min({r, g, b});
const float delta = cmax - cmin;
const float saturation = cmax ? (delta / cmax) : 0.0f; // S (of HSV)
s16 hue; // H (of HSV)
if (!delta)
{
hue = 0;
}
else if (cmax == r)
{
hue = (static_cast<s16>(60.0f * (g - b) / delta) + 360) % 360;
}
else if (cmax == g)
{
hue = (static_cast<s16>(60.0f * (b - r) / delta) + 120 + 360) % 360;
}
else
{
hue = (static_cast<s16>(60.0f * (r - g) / delta) + 240 + 360) % 360;
}
return { hue, saturation, cmax };
}

View File

@ -0,0 +1,95 @@
#pragma once
struct ps_move_info
{
bool valid = false;
f32 radius = 0.0f;
f32 distance = 0.0f;
u32 x_pos = 0;
u32 y_pos = 0;
u32 x_max = 0;
u32 y_max = 0;
};
template <bool DiagnosticsEnabled = false>
class ps_move_tracker
{
public:
ps_move_tracker();
virtual ~ps_move_tracker();
void set_image_data(const void* buf, u64 size, u32 width, u32 height, s32 format);
void init_workers();
void process_image();
void convert_image(s32 output_format);
void process_hues();
void process_contours(ps_move_info& info, u32 index);
void set_active(u32 index, bool active);
void set_hue(u32 index, u16 hue);
void set_hue_threshold(u32 index, u16 threshold);
void set_saturation_threshold(u32 index, u16 threshold);
void set_min_radius(f32 radius) { m_min_radius = radius; }
void set_max_radius(f32 radius) { m_max_radius = radius; }
void set_filter_small_contours(bool enabled = false) { m_filter_small_contours = enabled; }
void set_show_all_contours(bool enabled = false) { m_show_all_contours = enabled; }
void set_draw_contours(bool enabled = false) { m_draw_contours = enabled; }
void set_draw_overlays(bool enabled = false) { m_draw_overlays = enabled; }
const std::array<ps_move_info, CELL_GEM_MAX_NUM>& info() { return m_info; }
const std::array<u32, 360>& hues() { return m_hues; }
const std::vector<u8>& rgba() { return m_image_rgba; }
const std::vector<u8>& rgba_contours() { return m_image_rgba_contours; }
const std::vector<u8>& hsv() { return m_image_hsv; }
const std::vector<u8>& gray() { return m_image_gray; }
const std::vector<u8>& binary(u32 index) { return ::at32(m_image_binary, index); }
static std::tuple<u8, u8, u8> hsv_to_rgb(u16 hue, float saturation, float value);
static std::tuple<s16, float, float> rgb_to_hsv(float r, float g, float b);
private:
struct ps_move_config
{
bool active = false;
u16 hue = 0;
u16 hue_threshold = 0;
u16 saturation_threshold_u8 = 0;
s16 min_hue = 0;
s16 max_hue = 0;
f32 saturation_threshold = 0.0f;
void calculate_values();
};
u32 m_width = 0;
u32 m_height = 0;
s32 m_format = 0;
f32 m_min_radius = 0.0f;
f32 m_max_radius = 1.0f;
bool m_filter_small_contours = true;
bool m_show_all_contours = false;
bool m_draw_contours = false;
bool m_draw_overlays = false;
std::vector<u8> m_image_data;
std::vector<u8> m_image_rgba;
std::vector<u8> m_image_rgba_contours;
std::vector<u8> m_image_hsv;
std::vector<u8> m_image_gray;
std::array<std::vector<u8>, CELL_GEM_MAX_NUM> m_image_hue_filtered{};
std::array<std::vector<u8>, CELL_GEM_MAX_NUM> m_image_binary{};
std::array<u32, 360> m_hues{};
std::array<ps_move_info, CELL_GEM_MAX_NUM> m_info{};
std::array<ps_move_config, CELL_GEM_MAX_NUM> m_config{};
std::array<std::unique_ptr<named_thread<std::function<void()>>>, CELL_GEM_MAX_NUM> m_workers{};
std::array<atomic_t<u32>, CELL_GEM_MAX_NUM> m_wake_up_workers{};
std::array<atomic_t<u32>, CELL_GEM_MAX_NUM> m_workers_finished{};
};

View File

@ -71,7 +71,7 @@
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(SolutionDir)3rdparty\SoundTouch\soundtouch\include;$(SolutionDir)3rdparty\cubeb\extra;$(SolutionDir)3rdparty\cubeb\cubeb\include\;$(SolutionDir)3rdparty\flatbuffers\include;$(SolutionDir)3rdparty\wolfssl\wolfssl;$(SolutionDir)3rdparty\curl\curl\include;$(SolutionDir)3rdparty\rtmidi\rtmidi;$(SolutionDir)3rdparty\libusb\libusb\libusb;$(VULKAN_SDK)\Include;$(SolutionDir)3rdparty\libsdl-org\SDL\include;$(QTDIR)\include;$(QTDIR)\include\QtCore;$(QTDIR)\include\QtConcurrent;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtSvgWidgets;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtMultimedia;$(QTDIR)\mkspecs\win32-msvc;.\release;.\QTGeneratedFiles\$(ConfigurationName);.\QTGeneratedFiles;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(SolutionDir)3rdparty\opencv\opencv410\build\include;$(SolutionDir)3rdparty\SoundTouch\soundtouch\include;$(SolutionDir)3rdparty\cubeb\extra;$(SolutionDir)3rdparty\cubeb\cubeb\include\;$(SolutionDir)3rdparty\flatbuffers\include;$(SolutionDir)3rdparty\wolfssl\wolfssl;$(SolutionDir)3rdparty\curl\curl\include;$(SolutionDir)3rdparty\rtmidi\rtmidi;$(SolutionDir)3rdparty\libusb\libusb\libusb;$(VULKAN_SDK)\Include;$(SolutionDir)3rdparty\libsdl-org\SDL\include;$(QTDIR)\include;$(QTDIR)\include\QtCore;$(QTDIR)\include\QtConcurrent;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtSvgWidgets;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtMultimedia;$(QTDIR)\mkspecs\win32-msvc;.\release;.\QTGeneratedFiles\$(ConfigurationName);.\QTGeneratedFiles;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>/Zc:__cplusplus -Zc:strictStrings -Zc:throwingNew- -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions)</AdditionalOptions>
<AssemblerListingLocation>release\</AssemblerListingLocation>
<BrowseInformation>false</BrowseInformation>
@ -89,8 +89,8 @@
<ExternalWarningLevel>TurnOffAllWarnings</ExternalWarningLevel>
</ClCompile>
<Link>
<AdditionalDependencies>DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl.lib;Wldap32.lib;glslang.lib;OSDependent.lib;OGLCompiler.lib;SPIRV.lib;MachineIndependent.lib;GenericCodeGen.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Core.lib;Qt6Gui.lib;Qt6Widgets.lib;Qt6Concurrent.lib;Qt6Multimedia.lib;Qt6MultimediaWidgets.lib;Qt6Svg.lib;Qt6SvgWidgets.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(SolutionDir)3rdparty\openal\openal-soft\build;$(SolutionDir)3rdparty\glslang\build\hlsl\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\SPIRV\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\OGLCompilersDLL\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\glslang\OSDependent\Windows\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\glslang\$(CONFIGURATION);$(SolutionDir)3rdparty\discord-rpc\lib;$(SolutionDir)lib\$(CONFIGURATION)-$(PLATFORM);$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>opencv_world4100.lib;DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl.lib;Wldap32.lib;glslang.lib;OSDependent.lib;OGLCompiler.lib;SPIRV.lib;MachineIndependent.lib;GenericCodeGen.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Core.lib;Qt6Gui.lib;Qt6Widgets.lib;Qt6Concurrent.lib;Qt6Multimedia.lib;Qt6MultimediaWidgets.lib;Qt6Svg.lib;Qt6SvgWidgets.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(SolutionDir)3rdparty\opencv\opencv410\build\x64\vc16\lib;$(SolutionDir)3rdparty\openal\openal-soft\build;$(SolutionDir)3rdparty\glslang\build\hlsl\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\SPIRV\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\OGLCompilersDLL\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\glslang\OSDependent\Windows\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\glslang\$(CONFIGURATION);$(SolutionDir)3rdparty\discord-rpc\lib;$(SolutionDir)lib\$(CONFIGURATION)-$(PLATFORM);$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalOptions>"/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" %(AdditionalOptions)</AdditionalOptions>
<DataExecutionPrevention>true</DataExecutionPrevention>
<GenerateDebugInformation>Debug</GenerateDebugInformation>
@ -123,7 +123,7 @@
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(SolutionDir)3rdparty\SoundTouch\soundtouch\include;$(SolutionDir)3rdparty\cubeb\extra;$(SolutionDir)3rdparty\cubeb\cubeb\include\;$(SolutionDir)3rdparty\flatbuffers\include;$(SolutionDir)3rdparty\wolfssl\wolfssl;$(SolutionDir)3rdparty\curl\curl\include;$(SolutionDir)3rdparty\rtmidi\rtmidi;$(SolutionDir)3rdparty\libusb\libusb\libusb;$(VULKAN_SDK)\Include;$(SolutionDir)3rdparty\libsdl-org\SDL\include;$(QTDIR)\include;$(QTDIR)\include\QtCore;$(QTDIR)\include\QtConcurrent;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtSvgWidgets;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtMultimedia;$(QTDIR)\mkspecs\win32-msvc;.\debug;.\QTGeneratedFiles\$(ConfigurationName);.\QTGeneratedFiles;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(SolutionDir)3rdparty\opencv\opencv410\build\include;$(SolutionDir)3rdparty\SoundTouch\soundtouch\include;$(SolutionDir)3rdparty\cubeb\extra;$(SolutionDir)3rdparty\cubeb\cubeb\include\;$(SolutionDir)3rdparty\flatbuffers\include;$(SolutionDir)3rdparty\wolfssl\wolfssl;$(SolutionDir)3rdparty\curl\curl\include;$(SolutionDir)3rdparty\rtmidi\rtmidi;$(SolutionDir)3rdparty\libusb\libusb\libusb;$(VULKAN_SDK)\Include;$(SolutionDir)3rdparty\libsdl-org\SDL\include;$(QTDIR)\include;$(QTDIR)\include\QtCore;$(QTDIR)\include\QtConcurrent;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtSvgWidgets;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtMultimedia;$(QTDIR)\mkspecs\win32-msvc;.\debug;.\QTGeneratedFiles\$(ConfigurationName);.\QTGeneratedFiles;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>/Zc:__cplusplus -Zc:strictStrings -Zc:throwingNew- -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions)</AdditionalOptions>
<AssemblerListingLocation>debug\</AssemblerListingLocation>
<BrowseInformation>false</BrowseInformation>
@ -140,8 +140,8 @@
<ProgramDataBaseFileName>$(IntDir)vc$(PlatformToolsetVersion).pdb</ProgramDataBaseFileName>
</ClCompile>
<Link>
<AdditionalDependencies>DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl.lib;Wldap32.lib;glslangd.lib;OSDependentd.lib;OGLCompilerd.lib;SPIRVd.lib;MachineIndependentd.lib;GenericCodeGend.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Cored.lib;Qt6Guid.lib;Qt6Widgetsd.lib;Qt6Concurrentd.lib;Qt6Multimediad.lib;Qt6MultimediaWidgetsd.lib;Qt6Svgd.lib;Qt6SvgWidgetsd.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(SolutionDir)3rdparty\openal\openal-soft\build;$(SolutionDir)3rdparty\glslang\build\hlsl\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\SPIRV\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\OGLCompilersDLL\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\glslang\OSDependent\Windows\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\glslang\$(CONFIGURATION);$(SolutionDir)3rdparty\discord-rpc\lib;$(SolutionDir)lib\$(CONFIGURATION)-$(PLATFORM);$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>opencv_world4100.lib;DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl.lib;Wldap32.lib;glslangd.lib;OSDependentd.lib;OGLCompilerd.lib;SPIRVd.lib;MachineIndependentd.lib;GenericCodeGend.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Cored.lib;Qt6Guid.lib;Qt6Widgetsd.lib;Qt6Concurrentd.lib;Qt6Multimediad.lib;Qt6MultimediaWidgetsd.lib;Qt6Svgd.lib;Qt6SvgWidgetsd.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(SolutionDir)3rdparty\opencv\opencv410\build\x64\vc16\lib;$(SolutionDir)3rdparty\openal\openal-soft\build;$(SolutionDir)3rdparty\glslang\build\hlsl\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\SPIRV\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\OGLCompilersDLL\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\glslang\OSDependent\Windows\$(CONFIGURATION);$(SolutionDir)3rdparty\glslang\build\glslang\$(CONFIGURATION);$(SolutionDir)3rdparty\discord-rpc\lib;$(SolutionDir)lib\$(CONFIGURATION)-$(PLATFORM);$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalOptions>"/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /VERBOSE %(AdditionalOptions)</AdditionalOptions>
<DataExecutionPrevention>true</DataExecutionPrevention>
<GenerateDebugInformation>Debug</GenerateDebugInformation>
@ -179,8 +179,11 @@
<ClCompile Include="Input\evdev_gun_handler.cpp" />
<ClCompile Include="Input\gui_pad_thread.cpp" />
<ClCompile Include="Input\hid_pad_handler.cpp" />
<ClCompile Include="Input\ps_move_config.cpp" />
<ClCompile Include="Input\ps_move_tracker.cpp" />
<ClCompile Include="Input\raw_mouse_config.cpp" />
<ClCompile Include="Input\raw_mouse_handler.cpp" />
<ClCompile Include="Input\ps_move_handler.cpp" />
<ClCompile Include="Input\sdl_pad_handler.cpp" />
<ClCompile Include="Input\skateboard_pad_handler.cpp" />
<ClCompile Include="main.cpp" />
@ -213,6 +216,9 @@
<ClCompile Include="QTGeneratedFiles\Debug\moc_camera_settings_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_ps_move_tracker_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_cg_disasm_window.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
@ -486,6 +492,9 @@
<ClCompile Include="QTGeneratedFiles\Release\moc_camera_settings_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_ps_move_tracker_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_cg_disasm_window.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
@ -741,6 +750,7 @@
<ClCompile Include="rpcs3qt\breakpoint_list.cpp" />
<ClCompile Include="rpcs3qt\call_stack_list.cpp" />
<ClCompile Include="rpcs3qt\camera_settings_dialog.cpp" />
<ClCompile Include="rpcs3qt\ps_move_tracker_dialog.cpp" />
<ClCompile Include="rpcs3qt\cheat_manager.cpp" />
<ClCompile Include="rpcs3qt\config_adapter.cpp" />
<ClCompile Include="rpcs3qt\config_checker.cpp" />
@ -961,8 +971,11 @@
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath);$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\moc.exe;%(FullPath);$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
</CustomBuild>
<ClInclude Include="Input\ps_move_config.h" />
<ClInclude Include="Input\ps_move_tracker.h" />
<ClInclude Include="Input\raw_mouse_config.h" />
<ClInclude Include="Input\raw_mouse_handler.h" />
<ClInclude Include="Input\ps_move_handler.h" />
<ClInclude Include="Input\sdl_pad_handler.h" />
<ClInclude Include="Input\skateboard_pad_handler.h" />
<ClInclude Include="main_application.h" />
@ -1022,6 +1035,7 @@
<ClInclude Include="module_verifier.hpp" />
<ClInclude Include="QTGeneratedFiles\ui_about_dialog.h" />
<ClInclude Include="QTGeneratedFiles\ui_camera_settings_dialog.h" />
<ClInclude Include="QTGeneratedFiles\ui_ps_move_tracker_dialog.h" />
<ClInclude Include="QTGeneratedFiles\ui_main_window.h" />
<ClInclude Include="QTGeneratedFiles\ui_pad_led_settings_dialog.h" />
<ClInclude Include="QTGeneratedFiles\ui_pad_motion_settings_dialog.h" />
@ -1093,6 +1107,16 @@
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia"</Command>
</CustomBuild>
<CustomBuild Include="rpcs3qt\ps_move_tracker_dialog.h">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia"</Command>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Moc%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia"</Command>
</CustomBuild>
<ClInclude Include="rpcs3qt\category.h" />
<ClInclude Include="rpcs3qt\config_adapter.h" />
<CustomBuild Include="rpcs3qt\config_checker.h">
@ -1962,6 +1986,16 @@
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
</CustomBuild>
<CustomBuild Include="rpcs3qt\ps_move_tracker_dialog.ui">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Uic%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\QTGeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Uic%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
</CustomBuild>
<CustomBuild Include="rpcs3qt\pad_motion_settings_dialog.ui">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Uic%27ing %(Identity)...</Message>

View File

@ -184,6 +184,12 @@
<Filter Include="buildfiles\msvc">
<UniqueIdentifier>{5cace00d-92fc-4b03-82a2-72706cd04e84}</UniqueIdentifier>
</Filter>
<Filter Include="Gui\ps_move_tracker_dialog">
<UniqueIdentifier>{4a270c26-740b-46e8-880c-bb22e708b897}</UniqueIdentifier>
</Filter>
<Filter Include="Io\Move">
<UniqueIdentifier>{f8a98f7b-dc23-47c0-8a5f-d0b76eaf0df5}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
@ -879,6 +885,15 @@
<ClCompile Include="QTGeneratedFiles\Release\moc_camera_settings_dialog.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\ps_move_tracker_dialog.cpp">
<Filter>Gui\ps_move_tracker_dialog</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_ps_move_tracker_dialog.cpp">
<Filter>Generated Files\Debug</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_ps_move_tracker_dialog.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\shortcut_utils.cpp">
<Filter>Gui\utils</Filter>
</ClCompile>
@ -1119,6 +1134,15 @@
<ClCompile Include="rpcs3qt\vfs_tool_dialog.cpp">
<Filter>Gui\vfs</Filter>
</ClCompile>
<ClCompile Include="Input\ps_move_handler.cpp">
<Filter>Io\Move</Filter>
</ClCompile>
<ClCompile Include="Input\ps_move_tracker.cpp">
<Filter>Io\Move</Filter>
</ClCompile>
<ClCompile Include="Input\ps_move_config.cpp">
<Filter>Io\Move</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Input\ds4_pad_handler.h">
@ -1253,6 +1277,9 @@
<ClInclude Include="QTGeneratedFiles\ui_camera_settings_dialog.h">
<Filter>Generated Files</Filter>
</ClInclude>
<ClInclude Include="QTGeneratedFiles\ui_ps_move_tracker_dialog.h">
<Filter>Generated Files</Filter>
</ClInclude>
<ClInclude Include="rpcs3qt\shortcut_utils.h">
<Filter>Gui\utils</Filter>
</ClInclude>
@ -1307,6 +1334,15 @@
<ClInclude Include="Input\raw_mouse_config.h">
<Filter>Io\raw</Filter>
</ClInclude>
<ClInclude Include="Input\ps_move_handler.h">
<Filter>Io\Move</Filter>
</ClInclude>
<ClInclude Include="Input\ps_move_tracker.h">
<Filter>Io\Move</Filter>
</ClInclude>
<ClInclude Include="Input\ps_move_config.h">
<Filter>Io\Move</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<CustomBuild Include="debug\moc_predefs.h.cbt">
@ -1546,6 +1582,12 @@
<CustomBuild Include="rpcs3qt\camera_settings_dialog.h">
<Filter>Gui\settings</Filter>
</CustomBuild>
<CustomBuild Include="rpcs3qt\ps_move_tracker_dialog.ui">
<Filter>Form Files</Filter>
</CustomBuild>
<CustomBuild Include="rpcs3qt\ps_move_tracker_dialog.h">
<Filter>Gui\ps_move_tracker_dialog</Filter>
</CustomBuild>
<CustomBuild Include="rpcs3qt\qt_music_handler.h">
<Filter>Io\music</Filter>
</CustomBuild>

View File

@ -1150,6 +1150,7 @@ QString emu_settings::GetLocalizedSetting(const QString& original, emu_settings_
switch (static_cast<move_handler>(index))
{
case move_handler::null: return tr("Null", "Move handler");
case move_handler::real: return tr("Real", "Move handler");
case move_handler::fake: return tr("Fake", "Move handler");
case move_handler::mouse: return tr("Mouse", "Move handler");
case move_handler::raw_mouse: return tr("Raw Mouse", "Move handler");

View File

@ -84,6 +84,10 @@ emulated_pad_settings_dialog::emulated_pad_settings_dialog(pad_type type, QWidge
setWindowTitle(tr("Configure Emulated USIO"));
add_tabs<usio_btn>(tabs);
break;
case emulated_pad_settings_dialog::pad_type::gem:
setWindowTitle(tr("Configure Emulated PS Move (Real)"));
add_tabs<gem_btn>(tabs);
break;
case emulated_pad_settings_dialog::pad_type::ds3gem:
setWindowTitle(tr("Configure Emulated PS Move (Fake)"));
add_tabs<gem_btn>(tabs);
@ -113,12 +117,7 @@ void emulated_pad_settings_dialog::add_tabs(QTabWidget* tabs)
ensure(!!tabs);
constexpr u32 max_items_per_column = 6;
int rows = static_cast<int>(T::count);
for (u32 cols = 1; utils::aligned_div(static_cast<u32>(T::count), cols) > max_items_per_column;)
{
rows = utils::aligned_div(static_cast<u32>(T::count), ++cols);
}
int count = static_cast<int>(T::count);
usz players = 0;
switch (m_type)
@ -135,8 +134,16 @@ void emulated_pad_settings_dialog::add_tabs(QTabWidget* tabs)
case pad_type::usio:
players = g_cfg_usio.players.size();
break;
case pad_type::gem:
players = g_cfg_gem_real.players.size();
// Ignore x and y axis
static_assert(static_cast<int>(gem_btn::y_axis) == static_cast<int>(gem_btn::count) - 1);
static_assert(static_cast<int>(gem_btn::x_axis) == static_cast<int>(gem_btn::count) - 2);
count -= 2;
break;
case pad_type::ds3gem:
players = g_cfg_gem.players.size();
players = g_cfg_gem_fake.players.size();
break;
case pad_type::guncon3:
players = g_cfg_guncon3.players.size();
@ -149,6 +156,13 @@ void emulated_pad_settings_dialog::add_tabs(QTabWidget* tabs)
break;
}
int rows = count;
for (u32 cols = 1; utils::aligned_div(static_cast<u32>(count), cols) > max_items_per_column;)
{
rows = utils::aligned_div(static_cast<u32>(count), ++cols);
}
m_combos.resize(players);
for (usz player = 0; player < players; player++)
@ -156,7 +170,7 @@ void emulated_pad_settings_dialog::add_tabs(QTabWidget* tabs)
QWidget* widget = new QWidget(this);
QGridLayout* grid_layout = new QGridLayout(this);
for (int i = 0, row = 0, col = 0; i < static_cast<int>(T::count); i++, row++)
for (int i = 0, row = 0, col = 0; i < count; i++, row++)
{
const T id = static_cast<T>(i);
const QString name = QString::fromStdString(fmt::format("%s", id));
@ -201,8 +215,11 @@ void emulated_pad_settings_dialog::add_tabs(QTabWidget* tabs)
case pad_type::usio:
saved_btn_id = ::at32(g_cfg_usio.players, player)->get_pad_button(static_cast<usio_btn>(id));
break;
case pad_type::gem:
saved_btn_id = ::at32(g_cfg_gem_real.players, player)->get_pad_button(static_cast<gem_btn>(id));
break;
case pad_type::ds3gem:
saved_btn_id = ::at32(g_cfg_gem.players, player)->get_pad_button(static_cast<gem_btn>(id));
saved_btn_id = ::at32(g_cfg_gem_fake.players, player)->get_pad_button(static_cast<gem_btn>(id));
break;
case pad_type::guncon3:
saved_btn_id = ::at32(g_cfg_guncon3.players, player)->get_pad_button(static_cast<guncon3_btn>(id));
@ -242,8 +259,11 @@ void emulated_pad_settings_dialog::add_tabs(QTabWidget* tabs)
case pad_type::usio:
::at32(g_cfg_usio.players, player)->set_button(static_cast<usio_btn>(id), btn_id);
break;
case pad_type::gem:
::at32(g_cfg_gem_real.players, player)->set_button(static_cast<gem_btn>(id), btn_id);
break;
case pad_type::ds3gem:
::at32(g_cfg_gem.players, player)->set_button(static_cast<gem_btn>(id), btn_id);
::at32(g_cfg_gem_fake.players, player)->set_button(static_cast<gem_btn>(id), btn_id);
break;
case pad_type::guncon3:
::at32(g_cfg_guncon3.players, player)->set_button(static_cast<guncon3_btn>(id), btn_id);
@ -302,12 +322,18 @@ void emulated_pad_settings_dialog::load_config()
cfg_log.notice("Could not load usio config. Using defaults.");
}
break;
case emulated_pad_settings_dialog::pad_type::ds3gem:
if (!g_cfg_gem.load())
case emulated_pad_settings_dialog::pad_type::gem:
if (!g_cfg_gem_real.load())
{
cfg_log.notice("Could not load gem config. Using defaults.");
}
break;
case emulated_pad_settings_dialog::pad_type::ds3gem:
if (!g_cfg_gem_fake.load())
{
cfg_log.notice("Could not load fake gem config. Using defaults.");
}
break;
case emulated_pad_settings_dialog::pad_type::guncon3:
if (!g_cfg_guncon3.load())
{
@ -345,8 +371,11 @@ void emulated_pad_settings_dialog::save_config()
case emulated_pad_settings_dialog::pad_type::usio:
g_cfg_usio.save();
break;
case emulated_pad_settings_dialog::pad_type::gem:
g_cfg_gem_real.save();
break;
case emulated_pad_settings_dialog::pad_type::ds3gem:
g_cfg_gem.save();
g_cfg_gem_fake.save();
break;
case emulated_pad_settings_dialog::pad_type::guncon3:
g_cfg_guncon3.save();
@ -376,8 +405,11 @@ void emulated_pad_settings_dialog::reset_config()
case emulated_pad_settings_dialog::pad_type::usio:
g_cfg_usio.from_default();
break;
case emulated_pad_settings_dialog::pad_type::gem:
g_cfg_gem_real.from_default();
break;
case emulated_pad_settings_dialog::pad_type::ds3gem:
g_cfg_gem.from_default();
g_cfg_gem_fake.from_default();
break;
case emulated_pad_settings_dialog::pad_type::guncon3:
g_cfg_guncon3.from_default();
@ -416,8 +448,11 @@ void emulated_pad_settings_dialog::reset_config()
case pad_type::usio:
def_btn_id = ::at32(g_cfg_usio.players, player)->default_pad_button(static_cast<usio_btn>(data.toInt()));
break;
case pad_type::gem:
def_btn_id = ::at32(g_cfg_gem_real.players, player)->default_pad_button(static_cast<gem_btn>(data.toInt()));
break;
case pad_type::ds3gem:
def_btn_id = ::at32(g_cfg_gem.players, player)->default_pad_button(static_cast<gem_btn>(data.toInt()));
def_btn_id = ::at32(g_cfg_gem_fake.players, player)->default_pad_button(static_cast<gem_btn>(data.toInt()));
break;
case pad_type::guncon3:
def_btn_id = ::at32(g_cfg_guncon3.players, player)->default_pad_button(static_cast<guncon3_btn>(data.toInt()));

View File

@ -19,6 +19,7 @@ public:
turntable,
ghltar,
usio,
gem,
ds3gem,
guncon3,
topshotelite,

View File

@ -30,6 +30,7 @@
#include "gui_settings.h"
#include "input_dialog.h"
#include "camera_settings_dialog.h"
#include "ps_move_tracker_dialog.h"
#include "ipc_settings_dialog.h"
#include "shortcut_utils.h"
#include "config_checker.h"
@ -1990,6 +1991,7 @@ void main_window::OnEmuStop()
ui->actionManage_Users->setEnabled(true);
ui->confCamerasAct->setEnabled(true);
ui->actionPS_Move_Tracker->setEnabled(true);
// Refresh game list in order to update time played
if (m_game_list_frame && m_is_list_mode)
@ -2032,6 +2034,7 @@ void main_window::OnEmuReady() const
ui->actionManage_Users->setEnabled(false);
ui->confCamerasAct->setEnabled(false);
ui->actionPS_Move_Tracker->setEnabled(false);
ui->batchRemoveShaderCachesAct->setEnabled(false);
ui->batchRemovePPUCachesAct->setEnabled(false);
@ -2808,6 +2811,12 @@ void main_window::CreateConnects()
dlg->show();
});
connect(ui->confPSMoveAct, &QAction::triggered, this, [this]
{
emulated_pad_settings_dialog* dlg = new emulated_pad_settings_dialog(emulated_pad_settings_dialog::pad_type::gem, this);
dlg->show();
});
connect(ui->confGunCon3Act, &QAction::triggered, this, [this]
{
emulated_pad_settings_dialog* dlg = new emulated_pad_settings_dialog(emulated_pad_settings_dialog::pad_type::guncon3, this);
@ -2961,6 +2970,12 @@ void main_window::CreateConnects()
viewer->show_log();
});
connect(ui->actionPS_Move_Tracker, &QAction::triggered, this, [this]
{
ps_move_tracker_dialog* dlg = new ps_move_tracker_dialog(this);
dlg->open();
});
connect(ui->toolsCheckConfigAct, &QAction::triggered, this, [this]
{
const QString path_last_cfg = m_gui_settings->GetValue(gui::fd_cfg_check).toString();

View File

@ -242,6 +242,7 @@
<addaction name="confGHLtarAct"/>
<addaction name="confTurntableAct"/>
<addaction name="confUSIOAct"/>
<addaction name="confPSMoveAct"/>
<addaction name="confPSMoveDS3Act"/>
<addaction name="confGunCon3Act"/>
<addaction name="confTopShotEliteAct"/>
@ -261,6 +262,7 @@
<addaction name="menuMice"/>
<addaction name="menuEmulatedPads"/>
<addaction name="confCamerasAct"/>
<addaction name="actionPS_Move_Tracker"/>
<addaction name="confAudioAct"/>
<addaction name="confIOAct"/>
<addaction name="confSystemAct"/>
@ -1390,6 +1392,16 @@
<string>VFS Tool</string>
</property>
</action>
<action name="actionPS_Move_Tracker">
<property name="text">
<string>PS Move Tracker</string>
</property>
</action>
<action name="confPSMoveAct">
<property name="text">
<string>PS Move</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>

View File

@ -1416,6 +1416,7 @@ void pad_settings_dialog::ChangeHandler()
break;
case pad_handler::keyboard: m_description = tooltips.gamepad_settings.keyboard; break;
case pad_handler::skateboard: m_description = tooltips.gamepad_settings.skateboard; break;
case pad_handler::move: m_description = tooltips.gamepad_settings.move; break;
#ifdef _WIN32
case pad_handler::xinput: m_description = tooltips.gamepad_settings.xinput; break;
case pad_handler::mm: m_description = tooltips.gamepad_settings.mmjoy; break;
@ -1486,6 +1487,7 @@ void pad_settings_dialog::ChangeHandler()
case pad_handler::ds4:
case pad_handler::dualsense:
case pad_handler::skateboard:
case pad_handler::move:
{
const QString name_string = qstr(m_handler->name_string());
for (usz i = 1; i <= m_handler->max_devices(); i++) // Controllers 1-n in GUI
@ -1957,6 +1959,7 @@ QString pad_settings_dialog::GetLocalizedPadHandler(const QString& original, pad
case pad_handler::ds4: return tr("DualShock 4");
case pad_handler::dualsense: return tr("DualSense");
case pad_handler::skateboard: return tr("Skateboard");
case pad_handler::move: return tr("PS Move");
#ifdef _WIN32
case pad_handler::xinput: return tr("XInput");
case pad_handler::mm: return tr("MMJoystick");

View File

@ -0,0 +1,595 @@
#include "stdafx.h"
#include "ps_move_tracker_dialog.h"
#include "ui_ps_move_tracker_dialog.h"
#include "Emu/Cell/Modules/cellCamera.h"
#include "qt_camera_handler.h"
#include "Input/ps_move_handler.h"
#include "Input/ps_move_config.h"
#include "Input/ps_move_tracker.h"
#include <QCheckBox>
#include <QComboBox>
#include <QImage>
#include <QPainter>
#include <QPushButton>
#include <QSlider>
LOG_CHANNEL(ps_move);
extern u32 get_buffer_size_by_format(s32, s32, s32);
static constexpr bool tie_hue_to_color = true;
static constexpr int radius_range = 1000;
static const constexpr f64 min_radius_conversion = radius_range / g_cfg_move.min_radius.max;
static const constexpr f64 max_radius_conversion = radius_range / g_cfg_move.max_radius.max;
ps_move_tracker_dialog::ps_move_tracker_dialog(QWidget* parent)
: QDialog(parent)
, ui(new Ui::ps_move_tracker_dialog)
{
ui->setupUi(this);
if (!g_cfg_move.load())
{
ps_move.notice("Could not load PS Move config. Using defaults.");
}
setAttribute(Qt::WA_DeleteOnClose);
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton* button)
{
if (button == ui->buttonBox->button(QDialogButtonBox::Save))
{
g_cfg_move.save();
}
else if (button == ui->buttonBox->button(QDialogButtonBox::Apply))
{
g_cfg_move.save();
}
else if (button == ui->buttonBox->button(QDialogButtonBox::Close))
{
if (!g_cfg_move.load())
{
ps_move.notice("Could not load PS Move config. Using defaults.");
}
}
});
m_format = CELL_CAMERA_RGBA;
ui->inputFormatCombo->addItem(tr("RGBA"), static_cast<int>(CELL_CAMERA_RGBA));
ui->inputFormatCombo->addItem(tr("RAW8"), static_cast<int>(CELL_CAMERA_RAW8));
ui->inputFormatCombo->setCurrentIndex(ui->inputFormatCombo->findData(m_format));
connect(ui->inputFormatCombo, &QComboBox::currentIndexChanged, this, [this](int index)
{
if (index < 0) return;
if (const auto qvar = ui->inputFormatCombo->currentData(); qvar.canConvert<int>())
{
m_format = qvar.toInt();
reset_camera();
}
});
ui->viewCombo->addItem(tr("Image"), static_cast<int>(view_mode::image));
ui->viewCombo->addItem(tr("Grayscale"), static_cast<int>(view_mode::grayscale));
ui->viewCombo->addItem(tr("HSV Hue"), static_cast<int>(view_mode::hsv_hue));
ui->viewCombo->addItem(tr("HSV Saturation"), static_cast<int>(view_mode::hsv_saturation));
ui->viewCombo->addItem(tr("HSV Value"), static_cast<int>(view_mode::hsv_value));
ui->viewCombo->addItem(tr("Binary"), static_cast<int>(view_mode::binary));
ui->viewCombo->addItem(tr("Contours"), static_cast<int>(view_mode::contours));
ui->viewCombo->setCurrentIndex(ui->viewCombo->findData(static_cast<int>(m_view_mode)));
connect(ui->viewCombo, &QComboBox::currentIndexChanged, this, [this](int index)
{
if (index < 0) return;
if (const auto qvar = ui->viewCombo->currentData(); qvar.canConvert<int>())
{
m_view_mode = static_cast<view_mode>(qvar.toInt());
}
});
ui->histoCombo->addItem(tr("Hues"), static_cast<int>(histo_mode::unfiltered_hues));
ui->histoCombo->setCurrentIndex(ui->viewCombo->findData(static_cast<int>(m_histo_mode)));
connect(ui->histoCombo, &QComboBox::currentIndexChanged, this, [this](int index)
{
if (index < 0) return;
if (const auto qvar = ui->histoCombo->currentData(); qvar.canConvert<int>())
{
m_histo_mode = static_cast<histo_mode>(qvar.toInt());
}
});
connect(ui->hueSlider, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
const u16 hue = std::clamp<u16>(value, config->hue.min, config->hue.max);
config->hue.set(hue);
update_hue();
});
ui->hueSlider->setRange(g_cfg_move.move1.hue.min, g_cfg_move.move1.hue.max);
connect(ui->hueThresholdSlider, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
const u16 hue_threshold = std::clamp<u16>(value, config->hue_threshold.min, config->hue_threshold.max);
config->hue_threshold.set(hue_threshold);
update_hue_threshold();
});
ui->hueThresholdSlider->setRange(g_cfg_move.move1.hue_threshold.min, g_cfg_move.move1.hue_threshold.max);
connect(ui->saturationThresholdSlider, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
const u16 saturation_threshold = std::clamp<u16>(value, config->saturation_threshold.min, config->saturation_threshold.max);
config->saturation_threshold.set(saturation_threshold);
update_saturation_threshold();
});
ui->saturationThresholdSlider->setRange(g_cfg_move.move1.saturation_threshold.min, g_cfg_move.move1.saturation_threshold.max);
connect(ui->minRadiusSlider, &QSlider::valueChanged, this, [this](int value)
{
const f32 min_radius = std::clamp(value / min_radius_conversion, g_cfg_move.min_radius.min, g_cfg_move.min_radius.max);
g_cfg_move.min_radius.set(min_radius);
update_min_radius();
});
ui->minRadiusSlider->setRange(0, radius_range);
connect(ui->maxRadiusSlider, &QSlider::valueChanged, this, [this](int value)
{
const f32 max_radius = std::clamp(value / max_radius_conversion, g_cfg_move.max_radius.min, g_cfg_move.max_radius.max);
g_cfg_move.max_radius.set(max_radius);
update_max_radius();
});
ui->maxRadiusSlider->setRange(0, radius_range);
connect(ui->colorSliderR, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
config->r.set(std::clamp<u8>(value, config->r.min, config->r.max));
update_color();
});
connect(ui->colorSliderG, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
config->g.set(std::clamp<u8>(value, config->g.min, config->g.max));
update_color();
});
connect(ui->colorSliderB, &QSlider::valueChanged, this, [this](int value)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
config->b.set(std::clamp<u8>(value, config->b.min, config->b.max));
update_color();
});
ui->colorSliderR->setRange(g_cfg_move.move1.r.min, g_cfg_move.move1.r.max);
ui->colorSliderG->setRange(g_cfg_move.move1.g.min, g_cfg_move.move1.g.max);
ui->colorSliderB->setRange(g_cfg_move.move1.b.min, g_cfg_move.move1.b.max);
connect(ui->filterSmallContoursBox, &QCheckBox::toggled, [this](bool checked)
{
m_filter_small_contours = checked;
});
ui->filterSmallContoursBox->setChecked(m_filter_small_contours);
connect(ui->freezeFrameBox, &QCheckBox::toggled, [this](bool checked)
{
m_freeze_frame = checked;
});
ui->freezeFrameBox->setChecked(m_freeze_frame);
connect(ui->showAllContoursBox, &QCheckBox::toggled, [this](bool checked)
{
m_show_all_contours = checked;
});
ui->showAllContoursBox->setChecked(m_show_all_contours);
connect(ui->drawContoursBox, &QCheckBox::toggled, [this](bool checked)
{
m_draw_contours = checked;
});
ui->drawContoursBox->setChecked(m_draw_contours);
connect(ui->drawOverlaysBox, &QCheckBox::toggled, [this](bool checked)
{
m_draw_overlays = checked;
});
ui->drawOverlaysBox->setChecked(m_draw_overlays);
for (u32 index = 0; index < CELL_GEM_MAX_NUM; index++)
{
ui->comboSelectDevice->addItem(tr("PS Move #%0").arg(index + 1), index);
}
ui->comboSelectDevice->setCurrentIndex(ui->comboSelectDevice->findData(m_index));
connect(ui->comboSelectDevice, &QComboBox::currentIndexChanged, this, [this](int index)
{
if (index < 0) return;
if (const auto qvar = ui->comboSelectDevice->currentData(); qvar.canConvert<int>())
{
m_index = qvar.toInt();
update_color(true);
update_hue(true);
update_hue_threshold(true);
update_saturation_threshold(true);
}
});
m_ps_move_tracker = std::make_unique<ps_move_tracker<true>>();
m_update_timer = new QTimer(this);
connect(m_update_timer, &QTimer::timeout, this, [this]()
{
std::lock_guard lock(m_image_mutex);
if (m_image.isNull() || m_histogram.isNull())
return;
ui->imageLabel->setPixmap(m_image);
ui->histogramLabel->setPixmap(m_histogram);
});
reset_camera();
m_input_thread = std::make_unique<named_thread<pad_thread>>(thread(), window(), "");
while (!pad::g_started) QApplication::processEvents();
adjustSize();
update_color(true);
update_hue(true);
update_hue_threshold(true);
update_saturation_threshold(true);
update_min_radius(true);
update_max_radius(true);
}
ps_move_tracker_dialog::~ps_move_tracker_dialog()
{
m_update_timer->stop();
if (m_tracker_thread)
{
m_stop_threads = 1;
m_tracker_thread->wait();
}
if (m_camera_handler)
{
m_camera_handler->close_camera();
}
if (m_input_thread)
{
auto& thread = *m_input_thread;
thread = thread_state::aborting;
thread();
m_input_thread.reset();
}
}
void ps_move_tracker_dialog::update_color(bool update_sliders)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
ui->colorGb->setTitle(tr("Color: R=%0, G=%1, B=%2").arg(config->r.get()).arg(config->g.get()).arg(config->b.get()));
if (update_sliders)
{
ui->colorSliderR->setValue(config->r.get());
ui->colorSliderG->setValue(config->g.get());
ui->colorSliderB->setValue(config->b.get());
}
else if (tie_hue_to_color)
{
const auto [hue, saturation, value] = ps_move_tracker<true>::rgb_to_hsv(config->r.get() / 255.0f, config->g.get() / 255.0f, config->b.get() / 255.0f);
config->hue.set(std::clamp<u32>(hue, config->hue.min, config->hue.max));
update_hue(true);
}
if (!m_input_thread)
{
return;
}
std::lock_guard lock(pad::g_pad_mutex);
auto& handlers = m_input_thread->get_handlers();
if (auto it = handlers.find(pad_handler::move); it != handlers.end())
{
for (auto& binding : it->second->bindings())
{
if (binding.device)
{
binding.device->color_override_active = true;
binding.device->color_override.r = config->r.get();
binding.device->color_override.g = config->g.get();
binding.device->color_override.b = config->b.get();
}
}
}
}
void ps_move_tracker_dialog::update_hue(bool update_slider)
{
const u32 hue = ::at32(g_cfg_move.move, m_index)->hue.get();
ui->hueGb->setTitle(tr("Hue: %0").arg(hue));
if (update_slider)
{
ui->hueSlider->setValue(hue);
}
else if (tie_hue_to_color)
{
cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
const auto [r, g, b] = ps_move_tracker<true>::hsv_to_rgb(hue, 1.0f, 1.0f);
config->r.set(r / 100);
config->g.set(g / 100);
config->b.set(b / 100);
update_color(true);
}
}
void ps_move_tracker_dialog::update_hue_threshold(bool update_slider)
{
const u32 hue_threshold = ::at32(g_cfg_move.move, m_index)->hue_threshold.get();
ui->hueThresholdGb->setTitle(tr("Hue Threshold: %0").arg(hue_threshold));
if (update_slider)
{
ui->hueThresholdSlider->setValue(hue_threshold);
}
}
void ps_move_tracker_dialog::update_saturation_threshold(bool update_slider)
{
const u32 saturation_threshold = ::at32(g_cfg_move.move, m_index)->saturation_threshold.get();
ui->saturationThresholdGb->setTitle(tr("Saturation Threshold: %0").arg(saturation_threshold));
if (update_slider)
{
ui->saturationThresholdSlider->setValue(saturation_threshold);
}
}
void ps_move_tracker_dialog::update_min_radius(bool update_slider)
{
const f32 min_radius = std::clamp(g_cfg_move.min_radius / min_radius_conversion, g_cfg_move.min_radius.min, g_cfg_move.min_radius.max);
ui->minRadiusGb->setTitle(tr("Min Radius: %0 %").arg(min_radius));
if (update_slider)
{
ui->minRadiusSlider->setValue(g_cfg_move.min_radius * min_radius_conversion);
}
}
void ps_move_tracker_dialog::update_max_radius(bool update_slider)
{
const f32 max_radius = std::clamp(g_cfg_move.max_radius / max_radius_conversion, g_cfg_move.max_radius.min, g_cfg_move.max_radius.max);
ui->maxRadiusGb->setTitle(tr("Max Radius: %0 %").arg(max_radius));
if (update_slider)
{
ui->maxRadiusSlider->setValue(g_cfg_move.max_radius * max_radius_conversion);
}
}
void ps_move_tracker_dialog::reset_camera()
{
m_update_timer->stop();
if (m_tracker_thread)
{
m_stop_threads = 1;
m_tracker_thread->wait();
m_stop_threads = 0;
}
std::lock_guard camera_lock(m_camera_handler_mutex);
const u64 size = get_buffer_size_by_format(m_format, width, height);
m_image_data_frozen.resize(size);
m_image_data.resize(size);
m_frame_number = 0;
m_camera_handler = std::make_unique<qt_camera_handler>();
m_camera_handler->set_resolution(width, height);
m_camera_handler->set_format(m_format, size);
m_camera_handler->set_mirrored(true);
m_camera_handler->open_camera();
m_camera_handler->start_camera();
m_update_timer->start(1000 / 60);
m_tracker_thread.reset(QThread::create([this]()
{
while (!m_stop_threads)
{
process_camera_frame();
}
}));
m_tracker_thread->start();
}
void ps_move_tracker_dialog::process_camera_frame()
{
std::lock_guard camera_lock(m_camera_handler_mutex);
if (!m_camera_handler)
{
// Wait some time
std::this_thread::sleep_for(100us);
return;
}
u32 width = 0;
u32 height = 0;
u64 frame_number = 0;
u64 bytes_read = 0;
const camera_handler_base::camera_handler_state state = m_camera_handler->get_image(m_image_data.data(), m_image_data.size(), width, height, frame_number, bytes_read);
if (state != camera_handler_base::camera_handler_state::running || frame_number <= m_frame_number)
{
// Wait some time
std::this_thread::sleep_for(100us);
return;
}
m_frame_number = frame_number;
if (m_frame_frozen != m_freeze_frame)
{
m_frame_frozen = m_freeze_frame;
if (m_frame_frozen)
{
std::memcpy(m_image_data_frozen.data(), m_image_data.data(), m_image_data.size());
}
}
for (u32 index = 0; index < CELL_GEM_MAX_NUM; index++)
{
const cfg_ps_move* config = g_cfg_move.move[index];
m_ps_move_tracker->set_active(index, m_index == index);
m_ps_move_tracker->set_hue(index, config->hue);
m_ps_move_tracker->set_hue_threshold(index, config->hue_threshold);
m_ps_move_tracker->set_saturation_threshold(index, config->saturation_threshold);
}
m_ps_move_tracker->set_image_data(m_frame_frozen ? m_image_data_frozen.data() : m_image_data.data(), m_image_data.size(), width, height, m_camera_handler->format());
m_ps_move_tracker->set_min_radius(static_cast<f32>(g_cfg_move.min_radius.get() / g_cfg_move.min_radius.max));
m_ps_move_tracker->set_max_radius(static_cast<f32>(g_cfg_move.max_radius.get() / g_cfg_move.max_radius.max));
m_ps_move_tracker->set_filter_small_contours(m_filter_small_contours);
m_ps_move_tracker->set_show_all_contours(m_show_all_contours);
m_ps_move_tracker->set_draw_contours(m_draw_contours);
m_ps_move_tracker->set_draw_overlays(m_draw_overlays);
m_ps_move_tracker->process_image();
const std::vector<u8>* result = nullptr;
QImage::Format format = QImage::Format::Format_Invalid;
qsizetype bytes_per_line = 0;
switch (m_view_mode)
{
case view_mode::image:
{
result = &m_ps_move_tracker->rgba();
format = QImage::Format::Format_RGBA8888;
bytes_per_line = width * 4;
break;
}
case view_mode::grayscale:
{
result = &m_ps_move_tracker->gray();
format = QImage::Format::Format_Grayscale8;
bytes_per_line = width;
break;
}
case view_mode::hsv_hue:
case view_mode::hsv_saturation:
case view_mode::hsv_value:
{
const int index = static_cast<int>(m_view_mode) - static_cast<int>(view_mode::hsv_hue);
const std::vector<u8>& hsv = m_ps_move_tracker->hsv();
static std::vector<u8> hsv_single;
hsv_single.resize(hsv.size() / 3);
for (int i = 0; i < static_cast<int>(hsv_single.size()); i++)
{
hsv_single[i] = hsv[i * 3 + index];
}
result = &hsv_single;
format = QImage::Format::Format_Grayscale8;
bytes_per_line = width;
break;
}
case view_mode::binary:
{
result = &m_ps_move_tracker->binary(m_index);
format = QImage::Format::Format_Grayscale8;
bytes_per_line = width;
break;
}
case view_mode::contours:
{
result = &m_ps_move_tracker->rgba_contours();
format = QImage::Format::Format_RGBA8888;
bytes_per_line = width * 4;
break;
}
}
QPixmap histogram;
switch (m_histo_mode)
{
case histo_mode::unfiltered_hues:
{
histogram = get_histogram(m_ps_move_tracker->hues(), false);
break;
}
}
const QImage image(result->data(), width, height, bytes_per_line, format, nullptr, nullptr);
std::lock_guard lock(m_image_mutex);
m_image = QPixmap::fromImage(image);
m_histogram = std::move(histogram);
}
QPixmap ps_move_tracker_dialog::get_histogram(const std::array<u32, 360>& hues, bool ignore_zero) const
{
// Create image
const int height = ui->histogramLabel->height();
static QPixmap background = [&]()
{
// Paint background
QPixmap pxmap(static_cast<int>(hues.size()), height);
pxmap.fill(Qt::white);
return pxmap;
}();
QPixmap histo = background;
QPainter painter(&histo);
const cfg_ps_move* config = ::at32(g_cfg_move.move, m_index);
const u16 hue = config->hue;
const u16 hue_threshold = config->hue_threshold;
const int min_hue = hue - hue_threshold;
const int max_hue = hue + hue_threshold;
// Paint hue threshold
painter.fillRect(min_hue, 0, hue_threshold * 2 + 1, histo.height(), Qt::lightGray);
if (min_hue < 0)
{
painter.fillRect(min_hue + 360, 0, hue_threshold * 2 + 1, histo.height(), Qt::lightGray);
}
else if (max_hue >= 360)
{
painter.fillRect(0, 0, max_hue - 360, histo.height(), Qt::lightGray);
}
// Paint target hue
const auto [r, g, b] = ps_move_tracker<true>::hsv_to_rgb(hue, 1.0f, 1.0f);
painter.setPen(QColor(r, g, b));
painter.drawLine(hue, 0, hue, histo.height() - 1);
// Paint histogram
painter.setPen(Qt::black);
const u32 zero_offset = (ignore_zero ? 1 : 0);
const auto max_elem = std::max_element(hues.begin() + zero_offset, hues.end());
const u32 max_value = max_elem != hues.end() ? *max_elem : 0u;
if (!max_value)
return histo;
for (int i = zero_offset; i < static_cast<int>(hues.size()); i++)
{
const int bar_height = (hues[i] / static_cast<float>(max_value)) * height;
if (bar_height <= 0) continue;
painter.drawLine(i, height - 1, i, height - bar_height);
}
return histo;
}

View File

@ -0,0 +1,89 @@
#pragma once
#include "Utilities/mutex.h"
#include "Utilities/Thread.h"
#include "Input/pad_thread.h"
#include "Emu/Cell/Modules/cellGem.h"
#include <QDialog>
#include <QPixmap>
#include <QTimer>
#include <QThread>
class qt_camera_handler;
class pad_thread;
template <bool> class ps_move_tracker;
namespace Ui
{
class ps_move_tracker_dialog;
}
class ps_move_tracker_dialog : public QDialog
{
Q_OBJECT
public:
ps_move_tracker_dialog(QWidget* parent = nullptr);
virtual ~ps_move_tracker_dialog();
private:
void update_color(bool update_sliders = false);
void update_hue(bool update_slider = false);
void update_hue_threshold(bool update_slider = false);
void update_saturation_threshold(bool update_slider = false);
void update_min_radius(bool update_slider = false);
void update_max_radius(bool update_slider = false);
void reset_camera();
void process_camera_frame();
QPixmap get_histogram(const std::array<u32, 360>& hues, bool ignore_zero) const;
enum class view_mode
{
image,
grayscale,
hsv_hue,
hsv_saturation,
hsv_value,
binary,
contours
};
view_mode m_view_mode = view_mode::contours;
enum class histo_mode
{
unfiltered_hues
};
histo_mode m_histo_mode = histo_mode::unfiltered_hues;
std::unique_ptr<Ui::ps_move_tracker_dialog> ui;
std::unique_ptr<qt_camera_handler> m_camera_handler;
std::unique_ptr<ps_move_tracker<true>> m_ps_move_tracker;
std::vector<u8> m_image_data_frozen;
std::vector<u8> m_image_data;
std::array<u32, 360> m_hues{};
static constexpr s32 width = 640;
static constexpr s32 height = 480;
s32 m_format = 0;
u64 m_frame_number = 0;
u32 m_index = 0;
bool m_filter_small_contours = true;
bool m_freeze_frame = false;
bool m_frame_frozen = false;
bool m_show_all_contours = false;
bool m_draw_contours = true;
bool m_draw_overlays = true;
QPixmap m_image;
QPixmap m_histogram;
// Thread control
QTimer* m_update_timer = nullptr;
std::unique_ptr<QThread> m_tracker_thread;
atomic_t<u32> m_stop_threads = 0;
std::mutex m_camera_handler_mutex;
std::mutex m_image_mutex;
std::shared_ptr<named_thread<pad_thread>> m_input_thread;
};

View File

@ -0,0 +1,393 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ps_move_tracker_dialog</class>
<widget class="QDialog" name="ps_move_tracker_dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>988</width>
<height>710</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="main_layout" stretch="0,0">
<item>
<layout class="QHBoxLayout" name="mainLayout">
<item>
<layout class="QVBoxLayout" name="previewLayout">
<item>
<widget class="QGroupBox" name="gbPreview">
<property name="title">
<string>Preview</string>
</property>
<layout class="QVBoxLayout" name="gb_view_layout">
<item>
<widget class="QLabel" name="imageLabel">
<property name="minimumSize">
<size>
<width>640</width>
<height>480</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gistoGb">
<property name="title">
<string>Histogram</string>
</property>
<layout class="QVBoxLayout" name="gistoGbLayout">
<item>
<widget class="QLabel" name="histogramLabel">
<property name="minimumSize">
<size>
<width>0</width>
<height>100</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="rightLayout">
<item>
<widget class="QGroupBox" name="settingsGb">
<property name="minimumSize">
<size>
<width>300</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="minRadiusGb">
<property name="title">
<string>Min Radius</string>
</property>
<layout class="QVBoxLayout" name="minRadiusGbLayout">
<item>
<widget class="QSlider" name="minRadiusSlider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="maxRadiusGb">
<property name="title">
<string>Max Radius</string>
</property>
<layout class="QVBoxLayout" name="maxRadiusGbLayout">
<item>
<widget class="QSlider" name="maxRadiusSlider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="psMoveGb">
<property name="title">
<string>PS Move</string>
</property>
<layout class="QVBoxLayout" name="psMoveGbLayout">
<item>
<widget class="QComboBox" name="comboSelectDevice"/>
</item>
<item>
<widget class="QGroupBox" name="colorGb">
<property name="title">
<string>Color</string>
</property>
<layout class="QVBoxLayout" name="colorGbLayout">
<item>
<widget class="QSlider" name="colorSliderR">
<property name="maximum">
<number>255</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="colorSliderG">
<property name="maximum">
<number>255</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="colorSliderB">
<property name="maximum">
<number>255</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="hueGb">
<property name="title">
<string>Hue</string>
</property>
<layout class="QVBoxLayout" name="hueGbLayout">
<item>
<widget class="QSlider" name="hueSlider">
<property name="maximum">
<number>359</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="hueThresholdGb">
<property name="title">
<string>Hue Threshold</string>
</property>
<layout class="QVBoxLayout" name="hueThresholdLayout">
<item>
<widget class="QSlider" name="hueThresholdSlider">
<property name="maximum">
<number>359</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="saturationThresholdGb">
<property name="title">
<string>Saturation Threshold</string>
</property>
<layout class="QVBoxLayout" name="saturationThresholdGbLayout">
<item>
<widget class="QSlider" name="saturationThresholdSlider">
<property name="maximum">
<number>255</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="inputFormatGb">
<property name="title">
<string>Input Format</string>
</property>
<layout class="QVBoxLayout" name="input_format_layout">
<item>
<widget class="QComboBox" name="inputFormatCombo"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="viewGb">
<property name="title">
<string>View</string>
</property>
<layout class="QVBoxLayout" name="view_layout">
<item>
<widget class="QComboBox" name="viewCombo"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="histoSettingsGb">
<property name="title">
<string>Histogram</string>
</property>
<layout class="QVBoxLayout" name="histoSettingsGbLayout">
<item>
<widget class="QComboBox" name="histoCombo"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="displayGb">
<property name="title">
<string>Display</string>
</property>
<layout class="QVBoxLayout" name="filterGbLayout">
<item>
<widget class="QCheckBox" name="filterSmallContoursBox">
<property name="text">
<string>Filter Small Contours</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="freezeFrameBox">
<property name="text">
<string>Freeze Frame</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showAllContoursBox">
<property name="text">
<string>Show All Contours</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="drawContoursBox">
<property name="text">
<string>Draw Contours</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="drawOverlaysBox">
<property name="text">
<string>Draw Overlays</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Apply|QDialogButtonBox::Close|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ps_move_tracker_dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ps_move_tracker_dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -279,6 +279,7 @@ public:
const QString dualsense_linux = tr("The DualSense handler is recommended for official DualSense controllers.");
const QString dualsense_other = tr("The DualSense handler is recommended for official DualSense controllers.");
const QString skateboard = tr("The Skateboard handler is recommended for official RIDE skateboard controllers.");
const QString move = tr("The PS Move handler is recommended for official PS Move controllers.");
const QString xinput = tr("The XInput handler will work with Xbox controllers and many third-party PC-compatible controllers. Pressure sensitive buttons from SCP are supported when SCP's XInput1_3.dll is placed in the main RPCS3 directory. For more details, see the <a %0 href=\"https://wiki.rpcs3.net/index.php?title=Help:Controller_Configuration\">RPCS3 Wiki</a>.").arg(gui::utils::get_link_style());
const QString evdev = tr("The evdev handler should work with any controller that has Linux support.<br>If your joystick is not being centered properly, read the <a %0 href=\"https://wiki.rpcs3.net/index.php?title=Help:Controller_Configuration\">RPCS3 Wiki</a> for instructions.").arg(gui::utils::get_link_style());
const QString mmjoy = tr("The MMJoystick handler should work with almost any controller recognized by Windows. However, it is recommended that you use the more specific handlers if you have a controller that supports them.");