From b89cc9b97339bc7fba7abba746543d17d45c3c92 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Mon, 8 Jul 2024 20:17:21 +0200 Subject: [PATCH] cellGem: implement real ps move handler --- rpcs3/Emu/Cell/Modules/cellGem.cpp | 630 ++++++++++++-- rpcs3/Emu/Cell/Modules/cellGem.h | 3 +- rpcs3/Emu/Cell/Modules/cellPad.cpp | 7 +- rpcs3/Emu/Io/PadHandler.cpp | 27 +- rpcs3/Emu/Io/PadHandler.h | 11 + rpcs3/Emu/Io/camera_handler_base.h | 7 + rpcs3/Emu/Io/gem_config.h | 30 +- rpcs3/Emu/Io/pad_config_types.cpp | 1 + rpcs3/Emu/Io/pad_config_types.h | 1 + rpcs3/Emu/Io/pad_types.h | 25 +- rpcs3/Emu/system_config_types.cpp | 1 + rpcs3/Emu/system_config_types.h | 1 + rpcs3/Input/gui_pad_thread.cpp | 1 + rpcs3/Input/hid_pad_handler.cpp | 59 +- rpcs3/Input/hid_pad_handler.h | 3 + rpcs3/Input/pad_thread.cpp | 5 +- rpcs3/Input/ps_move_config.cpp | 40 + rpcs3/Input/ps_move_config.h | 40 + rpcs3/Input/ps_move_handler.cpp | 797 ++++++++++++++++++ rpcs3/Input/ps_move_handler.h | 166 ++++ rpcs3/Input/ps_move_tracker.cpp | 545 ++++++++++++ rpcs3/Input/ps_move_tracker.h | 95 +++ rpcs3/rpcs3.vcxproj | 46 +- rpcs3/rpcs3.vcxproj.filters | 42 + rpcs3/rpcs3qt/emu_settings.cpp | 1 + .../rpcs3qt/emulated_pad_settings_dialog.cpp | 65 +- rpcs3/rpcs3qt/emulated_pad_settings_dialog.h | 1 + rpcs3/rpcs3qt/main_window.cpp | 15 + rpcs3/rpcs3qt/main_window.ui | 12 + rpcs3/rpcs3qt/pad_settings_dialog.cpp | 3 + rpcs3/rpcs3qt/ps_move_tracker_dialog.cpp | 595 +++++++++++++ rpcs3/rpcs3qt/ps_move_tracker_dialog.h | 89 ++ rpcs3/rpcs3qt/ps_move_tracker_dialog.ui | 393 +++++++++ rpcs3/rpcs3qt/tooltips.h | 1 + 34 files changed, 3628 insertions(+), 130 deletions(-) create mode 100644 rpcs3/Input/ps_move_config.cpp create mode 100644 rpcs3/Input/ps_move_config.h create mode 100644 rpcs3/Input/ps_move_handler.cpp create mode 100644 rpcs3/Input/ps_move_handler.h create mode 100644 rpcs3/Input/ps_move_tracker.cpp create mode 100644 rpcs3/Input/ps_move_tracker.h create mode 100644 rpcs3/rpcs3qt/ps_move_tracker_dialog.cpp create mode 100644 rpcs3/rpcs3qt/ps_move_tracker_dialog.h create mode 100644 rpcs3/rpcs3qt/ps_move_tracker_dialog.ui diff --git a/rpcs3/Emu/Cell/Modules/cellGem.cpp b/rpcs3/Emu/Cell/Modules/cellGem.cpp index 7ab29e3071..6fea6f2479 100644 --- a/rpcs3/Emu/Cell/Modules/cellGem.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGem.cpp @@ -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; #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 controllers; u32 connected_controllers = 0; atomic_t video_conversion_in_progress{false}; - atomic_t update_started{false}; + atomic_t 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(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 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; +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(); + 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(); + + 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(g_cfg_move.min_radius.get() / g_cfg_move.min_radius.max)); + m_tracker.set_max_radius(static_cast(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 m_wake_up = 0; + atomic_t m_done = 1; + atomic_t m_busy = false; + ps_move_tracker m_tracker{}; + CellCameraInfoEx m_camera_info{}; + std::array m_hues{}; + std::array 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& 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(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(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& 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 +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>(); // 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>) + { + 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>) + { + 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& digital_buttons, be_t& analog_t) +static bool mouse_input_to_pad(u32 mouse_no, be_t& digital_buttons, be_t& analog_t) { digital_buttons = 0; analog_t = 0; @@ -954,7 +1268,7 @@ static bool mouse_input_to_pad(const u32 mouse_no, be_t& digital_buttons, b } template -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& digital_buttons, be_t& analog_t) +static bool gun_input_to_pad(u32 gem_no, be_t& digital_buttons, be_t& analog_t) { digital_buttons = 0; analog_t = 0; @@ -1027,7 +1341,7 @@ static bool gun_input_to_pad(const u32 gem_no, be_t& digital_buttons, be_t< } template -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>(); + tracker.wait_for_result(); + + gem.updating = false; + return CELL_GEM_ERROR_UNINITIALIZED; } @@ -1368,9 +1687,12 @@ error_code cellGemGetAllTrackableHues(vm::ptr hues) return CELL_GEM_ERROR_INVALID_PARAMETER; } - for (u32 i = 0; i < 360; i++) + auto& tracker = g_fxo->get>(); + 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 camera_frame, u32 hue, vm::ptr 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 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 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 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::ptrext); - 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>(); // 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 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>(); + 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 vc_ error_code cellGemReadExternalPortDeviceInfo(u32 gem_num, vm::ptr ext_id, vm::ptr 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(); @@ -2124,12 +2488,65 @@ error_code cellGemReadExternalPortDeviceInfo(u32 gem_num, vm::ptr 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 req_hues, vm::ptr 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 req_hues, vm::ptr 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>(); + 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 camera_frame, u64 timestamp) return CELL_GEM_ERROR_UNINITIALIZED; } + auto& tracker = g_fxo->get>(); + + 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 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 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(); @@ -2364,9 +2800,25 @@ error_code cellGemWriteExternalPort(u32 gem_num, vm::ptrGetPads(), 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; diff --git a/rpcs3/Emu/Cell/Modules/cellGem.h b/rpcs3/Emu/Cell/Modules/cellGem.h index b5e03db5fc..c70e2386ba 100644 --- a/rpcs3/Emu/Cell/Modules/cellGem.h +++ b/rpcs3/Emu/Cell/Modules/cellGem.h @@ -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 diff --git a/rpcs3/Emu/Cell/Modules/cellPad.cpp b/rpcs3/Emu/Cell/Modules/cellPad.cpp index c4278c6ba4..771bb04a6e 100644 --- a/rpcs3/Emu/Cell/Modules/cellPad.cpp +++ b/rpcs3/Emu/Cell/Modules/cellPad.cpp @@ -313,9 +313,10 @@ void clear_pad_buffer(const std::shared_ptr& 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) diff --git a/rpcs3/Emu/Io/PadHandler.cpp b/rpcs3/Emu/Io/PadHandler.cpp index cc8734b10c..e63368911b 100644 --- a/rpcs3/Emu/Io/PadHandler.cpp +++ b/rpcs3/Emu/Io/PadHandler.cpp @@ -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(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(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 = std::make_shared(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)); diff --git a/rpcs3/Emu/Io/PadHandler.h b/rpcs3/Emu/Io/PadHandler.h index d7811a0dcb..55bc548620 100644 --- a/rpcs3/Emu/Io/PadHandler.h +++ b/rpcs3/Emu/Io/PadHandler.h @@ -29,6 +29,15 @@ public: std::set trigger_code_right{}; std::array, 4> axis_code_left{}; std::array, 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 min_button_values; std::set blacklist; + std::shared_ptr m_pad_for_pad_settings; + static std::set narrow_set(const std::set& src); // Search an unordered map for a string value and return found keycode diff --git a/rpcs3/Emu/Io/camera_handler_base.h b/rpcs3/Emu/Io/camera_handler_base.h index 98f1158c43..531fa3abe3 100644 --- a/rpcs3/Emu/Io/camera_handler_base.h +++ b/rpcs3/Emu/Io/camera_handler_base.h @@ -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 m_state = camera_handler_state::closed; diff --git a/rpcs3/Emu/Io/gem_config.h b/rpcs3/Emu/Io/gem_config.h index 2197535c4d..d737928790 100644 --- a/rpcs3/Emu/Io/gem_config.h +++ b/rpcs3/Emu/Io/gem_config.h @@ -20,9 +20,9 @@ enum class gem_btn count }; -struct cfg_gem final : public emulated_pad_config +struct cfg_fake_gem final : public emulated_pad_config { - 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 start{ this, "Start", gem_btn::start, pad_button::start }; cfg_pad_btn select{ this, "Select", gem_btn::select, pad_button::select }; @@ -36,9 +36,29 @@ struct cfg_gem final : public emulated_pad_config cfg_pad_btn y_axis{ this, "Y-Axis", gem_btn::y_axis, pad_button::ls_y }; }; -struct cfg_gems final : public emulated_pads_config +struct cfg_fake_gems final : public emulated_pads_config { - cfg_gems() : emulated_pads_config("gem") {}; + cfg_fake_gems() : emulated_pads_config("gem") {}; }; -extern cfg_gems g_cfg_gem; +struct cfg_gem final : public emulated_pad_config +{ + cfg_gem(node* owner, const std::string& name) : emulated_pad_config(owner, name) {} + + cfg_pad_btn start{ this, "Start", gem_btn::start, pad_button::start }; + cfg_pad_btn select{ this, "Select", gem_btn::select, pad_button::select }; + cfg_pad_btn triangle{ this, "Triangle", gem_btn::triangle, pad_button::triangle }; + cfg_pad_btn circle{ this, "Circle", gem_btn::circle, pad_button::circle }; + cfg_pad_btn cross{ this, "Cross", gem_btn::cross, pad_button::cross }; + cfg_pad_btn square{ this, "Square", gem_btn::square, pad_button::square }; + cfg_pad_btn move{ this, "Move", gem_btn::move, pad_button::R1 }; + cfg_pad_btn t{ this, "T", gem_btn::t, pad_button::R2 }; +}; + +struct cfg_gems final : public emulated_pads_config +{ + cfg_gems() : emulated_pads_config("gem_real") {}; +}; + +extern cfg_gems g_cfg_gem_real; +extern cfg_fake_gems g_cfg_gem_fake; diff --git a/rpcs3/Emu/Io/pad_config_types.cpp b/rpcs3/Emu/Io/pad_config_types.cpp index 370da062b8..ab1503604e 100644 --- a/rpcs3/Emu/Io/pad_config_types.cpp +++ b/rpcs3/Emu/Io/pad_config_types.cpp @@ -14,6 +14,7 @@ void fmt_class_string::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"; diff --git a/rpcs3/Emu/Io/pad_config_types.h b/rpcs3/Emu/Io/pad_config_types.h index 4d1c64eea8..b9b7132ce1 100644 --- a/rpcs3/Emu/Io/pad_config_types.h +++ b/rpcs3/Emu/Io/pad_config_types.h @@ -10,6 +10,7 @@ enum class pad_handler ds4, dualsense, skateboard, + move, #ifdef _WIN32 xinput, mm, diff --git a/rpcs3/Emu/Io/pad_types.h b/rpcs3/Emu/Io/pad_types.h index e99b2edda3..d048c65651 100644 --- a/rpcs3/Emu/Io/pad_types.h +++ b/rpcs3/Emu/Io/pad_types.h @@ -337,8 +337,9 @@ struct CellPadData be_t 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 external_device_data{}; + std::array external_device_read{}; // CELL_GEM_EXTERNAL_PORT_DEVICE_INFO_SIZE + std::array 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) diff --git a/rpcs3/Emu/system_config_types.cpp b/rpcs3/Emu/system_config_types.cpp index 3819ec690b..c01692b8a5 100644 --- a/rpcs3/Emu/system_config_types.cpp +++ b/rpcs3/Emu/system_config_types.cpp @@ -437,6 +437,7 @@ void fmt_class_string::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"; diff --git a/rpcs3/Emu/system_config_types.h b/rpcs3/Emu/system_config_types.h index d648ff9779..f3e3b31f42 100644 --- a/rpcs3/Emu/system_config_types.h +++ b/rpcs3/Emu/system_config_types.h @@ -138,6 +138,7 @@ enum class fake_camera_type enum class move_handler { null, + real, fake, mouse, raw_mouse, diff --git a/rpcs3/Input/gui_pad_thread.cpp b/rpcs3/Input/gui_pad_thread.cpp index 2423d9bcc1..ff179f644e 100644 --- a/rpcs3/Input/gui_pad_thread.cpp +++ b/rpcs3/Input/gui_pad_thread.cpp @@ -198,6 +198,7 @@ std::shared_ptr 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: diff --git a/rpcs3/Input/hid_pad_handler.cpp b/rpcs3/Input/hid_pad_handler.cpp index 9d40dc777d..40dcbdef24 100644 --- a/rpcs3/Input/hid_pad_handler.cpp +++ b/rpcs3/Input/hid_pad_handler.cpp @@ -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 @@ -185,9 +193,19 @@ void hid_pad_handler::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::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 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 @@ -235,8 +276,15 @@ void hid_pad_handler::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; template class hid_pad_handler; template class hid_pad_handler; template class hid_pad_handler; +template class hid_pad_handler; diff --git a/rpcs3/Input/hid_pad_handler.h b/rpcs3/Input/hid_pad_handler.h index 5293eb4981..e0e14a0a27 100644 --- a/rpcs3/Input/hid_pad_handler.h +++ b/rpcs3/Input/hid_pad_handler.h @@ -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}; diff --git a/rpcs3/Input/pad_thread.cpp b/rpcs3/Input/pad_thread.cpp index 63d616ef34..0c207d6595 100644 --- a/rpcs3/Input/pad_thread.cpp +++ b/rpcs3/Input/pad_thread.cpp @@ -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(CELL_PAD_MAX_PORT_NUM) - static_cast(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(CELL_PAD_MAX_PORT_NUM) - static_cast(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 pad_thread::GetHandler(pad_handler type) return std::make_shared(); case pad_handler::skateboard: return std::make_shared(); + case pad_handler::move: + return std::make_shared(); #ifdef _WIN32 case pad_handler::xinput: return std::make_shared(); diff --git a/rpcs3/Input/ps_move_config.cpp b/rpcs3/Input/ps_move_config.cpp new file mode 100644 index 0000000000..14f9d081fe --- /dev/null +++ b/rpcs3/Input/ps_move_config.cpp @@ -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); + } +} diff --git a/rpcs3/Input/ps_move_config.h b/rpcs3/Input/ps_move_config.h new file mode 100644 index 0000000000..cdf70f9646 --- /dev/null +++ b/rpcs3/Input/ps_move_config.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Utilities/Config.h" + +#include + +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 move{ &move1, &move2, &move3, &move4 }; + + const std::string path; + + bool load(); + void save() const; +}; + +extern cfg_ps_moves g_cfg_move; diff --git a/rpcs3/Input/ps_move_handler.cpp b/rpcs3/Input/ps_move_handler.cpp new file mode 100644 index 0000000000..376ce72cd9 --- /dev/null +++ b/rpcs3/Input/ps_move_handler.cpp @@ -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(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(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 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(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& device) +{ + ps_move_device* move_device = static_cast(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(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 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(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 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(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& /*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& /*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 ps_move_handler::get_button_values(const std::shared_ptr& device) +{ + std::unordered_map key_buf; + ps_move_device* dev = static_cast(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& 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(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& 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(&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(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 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 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(device->battery_level * 20, 0, 100); +} diff --git a/rpcs3/Input/ps_move_handler.h b/rpcs3/Input/ps_move_handler.h new file mode 100644 index 0000000000..edf377e044 --- /dev/null +++ b/rpcs3/Input/ps_move_handler.h @@ -0,0 +1,166 @@ +#pragma once + +#include "hid_pad_handler.h" + +#include + +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 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 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 +{ + 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& device, u64 keyCode) override; + bool get_is_right_trigger(const std::shared_ptr& device, u64 keyCode) override; + PadHandlerBase::connection update_connection(const std::shared_ptr& device) override; + std::unordered_map get_button_values(const std::shared_ptr& device) override; + pad_preview_values get_preview_values(const std::unordered_map& 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); +}; diff --git a/rpcs3/Input/ps_move_tracker.cpp b/rpcs3/Input/ps_move_tracker.cpp new file mode 100644 index 0000000000..5812c851bb --- /dev/null +++ b/rpcs3/Input/ps_move_tracker.cpp @@ -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 + +#include + +LOG_CHANNEL(ps_move); + +namespace gem +{ + extern bool convert_image_format(CellCameraFormat input_format, CellGemVideoConvertFormatEnum output_format, + const std::vector& video_data_in, u32 width, u32 height, + u8* video_data_out, u32 video_data_out_size); +} + +template class ps_move_tracker; +template class ps_move_tracker; + +template +ps_move_tracker::ps_move_tracker() +{ + init_workers(); +} + +template +ps_move_tracker::~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 +void ps_move_tracker::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 +void ps_move_tracker::set_active(u32 index, bool active) +{ + ps_move_config& config = ::at32(m_config, index); + config.active = active; +} + +template +void ps_move_tracker::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 +void ps_move_tracker::set_hue(u32 index, u16 hue) +{ + ps_move_config& config = ::at32(m_config, index); + config.hue = hue; + config.calculate_values(); +} + +template +void ps_move_tracker::set_hue_threshold(u32 index, u16 threshold) +{ + ps_move_config& config = ::at32(m_config, index); + config.hue_threshold = threshold; + config.calculate_values(); +} + +template +void ps_move_tracker::set_saturation_threshold(u32 index, u16 threshold) +{ + ps_move_config& config = ::at32(m_config, index); + config.saturation_threshold_u8 = threshold; + config.calculate_values(); +} + +template +void ps_move_tracker::init_workers() +{ + for (u32 index = 0; index < CELL_GEM_MAX_NUM; index++) + { + if (m_workers[index]) + { + continue; + } + + m_workers[index] = std::make_unique>>(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 +void ps_move_tracker::process_image() +{ + // Convert image to RGBA + convert_image(CELL_GEM_RGBA_640x480); + + // Calculate hues + process_hues(); + + // Get active devices + std::vector 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 +void ps_move_tracker::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 +void ps_move_tracker::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(hue / 2); + hsv[1] = static_cast(saturation * 255.0f); + hsv[2] = static_cast(value * 255.0f); + + if constexpr (DiagnosticsEnabled) + { + *gray++ = static_cast(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& contour, float& area) +{ + std::vector approx; + cv::approxPolyDP(contour, approx, 0.01 * cv::arcLength(contour, true), true); + if (approx.size() < 8ULL) return false; + + area = static_cast(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 +void ps_move_tracker::process_contours(ps_move_info& info, u32 index) +{ + const ps_move_config& config = ::at32(m_config, index); + const std::vector& image_hsv = m_image_hsv; + std::vector& 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> 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> all_contours; + cv::findContours(binary, all_contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); + + if (all_contours.empty()) + return; + + std::vector> contours; + contours.reserve(all_contours.size()); + + std::vector centers; + centers.reserve(all_contours.size()); + + std::vector 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& 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(centers[best_index].x), 0u, width); + info.y_pos = std::clamp(static_cast(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 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(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 +std::tuple ps_move_tracker::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(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(std::clamp(std::round(r * 255.0f), 0.0f, 255.0f)); + const u8 green = static_cast(std::clamp(std::round(g * 255.0f), 0.0f, 255.0f)); + const u8 blue = static_cast(std::clamp(std::round(b * 255.0f), 0.0f, 255.0f)); + + return { red, green, blue }; +} + +template +std::tuple ps_move_tracker::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(60.0f * (g - b) / delta) + 360) % 360; + } + else if (cmax == g) + { + hue = (static_cast(60.0f * (b - r) / delta) + 120 + 360) % 360; + } + else + { + hue = (static_cast(60.0f * (r - g) / delta) + 240 + 360) % 360; + } + + return { hue, saturation, cmax }; +} diff --git a/rpcs3/Input/ps_move_tracker.h b/rpcs3/Input/ps_move_tracker.h new file mode 100644 index 0000000000..d4a9d5531a --- /dev/null +++ b/rpcs3/Input/ps_move_tracker.h @@ -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 +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& info() { return m_info; } + const std::array& hues() { return m_hues; } + const std::vector& rgba() { return m_image_rgba; } + const std::vector& rgba_contours() { return m_image_rgba_contours; } + const std::vector& hsv() { return m_image_hsv; } + const std::vector& gray() { return m_image_gray; } + const std::vector& binary(u32 index) { return ::at32(m_image_binary, index); } + + static std::tuple hsv_to_rgb(u16 hue, float saturation, float value); + static std::tuple 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 m_image_data; + std::vector m_image_rgba; + std::vector m_image_rgba_contours; + std::vector m_image_hsv; + std::vector m_image_gray; + + std::array, CELL_GEM_MAX_NUM> m_image_hue_filtered{}; + std::array, CELL_GEM_MAX_NUM> m_image_binary{}; + + std::array m_hues{}; + std::array m_info{}; + std::array m_config{}; + + std::array>>, CELL_GEM_MAX_NUM> m_workers{}; + std::array, CELL_GEM_MAX_NUM> m_wake_up_workers{}; + std::array, CELL_GEM_MAX_NUM> m_workers_finished{}; +}; diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 427133c035..f1292e222f 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -71,7 +71,7 @@ - $(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) + $(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) /Zc:__cplusplus -Zc:strictStrings -Zc:throwingNew- -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions) release\ false @@ -89,8 +89,8 @@ TurnOffAllWarnings - 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) - $(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) + 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) + $(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) "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" %(AdditionalOptions) true Debug @@ -123,7 +123,7 @@ - $(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) + $(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) /Zc:__cplusplus -Zc:strictStrings -Zc:throwingNew- -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions) debug\ false @@ -140,8 +140,8 @@ $(IntDir)vc$(PlatformToolsetVersion).pdb - 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) - $(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) + 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) + $(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) "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /VERBOSE %(AdditionalOptions) true Debug @@ -179,8 +179,11 @@ + + + @@ -213,6 +216,9 @@ true + + true + true @@ -486,6 +492,9 @@ true + + true + true @@ -741,6 +750,7 @@ + @@ -961,8 +971,11 @@ $(QTDIR)\bin\moc.exe;%(FullPath);$(QTDIR)\bin\moc.exe;%(FullPath) $(QTDIR)\bin\moc.exe;%(FullPath);$(QTDIR)\bin\moc.exe;%(FullPath) + + + @@ -1022,6 +1035,7 @@ + @@ -1093,6 +1107,16 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(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" + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(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" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(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" + @@ -1962,6 +1986,16 @@ .\QTGeneratedFiles\ui_%(Filename).h;%(Outputs) "$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)" + + $(QTDIR)\bin\uic.exe;%(AdditionalInputs) + Uic%27ing %(Identity)... + .\QTGeneratedFiles\ui_%(Filename).h;%(Outputs) + "$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)" + $(QTDIR)\bin\uic.exe;%(AdditionalInputs) + Uic%27ing %(Identity)... + .\QTGeneratedFiles\ui_%(Filename).h;%(Outputs) + "$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)" + $(QTDIR)\bin\uic.exe;%(AdditionalInputs) Uic%27ing %(Identity)... diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 5550ee2e28..b7e277ca51 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -184,6 +184,12 @@ {5cace00d-92fc-4b03-82a2-72706cd04e84} + + {4a270c26-740b-46e8-880c-bb22e708b897} + + + {f8a98f7b-dc23-47c0-8a5f-d0b76eaf0df5} + @@ -879,6 +885,15 @@ Generated Files\Release + + Gui\ps_move_tracker_dialog + + + Generated Files\Debug + + + Generated Files\Release + Gui\utils @@ -1119,6 +1134,15 @@ Gui\vfs + + Io\Move + + + Io\Move + + + Io\Move + @@ -1253,6 +1277,9 @@ Generated Files + + Generated Files + Gui\utils @@ -1307,6 +1334,15 @@ Io\raw + + Io\Move + + + Io\Move + + + Io\Move + @@ -1546,6 +1582,12 @@ Gui\settings + + Form Files + + + Gui\ps_move_tracker_dialog + Io\music diff --git a/rpcs3/rpcs3qt/emu_settings.cpp b/rpcs3/rpcs3qt/emu_settings.cpp index 5a80174198..2b4d4a0b87 100644 --- a/rpcs3/rpcs3qt/emu_settings.cpp +++ b/rpcs3/rpcs3qt/emu_settings.cpp @@ -1150,6 +1150,7 @@ QString emu_settings::GetLocalizedSetting(const QString& original, emu_settings_ switch (static_cast(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"); diff --git a/rpcs3/rpcs3qt/emulated_pad_settings_dialog.cpp b/rpcs3/rpcs3qt/emulated_pad_settings_dialog.cpp index d423dae792..835f940797 100644 --- a/rpcs3/rpcs3qt/emulated_pad_settings_dialog.cpp +++ b/rpcs3/rpcs3qt/emulated_pad_settings_dialog.cpp @@ -84,6 +84,10 @@ emulated_pad_settings_dialog::emulated_pad_settings_dialog(pad_type type, QWidge setWindowTitle(tr("Configure Emulated USIO")); add_tabs(tabs); break; + case emulated_pad_settings_dialog::pad_type::gem: + setWindowTitle(tr("Configure Emulated PS Move (Real)")); + add_tabs(tabs); + break; case emulated_pad_settings_dialog::pad_type::ds3gem: setWindowTitle(tr("Configure Emulated PS Move (Fake)")); add_tabs(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(T::count); - - for (u32 cols = 1; utils::aligned_div(static_cast(T::count), cols) > max_items_per_column;) - { - rows = utils::aligned_div(static_cast(T::count), ++cols); - } + int count = static_cast(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(gem_btn::y_axis) == static_cast(gem_btn::count) - 1); + static_assert(static_cast(gem_btn::x_axis) == static_cast(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(count), cols) > max_items_per_column;) + { + rows = utils::aligned_div(static_cast(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(T::count); i++, row++) + for (int i = 0, row = 0, col = 0; i < count; i++, row++) { const T id = static_cast(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(id)); break; + case pad_type::gem: + saved_btn_id = ::at32(g_cfg_gem_real.players, player)->get_pad_button(static_cast(id)); + break; case pad_type::ds3gem: - saved_btn_id = ::at32(g_cfg_gem.players, player)->get_pad_button(static_cast(id)); + saved_btn_id = ::at32(g_cfg_gem_fake.players, player)->get_pad_button(static_cast(id)); break; case pad_type::guncon3: saved_btn_id = ::at32(g_cfg_guncon3.players, player)->get_pad_button(static_cast(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(id), btn_id); break; + case pad_type::gem: + ::at32(g_cfg_gem_real.players, player)->set_button(static_cast(id), btn_id); + break; case pad_type::ds3gem: - ::at32(g_cfg_gem.players, player)->set_button(static_cast(id), btn_id); + ::at32(g_cfg_gem_fake.players, player)->set_button(static_cast(id), btn_id); break; case pad_type::guncon3: ::at32(g_cfg_guncon3.players, player)->set_button(static_cast(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(data.toInt())); break; + case pad_type::gem: + def_btn_id = ::at32(g_cfg_gem_real.players, player)->default_pad_button(static_cast(data.toInt())); + break; case pad_type::ds3gem: - def_btn_id = ::at32(g_cfg_gem.players, player)->default_pad_button(static_cast(data.toInt())); + def_btn_id = ::at32(g_cfg_gem_fake.players, player)->default_pad_button(static_cast(data.toInt())); break; case pad_type::guncon3: def_btn_id = ::at32(g_cfg_guncon3.players, player)->default_pad_button(static_cast(data.toInt())); diff --git a/rpcs3/rpcs3qt/emulated_pad_settings_dialog.h b/rpcs3/rpcs3qt/emulated_pad_settings_dialog.h index f9784b3123..772f467182 100644 --- a/rpcs3/rpcs3qt/emulated_pad_settings_dialog.h +++ b/rpcs3/rpcs3qt/emulated_pad_settings_dialog.h @@ -19,6 +19,7 @@ public: turntable, ghltar, usio, + gem, ds3gem, guncon3, topshotelite, diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 0efe7070af..b2fb152f3e 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -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(); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index a05e531dc6..2d0396c499 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -242,6 +242,7 @@ + @@ -261,6 +262,7 @@ + @@ -1390,6 +1392,16 @@ VFS Tool + + + PS Move Tracker + + + + + PS Move + + diff --git a/rpcs3/rpcs3qt/pad_settings_dialog.cpp b/rpcs3/rpcs3qt/pad_settings_dialog.cpp index 8676c36506..ee239717af 100644 --- a/rpcs3/rpcs3qt/pad_settings_dialog.cpp +++ b/rpcs3/rpcs3qt/pad_settings_dialog.cpp @@ -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"); diff --git a/rpcs3/rpcs3qt/ps_move_tracker_dialog.cpp b/rpcs3/rpcs3qt/ps_move_tracker_dialog.cpp new file mode 100644 index 0000000000..6a5f4b0300 --- /dev/null +++ b/rpcs3/rpcs3qt/ps_move_tracker_dialog.cpp @@ -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 +#include +#include +#include +#include +#include + +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(CELL_CAMERA_RGBA)); + ui->inputFormatCombo->addItem(tr("RAW8"), static_cast(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()) + { + m_format = qvar.toInt(); + reset_camera(); + } + }); + + ui->viewCombo->addItem(tr("Image"), static_cast(view_mode::image)); + ui->viewCombo->addItem(tr("Grayscale"), static_cast(view_mode::grayscale)); + ui->viewCombo->addItem(tr("HSV Hue"), static_cast(view_mode::hsv_hue)); + ui->viewCombo->addItem(tr("HSV Saturation"), static_cast(view_mode::hsv_saturation)); + ui->viewCombo->addItem(tr("HSV Value"), static_cast(view_mode::hsv_value)); + ui->viewCombo->addItem(tr("Binary"), static_cast(view_mode::binary)); + ui->viewCombo->addItem(tr("Contours"), static_cast(view_mode::contours)); + ui->viewCombo->setCurrentIndex(ui->viewCombo->findData(static_cast(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()) + { + m_view_mode = static_cast(qvar.toInt()); + } + }); + + ui->histoCombo->addItem(tr("Hues"), static_cast(histo_mode::unfiltered_hues)); + ui->histoCombo->setCurrentIndex(ui->viewCombo->findData(static_cast(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()) + { + m_histo_mode = static_cast(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(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(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(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(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(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(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()) + { + 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>(); + + 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>(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::rgb_to_hsv(config->r.get() / 255.0f, config->g.get() / 255.0f, config->b.get() / 255.0f); + config->hue.set(std::clamp(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::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(); + 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(g_cfg_move.min_radius.get() / g_cfg_move.min_radius.max)); + m_ps_move_tracker->set_max_radius(static_cast(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* 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(m_view_mode) - static_cast(view_mode::hsv_hue); + const std::vector& hsv = m_ps_move_tracker->hsv(); + static std::vector hsv_single; + hsv_single.resize(hsv.size() / 3); + for (int i = 0; i < static_cast(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& hues, bool ignore_zero) const +{ + // Create image + const int height = ui->histogramLabel->height(); + static QPixmap background = [&]() + { + // Paint background + QPixmap pxmap(static_cast(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::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(hues.size()); i++) + { + const int bar_height = (hues[i] / static_cast(max_value)) * height; + if (bar_height <= 0) continue; + painter.drawLine(i, height - 1, i, height - bar_height); + } + + return histo; +} diff --git a/rpcs3/rpcs3qt/ps_move_tracker_dialog.h b/rpcs3/rpcs3qt/ps_move_tracker_dialog.h new file mode 100644 index 0000000000..c7a8641e7e --- /dev/null +++ b/rpcs3/rpcs3qt/ps_move_tracker_dialog.h @@ -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 +#include +#include +#include + +class qt_camera_handler; +class pad_thread; +template 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& 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; + std::unique_ptr m_camera_handler; + std::unique_ptr> m_ps_move_tracker; + std::vector m_image_data_frozen; + std::vector m_image_data; + std::array 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 m_tracker_thread; + atomic_t m_stop_threads = 0; + std::mutex m_camera_handler_mutex; + std::mutex m_image_mutex; + std::shared_ptr> m_input_thread; +}; diff --git a/rpcs3/rpcs3qt/ps_move_tracker_dialog.ui b/rpcs3/rpcs3qt/ps_move_tracker_dialog.ui new file mode 100644 index 0000000000..2272c989dc --- /dev/null +++ b/rpcs3/rpcs3qt/ps_move_tracker_dialog.ui @@ -0,0 +1,393 @@ + + + ps_move_tracker_dialog + + + + 0 + 0 + 988 + 710 + + + + + 0 + 0 + + + + Dialog + + + + + + + + + + Preview + + + + + + + 640 + 480 + + + + + + + + + + + Histogram + + + + + + + 0 + 100 + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 0 + 0 + + + + + + + + + + + + + + + + 300 + 0 + + + + Settings + + + + + + Min Radius + + + + + + Qt::Horizontal + + + + + + + + + + Max Radius + + + + + + Qt::Horizontal + + + + + + + + + + PS Move + + + + + + + + + Color + + + + + + 255 + + + Qt::Horizontal + + + + + + + 255 + + + Qt::Horizontal + + + + + + + 255 + + + Qt::Horizontal + + + + + + + + + + Hue + + + + + + 359 + + + Qt::Horizontal + + + + + + + + + + Hue Threshold + + + + + + 359 + + + Qt::Horizontal + + + + + + + + + + Saturation Threshold + + + + + + 255 + + + Qt::Horizontal + + + + + + + + + + + + + + + + Input Format + + + + + + + + + + + + View + + + + + + + + + + + + Histogram + + + + + + + + + + + + Display + + + + + + Filter Small Contours + + + + + + + Freeze Frame + + + + + + + Show All Contours + + + + + + + Draw Contours + + + + + + + Draw Overlays + + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 0 + 0 + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Apply|QDialogButtonBox::Close|QDialogButtonBox::Save + + + + + + + + + buttonBox + accepted() + ps_move_tracker_dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ps_move_tracker_dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/rpcs3/rpcs3qt/tooltips.h b/rpcs3/rpcs3qt/tooltips.h index 8549ad0137..99de286ec2 100644 --- a/rpcs3/rpcs3qt/tooltips.h +++ b/rpcs3/rpcs3qt/tooltips.h @@ -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 RPCS3 Wiki.").arg(gui::utils::get_link_style()); const QString evdev = tr("The evdev handler should work with any controller that has Linux support.
If your joystick is not being centered properly, read the RPCS3 Wiki 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.");