diff --git a/rpcs3/Emu/Cell/Modules/cellGem.cpp b/rpcs3/Emu/Cell/Modules/cellGem.cpp index 8475b57b04..128ffd0f51 100644 --- a/rpcs3/Emu/Cell/Modules/cellGem.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGem.cpp @@ -247,7 +247,7 @@ public: u32 hue = 0; // Tracking hue of the motion controller f32 distance_mm{3000.0f}; // Distance from the camera in mm f32 radius{5.0f}; // Radius of the sphere in camera pixels - bool radius_valid = true; // If the radius and distance of the sphere was computed. + bool radius_valid = false; // If the radius and distance of the sphere was computed. Also used for visibility. bool is_calibrating{false}; // Whether or not we are currently calibrating u64 calibration_start_us{0}; // The start timestamp of the calibration in microseconds @@ -321,13 +321,28 @@ public: void update_connections() { + connected_controllers = 0; + + const auto update_connection = [this](u32 i, bool connected) + { + if (connected) + { + connected_controllers++; + controllers[i].status = CELL_GEM_STATUS_READY; + controllers[i].port = port_num(i); + } + else + { + controllers[i].status = CELL_GEM_STATUS_DISCONNECTED; + controllers[i].port = 0; + } + }; + switch (g_cfg.io.move) { case move_handler::real: case move_handler::fake: { - connected_controllers = 0; - std::lock_guard lock(pad::g_pad_mutex); const auto handler = pad::get_pad_thread(true); if (!handler) break; @@ -335,51 +350,41 @@ public: for (u32 i = 0; i < CELL_GEM_MAX_NUM; i++) { const auto& pad = ::at32(handler->GetPads(), pad_num(i)); - const bool connected = (pad && (pad->m_port_status & CELL_PAD_STATUS_CONNECTED) && i < attribute.max_connect); + const bool connected = pad && (pad->m_port_status & CELL_PAD_STATUS_CONNECTED) && i < attribute.max_connect; const bool is_real_move = g_cfg.io.move != move_handler::real || pad->m_pad_handler == pad_handler::move; - if (connected && is_real_move) - { - connected_controllers++; - controllers[i].status = CELL_GEM_STATUS_READY; - controllers[i].port = port_num(i); - } - else - { - controllers[i].status = CELL_GEM_STATUS_DISCONNECTED; - controllers[i].port = 0; - } + update_connection(i, connected && is_real_move); } break; } + case move_handler::mouse: case move_handler::raw_mouse: { - connected_controllers = 0; - auto& handler = g_fxo->get(); std::lock_guard mouse_lock(handler.mutex); - const MouseInfo& info = handler.GetInfo(); for (u32 i = 0; i < CELL_GEM_MAX_NUM; i++) { - const bool connected = i < attribute.max_connect && info.status[i] == CELL_MOUSE_STATUS_CONNECTED; - - if (connected) - { - connected_controllers++; - controllers[i].status = CELL_GEM_STATUS_READY; - controllers[i].port = port_num(i); - } - else - { - controllers[i].status = CELL_GEM_STATUS_DISCONNECTED; - controllers[i].port = 0; - } + update_connection(i, i < attribute.max_connect && info.status[i] == CELL_MOUSE_STATUS_CONNECTED); } break; } - default: +#ifdef HAVE_LIBEVDEV + case move_handler::gun: + { + gun_thread& gun = g_fxo->get(); + std::scoped_lock lock(gun.handler.mutex); + gun.num_devices = gun.handler.init() ? gun.handler.get_num_guns() : 0; + + for (u32 i = 0; i < CELL_GEM_MAX_NUM; i++) + { + update_connection(i, i < attribute.max_connect && i < gun.num_devices); + } + break; + } +#endif + case move_handler::null: { break; } @@ -1598,13 +1603,17 @@ static inline void draw_overlay_cursor(u32 gem_num, const gem_config::gem_contro rsx::overlays::set_cursor(rsx::overlays::cursor_offset::cell_gem + gem_num, x, y, color, 2'000'000, false); } -static inline void pos_to_gem_image_state(u32 gem_num, const gem_config::gem_controller& controller, vm::ptr& gem_image_state, s32 x_pos, s32 y_pos, s32 x_max, s32 y_max) +static inline void pos_to_gem_image_state(u32 gem_num, gem_config::gem_controller& controller, vm::ptr& gem_image_state, s32 x_pos, s32 y_pos, s32 x_max, s32 y_max) { const auto& shared_data = g_fxo->get(); if (x_max <= 0) x_max = shared_data.width; if (y_max <= 0) y_max = shared_data.height; + // Move the cursor out of the screen if we're at the screen border (Time Crisis 4 needs this) + if (x_pos <= 0) x_pos -= x_max / 10; else if (x_pos >= x_max) x_pos += x_max / 10; + if (y_pos <= 0) y_pos -= y_max / 10; else if (y_pos >= y_max) y_pos += y_max / 10; + const f32 scaling_width = x_max / static_cast(shared_data.width); const f32 scaling_height = y_max / static_cast(shared_data.height); const f32 mmPerPixel = CELL_GEM_SPHERE_RADIUS_MM / controller.radius; @@ -1629,6 +1638,13 @@ static inline void pos_to_gem_image_state(u32 gem_num, const gem_config::gem_con gem_image_state->projectionx = camera_x / controller.distance_mm; gem_image_state->projectiony = camera_y / controller.distance_mm; + // Update visibility for fake handlers + if (g_cfg.io.move != move_handler::real) + { + // Let's say the sphere is not visible if the position is at the edge of the screen + controller.radius_valid = x_pos > 0 && x_pos < x_max && y_pos > 0 && y_pos < y_max; + } + if (g_cfg.io.show_move_cursor) { draw_overlay_cursor(gem_num, controller, x_pos, y_pos, x_max, y_max); @@ -1647,6 +1663,10 @@ static inline void pos_to_gem_state(u32 gem_num, gem_config::gem_controller& con if (x_max <= 0) x_max = shared_data.width; if (y_max <= 0) y_max = shared_data.height; + // Move the cursor out of the screen if we're at the screen border (Time Crisis 4 needs this) + if (x_pos <= 0) x_pos -= x_max / 10; else if (x_pos >= x_max) x_pos += x_max / 10; + if (y_pos <= 0) y_pos -= y_max / 10; else if (y_pos >= y_max) y_pos += y_max / 10; + const f32 scaling_width = x_max / static_cast(shared_data.width); const f32 scaling_height = y_max / static_cast(shared_data.height); const f32 mmPerPixel = CELL_GEM_SPHERE_RADIUS_MM / controller.radius; @@ -1712,6 +1732,13 @@ static inline void pos_to_gem_state(u32 gem_num, gem_config::gem_controller& con gem_state->quat[3] = q_w; } + // Update visibility for fake handlers + if (g_cfg.io.move != move_handler::real) + { + // Let's say the sphere is not visible if the position is at the edge of the screen + controller.radius_valid = x_pos > 0 && x_pos < x_max && y_pos > 0 && y_pos < y_max; + } + if (g_cfg.io.show_move_cursor) { draw_overlay_cursor(gem_num, controller, x_pos, y_pos, x_max, y_max); @@ -1730,7 +1757,7 @@ extern bool is_input_allowed(); * Unavoidably buttons conflict with DS3 mappings, which is problematic for some games. * \param gem_num gem index to use * \param digital_buttons Bitmask filled with CELL_GEM_CTRL_* values - * \param analog_t Analog value of Move's Trigger. Currently mapped to R2. + * \param analog_t Analog value of Move's Trigger. * \return true on success, false if controller is disconnected */ static void ds3_input_to_pad(const u32 gem_num, be_t& digital_buttons, be_t& analog_t) @@ -1810,22 +1837,17 @@ static inline void ds3_get_stick_values(u32 gem_num, const std::shared_ptr& const auto& cfg = ::at32(g_cfg_gem_fake.players, gem_num); cfg->handle_input(pad, true, [&](gem_btn btn, pad_button /*pad_btn*/, u16 value, bool pressed, bool& /*abort*/) - { - if (!pressed) - return; + { + if (!pressed) + return; - switch (btn) - { - case gem_btn::x_axis: - x_pos = value; - break; - case gem_btn::y_axis: - y_pos = value; - break; - default: - break; - } - }); + switch (btn) + { + case gem_btn::x_axis: x_pos = value; break; + case gem_btn::y_axis: y_pos = value; break; + default: break; + } + }); } template @@ -2095,7 +2117,7 @@ static bool mouse_input_to_pad(u32 mouse_no, be_t& digital_buttons, be_t& digital_buttons, be_t& if (gun.handler.get_button(gem_no, gun_button::btn_6) == 1) digital_buttons |= CELL_GEM_CTRL_SQUARE; - analog_t = gun.handler.get_button(gem_no, gun_button::btn_left) ? 0xFFFF : 0; + analog_t = gun.handler.get_button(gem_no, gun_button::btn_left) ? 255 : 0; return true; } @@ -2657,6 +2679,7 @@ error_code cellGemGetImageState(u32 gem_num, vm::ptr gem_imag cellGem.warning("cellGemGetImageState(gem_num=%d, image_state=&0x%x)", gem_num, gem_image_state); auto& gem = g_fxo->get(); + std::scoped_lock lock(gem.mtx); if (!gem.state) { @@ -2677,10 +2700,6 @@ error_code cellGemGetImageState(u32 gem_num, vm::ptr gem_imag gem_image_state->frame_timestamp = shared_data.frame_timestamp_us.load(); gem_image_state->timestamp = gem_image_state->frame_timestamp + 10; - gem_image_state->r = controller.radius; // Radius in camera pixels - gem_image_state->distance = controller.distance_mm; - gem_image_state->visible = gem.is_controller_ready(gem_num); - gem_image_state->r_valid = controller.radius_valid; switch (g_cfg.io.move) { @@ -2702,6 +2721,11 @@ error_code cellGemGetImageState(u32 gem_num, vm::ptr gem_imag case move_handler::null: fmt::throw_exception("Unreachable"); } + + gem_image_state->r = controller.radius; // Radius in camera pixels + gem_image_state->distance = controller.distance_mm; + gem_image_state->visible = controller.radius_valid && gem.is_controller_ready(gem_num); + gem_image_state->r_valid = controller.radius_valid; } return CELL_OK; diff --git a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp index 8e0678b510..cf4a6f265d 100644 --- a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp @@ -73,6 +73,19 @@ struct UsbPipe u8 endpoint = 0; }; +struct usb_allow_list_entry +{ + u16 id_vendor; + u16 id_product_min; + u16 id_product_max; + std::string_view device_name; + u16(*max_device_count)(void); + std::shared_ptr(*make_instance)(u32, const std::array&); + auto operator<(const usb_allow_list_entry& r) const + { + return std::tuple(id_vendor, id_product_min, id_product_max, device_name, max_device_count, make_instance) < std::tuple(r.id_vendor, r.id_product_min, r.id_product_max, device_name, max_device_count, make_instance); + } +}; class usb_handler_thread { public: @@ -132,6 +145,8 @@ public: shared_mutex mutex_sq; ppu_thread* sq{}; + atomic_t usb_hotplug_timeout = umax; + static constexpr auto thread_name = "Usb Manager Thread"sv; private: @@ -140,6 +155,7 @@ private: u32 get_free_transfer_id(); void send_message(u32 message, u32 tr_id); + void perform_scan(); private: // Counters for device IDs, transfer IDs and pipe IDs @@ -150,6 +166,101 @@ private: // List of device drivers std::unordered_map> ldds; + const std::vector device_allow_list + { + // Portals + {0x1430, 0x0150, 0x0150, "Skylanders Portal", &usb_device_skylander::get_num_emu_devices, &usb_device_skylander::make_instance}, + {0x0E6F, 0x0129, 0x0129, "Disney Infinity Base", &usb_device_infinity::get_num_emu_devices, &usb_device_infinity::make_instance}, + {0x0E6F, 0x0241, 0x0241, "Lego Dimensions Portal", &usb_device_dimensions::get_num_emu_devices, &usb_device_dimensions::make_instance}, + {0x0E6F, 0x200A, 0x200A, "Kamen Rider Summonride Portal", nullptr, nullptr}, + + // Cameras + // {0x1415, 0x0020, 0x2000, "Sony Playstation Eye", nullptr, nullptr}, // TODO: verifiy + + // Music devices + {0x1415, 0x0000, 0x0000, "Singstar Microphone", nullptr, nullptr}, + // {0x1415, 0x0020, 0x0020, "SingStar Microphone Wireless", nullptr, nullptr}, // TODO: verifiy + + {0x12BA, 0x00FF, 0x00FF, "Rocksmith Guitar Adapter", nullptr, nullptr}, + {0x12BA, 0x0100, 0x0100, "Guitar Hero Guitar", nullptr, nullptr}, + {0x12BA, 0x0120, 0x0120, "Guitar Hero Drums", nullptr, nullptr}, + {0x12BA, 0x074B, 0x074B, "Guitar Hero Live Guitar", &usb_device_ghltar::get_num_emu_devices, &usb_device_ghltar::make_instance}, + + {0x12BA, 0x0140, 0x0140, "DJ Hero Turntable", &usb_device_turntable::get_num_emu_devices, &usb_device_turntable::make_instance}, + {0x12BA, 0x0200, 0x020F, "Harmonix Guitar", nullptr, nullptr}, + {0x12BA, 0x0210, 0x021F, "Harmonix Drums", nullptr, nullptr}, + {0x12BA, 0x2330, 0x233F, "Harmonix Keyboard", nullptr, nullptr}, + {0x12BA, 0x2430, 0x243F, "Harmonix Button Guitar", nullptr, nullptr}, + {0x12BA, 0x2530, 0x253F, "Harmonix Real Guitar", nullptr, nullptr}, + + {0x1BAD, 0x0004, 0x0004, "Harmonix RB1 Guitar - Wii", nullptr, nullptr}, + {0x1BAD, 0x0005, 0x0005, "Harmonix RB1 Drums - Wii", nullptr, nullptr}, + {0x1BAD, 0x3010, 0x301F, "Harmonix RB2 Guitar - Wii", nullptr, nullptr}, + {0x1BAD, 0x3110, 0x313F, "Harmonix RB2 Drums - Wii", nullptr, nullptr}, + {0x1BAD, 0x3330, 0x333F, "Harmonix Keyboard - Wii", nullptr, nullptr}, + {0x1BAD, 0x3430, 0x343F, "Harmonix Button Guitar - Wii", nullptr, nullptr}, + {0x1BAD, 0x3530, 0x353F, "Harmonix Real Guitar - Wii", nullptr, nullptr}, + + //Top Shot Elite controllers + {0x12BA, 0x04A0, 0x04A0, "Top Shot Elite", nullptr, nullptr}, + {0x12BA, 0x04A1, 0x04A1, "Top Shot Fearmaster", nullptr, nullptr}, + {0x12BA, 0x04B0, 0x04B0, "Rapala Fishing Rod", nullptr, nullptr}, + + + // GT5 Wheels&co + {0x046D, 0xC283, 0xC29B, "lgFF_c283_c29b", nullptr, nullptr}, + {0x044F, 0xB653, 0xB653, "Thrustmaster RGT FFB Pro", nullptr, nullptr}, + {0x044F, 0xB65A, 0xB65A, "Thrustmaster F430", nullptr, nullptr}, + {0x044F, 0xB65D, 0xB65D, "Thrustmaster FFB", nullptr, nullptr}, + {0x044F, 0xB65E, 0xB65E, "Thrustmaster TRS", nullptr, nullptr}, + {0x044F, 0xB660, 0xB660, "Thrustmaster T500 RS Gear Shift", nullptr, nullptr}, + + // GT6 + {0x2833, 0x0001, 0x0001, "Oculus", nullptr, nullptr}, + {0x046D, 0xCA03, 0xCA03, "lgFF_ca03_ca03", nullptr, nullptr}, + + // Buzz controllers + {0x054C, 0x1000, 0x1040, "buzzer0", &usb_device_buzz::get_num_emu_devices, &usb_device_buzz::make_instance}, + {0x054C, 0x0001, 0x0041, "buzzer1", nullptr, nullptr}, + {0x054C, 0x0042, 0x0042, "buzzer2", nullptr, nullptr}, + {0x046D, 0xC220, 0xC220, "buzzer9", nullptr, nullptr}, + + // GCon3 Gun + {0x0B9A, 0x0800, 0x0800, "guncon3", nullptr, nullptr}, + + // uDraw GameTablet + {0x20D6, 0xCB17, 0xCB17, "uDraw GameTablet", nullptr, nullptr}, + + // DVB-T + {0x1415, 0x0003, 0x0003, "PlayTV SCEH-0036", nullptr, nullptr}, + + // PSP Devices + {0x054C, 0x01C8, 0x01C8, "PSP Type A", nullptr, nullptr}, + {0x054C, 0x01C9, 0x01C9, "PSP Type B", nullptr, nullptr}, + {0x054C, 0x01CA, 0x01CA, "PSP Type C", nullptr, nullptr}, + {0x054C, 0x01CB, 0x01CB, "PSP Type D", nullptr, nullptr}, + {0x054C, 0x02D2, 0x02D2, "PSP Slim", nullptr, nullptr}, + + // 0x0900: "H050 USJ(C) PCB rev00", 0x0910: "USIO PCB rev00" + {0x0B9A, 0x0900, 0x0910, "PS3A-USJ", &usb_device_usio::get_num_emu_devices, &usb_device_usio::make_instance}, + + // Densha de GO! controller + {0x0AE4, 0x0004, 0x0004, "Densha de GO! Type 2 Controller", nullptr, nullptr}, + + // EA Active 2 dongle for connecting wristbands & legband + {0x21A4, 0xAC27, 0xAC27, "EA Active 2 Dongle", nullptr, nullptr}, + + // Tony Hawk RIDE Skateboard + {0x12BA, 0x0400, 0x0400, "Tony Hawk RIDE Skateboard Controller", nullptr, nullptr}, + + // PSP in UsbPspCm mode + {0x054C, 0x01CB, 0x01CB, "UsbPspcm", nullptr, nullptr}, + + // Sony Stereo Headsets + {0x12BA, 0x0032, 0x0032, "Wireless Stereo Headset", nullptr, nullptr}, + {0x12BA, 0x0042, 0x0042, "Wireless Stereo Headset", nullptr, nullptr}, + }; + // List of pipes std::map open_pipes; // Transfers infos @@ -163,8 +274,15 @@ private: // List of devices "connected" to the ps3 std::array location{}; std::vector> usb_devices; + std::unordered_map> usb_passthrough_devices; libusb_context* ctx = nullptr; + +#if LIBUSB_API_VERSION >= 0x01000102 + libusb_hotplug_callback_handle callback_handle {}; +#endif + + bool hotplug_supported = false; }; void LIBUSB_CALL callback_transfer(struct libusb_transfer* transfer) @@ -177,6 +295,14 @@ void LIBUSB_CALL callback_transfer(struct libusb_transfer* transfer) usbh.transfer_complete(transfer); } +#if LIBUSB_API_VERSION >= 0x01000102 +static int LIBUSB_CALL hotplug_callback(libusb_context* /*ctx*/, libusb_device * /*dev*/, libusb_hotplug_event event, void * /*user_data*/) +{ + handle_hotplug_event(event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED); + return 0; +} +#endif + #if LIBUSB_API_VERSION >= 0x0100010A static void LIBUSB_CALL log_cb(libusb_context* /*ctx*/, enum libusb_log_level level, const char* str) { @@ -205,6 +331,93 @@ static void LIBUSB_CALL log_cb(libusb_context* /*ctx*/, enum libusb_log_level le } #endif +void usb_handler_thread::perform_scan() +{ + // look if any device which we could be interested in is actually connected + libusb_device** list = nullptr; + const ssize_t ndev = libusb_get_device_list(ctx, &list); + std::set seen_usb_devices; + + if (ndev < 0) + { + sys_usbd.error("Failed to get device list: %s", libusb_error_name(static_cast(ndev))); + return; + } + + for (ssize_t index = 0; index < ndev; index++) + { + libusb_device* dev = list[index]; + libusb_device_descriptor desc; + if (int res = libusb_get_device_descriptor(dev, &desc); res < 0) + { + sys_usbd.error("Failed to get device descriptor: %s", libusb_error_name(res)); + continue; + } + + const u8 port = libusb_get_port_number(dev); + const u8 address = libusb_get_device_address(dev); + const u64 usb_id = (static_cast(desc.idVendor) << 48) | (static_cast(desc.idProduct) << 32) | (static_cast(port) << 8) | address; + + seen_usb_devices.insert(usb_id); + if (usb_passthrough_devices.contains(usb_id)) + { + continue; + } + + for (const auto& entry : device_allow_list) + { + // attach + if (desc.idVendor == entry.id_vendor + && desc.idProduct >= entry.id_product_min + && desc.idProduct <= entry.id_product_max) + { + sys_usbd.success("Found device: %s", std::basic_string(entry.device_name)); + libusb_ref_device(dev); + std::shared_ptr usb_dev = std::make_shared(dev, desc, get_new_location()); + connect_usb_device(usb_dev, true); + usb_passthrough_devices[usb_id] = usb_dev; + } + } + + if (desc.idVendor == 0x1209 && desc.idProduct == 0x2882) + { + sys_usbd.success("Found device: Santroller"); + // Send the device a specific control transfer so that it jumps to a RPCS3 compatible mode + libusb_device_handle* lusb_handle; + if (libusb_open(dev, &lusb_handle) == LIBUSB_SUCCESS) + { +#ifdef __linux__ + libusb_set_auto_detach_kernel_driver(lusb_handle, true); + libusb_claim_interface(lusb_handle, 2); +#endif + libusb_control_transfer(lusb_handle, +LIBUSB_ENDPOINT_IN | +LIBUSB_REQUEST_TYPE_CLASS | +LIBUSB_RECIPIENT_INTERFACE, 0x01, 0x03f2, 2, nullptr, 0, 5000); + libusb_close(lusb_handle); + } + else + { + sys_usbd.error("Unable to open Santroller device, make sure Santroller isn't open in the background."); + } + } + } + + for (auto it = usb_passthrough_devices.begin(); it != usb_passthrough_devices.end();) + { + auto& dev = *it; + // If a device is no longer visible, disconnect it + if (seen_usb_devices.contains(dev.first)) + { + ++it; + } + else + { + disconnect_usb_device(dev.second, true); + it = usb_passthrough_devices.erase(it); + } + + } + libusb_free_device_list(list, 1); +} + usb_handler_thread::usb_handler_thread() { #if LIBUSB_API_VERSION >= 0x0100010A @@ -230,164 +443,97 @@ usb_handler_thread::usb_handler_thread() return; } +#ifdef _WIN32 + hotplug_supported = true; +#elif LIBUSB_API_VERSION >= 0x01000102 + if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) + { + if (int res = libusb_hotplug_register_callback(ctx, static_cast(LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | + LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT), static_cast(0), LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, + LIBUSB_HOTPLUG_MATCH_ANY, static_cast(hotplug_callback), nullptr, + &callback_handle); res < 0) + { + sys_usbd.error("Failed to initialize sys_usbd hotplug: %s", libusb_error_name(res)); + } + else + { + hotplug_supported = true; + } + } +#endif + for (u32 index = 0; index < MAX_SYS_USBD_TRANSFERS; index++) { transfers[index].transfer = libusb_alloc_transfer(8); transfers[index].transfer_id = index; } - // look if any device which we could be interested in is actually connected - libusb_device** list = nullptr; - ssize_t ndev = libusb_get_device_list(ctx, &list); - - if (ndev < 0) + if (!g_cfg_usio.load()) { - sys_usbd.error("Failed to get device list: %s", libusb_error_name(static_cast(ndev))); - return; + sys_usbd.notice("Could not load usio config. Using defaults."); } - bool found_skylander = false; - bool found_infinity = false; - bool found_dimension = false; - bool found_usj = false; + sys_usbd.notice("USIO config=\n", g_cfg_usio.to_string()); - for (ssize_t index = 0; index < ndev; index++) + if (g_cfg.io.ghltar == ghltar_handler::one_controller || g_cfg.io.ghltar == ghltar_handler::two_controllers) { - libusb_device_descriptor desc; - if (int res = libusb_get_device_descriptor(list[index], &desc); res < 0) + if (!g_cfg_ghltar.load()) { - sys_usbd.error("Failed to get device descriptor: %s", libusb_error_name(res)); - continue; + sys_usbd.notice("Could not load ghltar config. Using defaults."); } - auto check_device = [&](const u16 id_vendor, const u16 id_product_min, const u16 id_product_max, const char* s_name) -> bool - { - if (desc.idVendor == id_vendor && desc.idProduct >= id_product_min && desc.idProduct <= id_product_max) - { - sys_usbd.success("Found device: %s", s_name); - libusb_ref_device(list[index]); - std::shared_ptr usb_dev = std::make_shared(list[index], desc, get_new_location()); - usb_devices.push_back(usb_dev); - return true; - } - return false; - }; - - // Portals - if (check_device(0x1430, 0x0150, 0x0150, "Skylanders Portal")) - { - found_skylander = true; - } - - if (check_device(0x0E6F, 0x0129, 0x0129, "Disney Infinity Base")) - { - found_infinity = true; - } - - if (check_device(0x0E6F, 0x0241, 0x0241, "Lego Dimensions Portal")) - { - found_dimension = true; - } - - check_device(0x0E6F, 0x200A, 0x200A, "Kamen Rider Summonride Portal"); - - // Cameras - // check_device(0x1415, 0x0020, 0x2000, "Sony Playstation Eye"); // TODO: verifiy - - // Music devices - check_device(0x1415, 0x0000, 0x0000, "SingStar Microphone"); - // check_device(0x1415, 0x0020, 0x0020, "SingStar Microphone Wireless"); // TODO: verifiy - check_device(0x12BA, 0x0100, 0x0100, "Guitar Hero Guitar"); - check_device(0x12BA, 0x0120, 0x0120, "Guitar Hero Drums"); - check_device(0x12BA, 0x074B, 0x074B, "Guitar Hero Live Guitar"); - - check_device(0x12BA, 0x0140, 0x0140, "DJ Hero Turntable"); - check_device(0x12BA, 0x0200, 0x020F, "Harmonix Guitar"); - check_device(0x12BA, 0x0210, 0x021F, "Harmonix Drums"); - check_device(0x12BA, 0x2330, 0x233F, "Harmonix Keyboard"); - check_device(0x12BA, 0x2430, 0x243F, "Harmonix Button Guitar"); - check_device(0x12BA, 0x2530, 0x253F, "Harmonix Real Guitar"); - - check_device(0x1BAD, 0x0004, 0x0004, "Harmonix RB1 Guitar - Wii"); - check_device(0x1BAD, 0x0005, 0x0005, "Harmonix RB1 Drums - Wii"); - check_device(0x1BAD, 0x3010, 0x301F, "Harmonix RB2 Guitar - Wii"); - check_device(0x1BAD, 0x3110, 0x313F, "Harmonix RB2 Drums - Wii"); - check_device(0x1BAD, 0x3330, 0x333F, "Harmonix Keyboard - Wii"); - check_device(0x1BAD, 0x3430, 0x343F, "Harmonix Button Guitar - Wii"); - check_device(0x1BAD, 0x3530, 0x353F, "Harmonix Real Guitar - Wii"); - - if (desc.idVendor == 0x1209 && desc.idProduct == 0x2882) - { - sys_usbd.success("Found device: Santroller"); - // Send the device a specific control transfer so that it jumps to a RPCS3 compatible mode - libusb_device_handle* lusb_handle; - if (libusb_open(list[index], &lusb_handle) == LIBUSB_SUCCESS) - { -#ifdef __linux__ - libusb_set_auto_detach_kernel_driver(lusb_handle, true); - libusb_claim_interface(lusb_handle, 2); -#endif - libusb_control_transfer(lusb_handle, +LIBUSB_ENDPOINT_IN | +LIBUSB_REQUEST_TYPE_CLASS | +LIBUSB_RECIPIENT_INTERFACE, 0x01, 0x03f2, 2, nullptr, 0, 5000); - libusb_close(lusb_handle); - } - else - { - sys_usbd.error("Unable to open Santroller device, make sure Santroller isn't open in the background."); - } - } - - // Top Shot Elite controllers - check_device(0x12BA, 0x04A0, 0x04A0, "Top Shot Elite"); - check_device(0x12BA, 0x04A1, 0x04A1, "Top Shot Fearmaster"); - check_device(0x12BA, 0x04B0, 0x04B0, "Rapala Fishing Rod"); - - // GT5 Wheels&co - check_device(0x046D, 0xC283, 0xC29B, "lgFF_c283_c29b"); - check_device(0x044F, 0xB653, 0xB653, "Thrustmaster RGT FFB Pro"); - check_device(0x044F, 0xB65A, 0xB65A, "Thrustmaster F430"); - check_device(0x044F, 0xB65D, 0xB65D, "Thrustmaster FFB"); - check_device(0x044F, 0xB65E, 0xB65E, "Thrustmaster TRS"); - check_device(0x044F, 0xB660, 0xB660, "Thrustmaster T500 RS Gear Shift"); - - // GT6 - check_device(0x2833, 0x0001, 0x0001, "Oculus"); - check_device(0x046D, 0xCA03, 0xCA03, "lgFF_ca03_ca03"); - - // Buzz controllers - check_device(0x054C, 0x1000, 0x1040, "buzzer0"); - check_device(0x054C, 0x0001, 0x0041, "buzzer1"); - check_device(0x054C, 0x0042, 0x0042, "buzzer2"); - check_device(0x046D, 0xC220, 0xC220, "buzzer9"); - - // GunCon3 Gun - check_device(0x0B9A, 0x0800, 0x0800, "GunCon3"); - - // uDraw GameTablet - check_device(0x20D6, 0xCB17, 0xCB17, "uDraw GameTablet"); - - // DVB-T - check_device(0x1415, 0x0003, 0x0003, "PlayTV SCEH-0036"); - - // 0x0900: "H050 USJ(C) PCB rev00", 0x0910: "USIO PCB rev00" - if (check_device(0x0B9A, 0x0900, 0x0910, "PS3A-USJ")) - { - found_usj = true; - } - - // Densha de GO! controller - check_device(0x0AE4, 0x0004, 0x0004, "Densha de GO! Type 2 Controller"); - - // EA Active 2 dongle for connecting wristbands & legband - check_device(0x21A4, 0xAC27, 0xAC27, "EA Active 2 Dongle"); - - // Tony Hawk RIDE Skateboard - check_device(0x12BA, 0x0400, 0x0400, "Tony Hawk RIDE Skateboard Controller"); - - // PSP in UsbPspCm mode - check_device(0x054C, 0x01CB, 0x01CB, "UsbPspcm"); + sys_usbd.notice("Ghltar config=\n", g_cfg_ghltar.to_string()); } - libusb_free_device_list(list, 1); + if (g_cfg.io.turntable == turntable_handler::one_controller || g_cfg.io.turntable == turntable_handler::two_controllers) + { + if (!g_cfg_turntable.load()) + { + sys_usbd.notice("Could not load turntable config. Using defaults."); + } + + sys_usbd.notice("Turntable config=\n", g_cfg_turntable.to_string()); + } + + if (g_cfg.io.buzz == buzz_handler::one_controller || g_cfg.io.buzz == buzz_handler::two_controllers) + { + if (!g_cfg_buzz.load()) + { + sys_usbd.notice("Could not load buzz config. Using defaults."); + } + + sys_usbd.notice("Buzz config=\n", g_cfg_buzz.to_string()); + } + + perform_scan(); + + // Set up emulated devices for any devices that are not already being passed through + std::map emulate_device_check; + for (const auto& dev : usb_devices) + { + for (const auto& entry : device_allow_list) + { + const u16 idVendor = dev->device._device.idVendor; + const u16 idProduct = dev->device._device.idProduct; + if (entry.max_device_count != nullptr && (idVendor == entry.id_vendor && idProduct >= entry.id_product_min && idProduct <= entry.id_product_max)) + { + emulate_device_check[entry]++; + } + } + } + + for (const auto& [entry, count] : emulate_device_check) + { + if (entry.max_device_count && entry.make_instance) + { + for (int i = count; i < entry.max_device_count(); i++) + { + sys_usbd.success("Emulating device: %s (%d)", std::basic_string(entry.device_name), i); + auto usb_dev = entry.make_instance(i, get_new_location()); + connect_usb_device(usb_dev, true); + } + } + } for (int i = 0; i < 8; i++) // Add VFS USB mass storage devices (/dev_usbXXX) to the USB device list { @@ -396,37 +542,6 @@ usb_handler_thread::usb_handler_thread() usb_devices.push_back(std::make_shared(usb_info, get_new_location())); } - if (!found_skylander) - { - sys_usbd.notice("Adding emulated skylander"); - usb_devices.push_back(std::make_shared(get_new_location())); - } - - if (!found_infinity) - { - sys_usbd.notice("Adding emulated infinity base"); - usb_devices.push_back(std::make_shared(get_new_location())); - } - - if (!found_dimension) - { - sys_usbd.notice("Adding emulated dimension toypad"); - usb_devices.push_back(std::make_shared(get_new_location())); - } - - if (!found_usj) - { - if (!g_cfg_usio.load()) - { - sys_usbd.notice("Could not load usio config. Using defaults."); - } - - sys_usbd.notice("Adding emulated USIO"); - usb_devices.push_back(std::make_shared(get_new_location())); - - sys_usbd.notice("USIO config=\n", g_cfg_usio.to_string()); - } - const std::vector devices_list = fmt::split(g_cfg.io.midi_devices.to_string(), { "@@@" }); for (usz index = 0; index < std::min(max_midi_devices, devices_list.size()); index++) { @@ -458,65 +573,6 @@ usb_handler_thread::usb_handler_thread() break; } } - - if (g_cfg.io.ghltar == ghltar_handler::one_controller || g_cfg.io.ghltar == ghltar_handler::two_controllers) - { - if (!g_cfg_ghltar.load()) - { - sys_usbd.notice("Could not load ghltar config. Using defaults."); - } - - sys_usbd.notice("Adding emulated GHLtar (1 player)"); - usb_devices.push_back(std::make_shared(0, get_new_location())); - - if (g_cfg.io.ghltar == ghltar_handler::two_controllers) - { - sys_usbd.notice("Adding emulated GHLtar (2 players)"); - usb_devices.push_back(std::make_shared(1, get_new_location())); - } - - sys_usbd.notice("Ghltar config=\n", g_cfg_ghltar.to_string()); - } - - if (g_cfg.io.turntable == turntable_handler::one_controller || g_cfg.io.turntable == turntable_handler::two_controllers) - { - if (!g_cfg_turntable.load()) - { - sys_usbd.notice("Could not load turntable config. Using defaults."); - } - - sys_usbd.notice("Adding emulated turntable (1 player)"); - usb_devices.push_back(std::make_shared(0, get_new_location())); - - if (g_cfg.io.turntable == turntable_handler::two_controllers) - { - sys_usbd.notice("Adding emulated turntable (2 players)"); - usb_devices.push_back(std::make_shared(1, get_new_location())); - } - - sys_usbd.notice("Turntable config=\n", g_cfg_turntable.to_string()); - } - - if (g_cfg.io.buzz == buzz_handler::one_controller || g_cfg.io.buzz == buzz_handler::two_controllers) - { - if (!g_cfg_buzz.load()) - { - sys_usbd.notice("Could not load buzz config. Using defaults."); - } - - sys_usbd.notice("Adding emulated Buzz! buzzer (1-4 players)"); - usb_devices.push_back(std::make_shared(0, 3, get_new_location())); - - if (g_cfg.io.buzz == buzz_handler::two_controllers) - { - // The current buzz emulation piggybacks on the pad input. - // Since there can only be 7 pads connected on a PS3 the 8th player is currently not supported - sys_usbd.notice("Adding emulated Buzz! buzzer (5-7 players)"); - usb_devices.push_back(std::make_shared(4, 6, get_new_location())); - } - - sys_usbd.notice("Buzz config=\n", g_cfg_buzz.to_string()); - } } usb_handler_thread::~usb_handler_thread() @@ -525,6 +581,7 @@ usb_handler_thread::~usb_handler_thread() handled_devices.clear(); open_pipes.clear(); usb_devices.clear(); + usb_passthrough_devices.clear(); for (u32 index = 0; index < MAX_SYS_USBD_TRANSFERS; index++) { @@ -532,6 +589,10 @@ usb_handler_thread::~usb_handler_thread() libusb_free_transfer(transfers[index].transfer); } +#if LIBUSB_API_VERSION >= 0x01000102 + libusb_hotplug_deregister_callback(ctx, callback_handle); +#endif + if (ctx) libusb_exit(ctx); } @@ -539,10 +600,21 @@ usb_handler_thread::~usb_handler_thread() void usb_handler_thread::operator()() { timeval lusb_tv{0, 0}; - + if (!hotplug_supported) + { + usb_hotplug_timeout = get_system_time() + 4'000'000ull; + } while (ctx && thread_ctrl::state() != thread_state::aborting) { - // Todo: Hotplug here? + const u64 now = get_system_time(); + if (now > usb_hotplug_timeout) + { + // If we did the hotplug scan each cycle the game performance was significantly degraded, so we only perform this scan + // every 4 seconds. + // On systems where hotplug is native, we wait a little bit for devices to settle before we start the scan + perform_scan(); + usb_hotplug_timeout = hotplug_supported ? umax : get_system_time() + 4'000'000ull; + } // Process asynchronous requests that are pending libusb_handle_events_timeout_completed(ctx, &lusb_tv, nullptr); @@ -836,6 +908,7 @@ void usb_handler_thread::connect_usb_device(std::shared_ptr dev, boo if (!dev->open_device()) { sys_usbd.error("Failed to open USB device(VID=0x%04x, PID=0x%04x) for LDD <%s>", dev->device._device.idVendor, dev->device._device.idProduct, name); + disconnect_usb_device(dev); return; } @@ -844,6 +917,7 @@ void usb_handler_thread::connect_usb_device(std::shared_ptr dev, boo handled_devices.emplace(dev->assigned_number, std::pair(UsbInternalDevice{0x00, narrow(dev->assigned_number), 0x02, 0x40}, dev)); send_message(SYS_USBD_ATTACH, dev->assigned_number); sys_usbd.success("USB device(VID=0x%04x, PID=0x%04x) matches up with LDD <%s>, assigned as handled_device=0x%x", dev->device._device.idVendor, dev->device._device.idProduct, name, dev->assigned_number); + return; } } } @@ -855,10 +929,18 @@ void usb_handler_thread::disconnect_usb_device(std::shared_ptr dev, send_message(SYS_USBD_DETACH, dev->assigned_number); sys_usbd.success("USB device(VID=0x%04x, PID=0x%04x) unassigned, handled_device=0x%x", dev->device._device.idVendor, dev->device._device.idProduct, dev->assigned_number); dev->assigned_number = 0; + std::erase_if(open_pipes, [&](const auto& val) + { + return val.second.device == dev; + }); } - if (update_usb_devices) - usb_devices.erase(find(usb_devices.begin(), usb_devices.end(), dev)); + { + std::erase_if(usb_devices, [&](const auto& val) + { + return val == dev; + }); + } } void connect_usb_controller(u8 index, input::product_type type) @@ -947,6 +1029,14 @@ void connect_usb_controller(u8 index, input::product_type type) } } +void handle_hotplug_event(bool connected) +{ + if (auto usbh = g_fxo->try_get>()) + { + usbh->usb_hotplug_timeout = get_system_time() + (connected ? 1'000'000ull : 0); + } +} + error_code sys_usbd_initialize(ppu_thread& ppu, vm::ptr handle) { diff --git a/rpcs3/Emu/Cell/lv2/sys_usbd.h b/rpcs3/Emu/Cell/lv2/sys_usbd.h index 9c81fb7b80..a2fd911e35 100644 --- a/rpcs3/Emu/Cell/lv2/sys_usbd.h +++ b/rpcs3/Emu/Cell/lv2/sys_usbd.h @@ -89,3 +89,4 @@ error_code sys_usbd_register_extra_ldd(ppu_thread& ppu, u32 handle, vm::cptr s_product, u16 slen_product); void connect_usb_controller(u8 index, input::product_type); +void handle_hotplug_event(bool connected); diff --git a/rpcs3/Emu/Io/Buzz.cpp b/rpcs3/Emu/Io/Buzz.cpp index 575c5585b1..7c7890d41e 100644 --- a/rpcs3/Emu/Io/Buzz.cpp +++ b/rpcs3/Emu/Io/Buzz.cpp @@ -4,6 +4,7 @@ #include "Buzz.h" #include "Emu/Cell/lv2/sys_usbd.h" #include "Emu/Io/buzz_config.h" +#include "Emu/system_config.h" #include "Input/pad_thread.h" LOG_CHANNEL(buzz_log, "BUZZ"); @@ -85,6 +86,23 @@ usb_device_buzz::~usb_device_buzz() { } +std::shared_ptr usb_device_buzz::make_instance(u32 controller_index, const std::array& location) +{ + if (controller_index == 0) + { + return std::make_shared(0, 3, location); + } + + // The current buzz emulation piggybacks on the pad input. + // Since there can only be 7 pads connected on a PS3 the 8th player is currently not supported + return std::make_shared(4, 6, location); +} + +u16 usb_device_buzz::get_num_emu_devices() +{ + return static_cast(g_cfg.io.buzz.get()); +} + void usb_device_buzz::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) { transfer->fake = true; diff --git a/rpcs3/Emu/Io/Buzz.h b/rpcs3/Emu/Io/Buzz.h index 0515dbac71..e8aafc0e35 100644 --- a/rpcs3/Emu/Io/Buzz.h +++ b/rpcs3/Emu/Io/Buzz.h @@ -8,6 +8,9 @@ public: usb_device_buzz(u32 first_controller, u32 last_controller, const std::array& location); ~usb_device_buzz(); + static std::shared_ptr make_instance(u32 controller_index, const std::array& location); + static u16 get_num_emu_devices(); + void control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) override; void interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) override; diff --git a/rpcs3/Emu/Io/Dimensions.cpp b/rpcs3/Emu/Io/Dimensions.cpp index 40216e9ba4..9a61f7abce 100644 --- a/rpcs3/Emu/Io/Dimensions.cpp +++ b/rpcs3/Emu/Io/Dimensions.cpp @@ -573,6 +573,16 @@ usb_device_dimensions::~usb_device_dimensions() { } +std::shared_ptr usb_device_dimensions::make_instance(u32, const std::array& location) +{ + return std::make_shared(location); +} + +u16 usb_device_dimensions::get_num_emu_devices() +{ + return 1; +} + void usb_device_dimensions::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) { usb_device_emulated::control_transfer(bmRequestType, bRequest, wValue, wIndex, wLength, buf_size, buf, transfer); diff --git a/rpcs3/Emu/Io/Dimensions.h b/rpcs3/Emu/Io/Dimensions.h index db1c6577c3..9cf1bd7395 100644 --- a/rpcs3/Emu/Io/Dimensions.h +++ b/rpcs3/Emu/Io/Dimensions.h @@ -71,6 +71,9 @@ public: usb_device_dimensions(const std::array& location); ~usb_device_dimensions(); + static std::shared_ptr make_instance(u32 controller_index, const std::array& location); + static u16 get_num_emu_devices(); + void control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) override; void interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) override; void isochronous_transfer(UsbTransfer* transfer) override; diff --git a/rpcs3/Emu/Io/GHLtar.cpp b/rpcs3/Emu/Io/GHLtar.cpp index db60cccd5b..e520e53c7f 100644 --- a/rpcs3/Emu/Io/GHLtar.cpp +++ b/rpcs3/Emu/Io/GHLtar.cpp @@ -4,6 +4,7 @@ #include "GHLtar.h" #include "Emu/Cell/lv2/sys_usbd.h" #include "Emu/Io/ghltar_config.h" +#include "Emu/system_config.h" #include "Input/pad_thread.h" LOG_CHANNEL(ghltar_log, "GHLTAR"); @@ -52,6 +53,16 @@ usb_device_ghltar::~usb_device_ghltar() { } +std::shared_ptr usb_device_ghltar::make_instance(u32 controller_index, const std::array& location) +{ + return std::make_shared(controller_index, location); +} + +u16 usb_device_ghltar::get_num_emu_devices() +{ + return static_cast(g_cfg.io.ghltar.get()); +} + void usb_device_ghltar::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) { transfer->fake = true; diff --git a/rpcs3/Emu/Io/GHLtar.h b/rpcs3/Emu/Io/GHLtar.h index b75fcc8df9..e36df36498 100644 --- a/rpcs3/Emu/Io/GHLtar.h +++ b/rpcs3/Emu/Io/GHLtar.h @@ -8,6 +8,9 @@ public: usb_device_ghltar(u32 controller_index, const std::array& location); ~usb_device_ghltar(); + static std::shared_ptr make_instance(u32 controller_index, const std::array& location); + static u16 get_num_emu_devices(); + void control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) override; void interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) override; diff --git a/rpcs3/Emu/Io/Infinity.cpp b/rpcs3/Emu/Io/Infinity.cpp index a91c295028..ca70c92344 100644 --- a/rpcs3/Emu/Io/Infinity.cpp +++ b/rpcs3/Emu/Io/Infinity.cpp @@ -373,6 +373,16 @@ usb_device_infinity::~usb_device_infinity() { } +std::shared_ptr usb_device_infinity::make_instance(u32, const std::array& location) +{ + return std::make_shared(location); +} + +u16 usb_device_infinity::get_num_emu_devices() +{ + return 1; +} + void usb_device_infinity::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) { usb_device_emulated::control_transfer(bmRequestType, bRequest, wValue, wIndex, wLength, buf_size, buf, transfer); diff --git a/rpcs3/Emu/Io/Infinity.h b/rpcs3/Emu/Io/Infinity.h index 1aaeebeb41..2709a0c266 100644 --- a/rpcs3/Emu/Io/Infinity.h +++ b/rpcs3/Emu/Io/Infinity.h @@ -60,6 +60,9 @@ public: usb_device_infinity(const std::array& location); ~usb_device_infinity(); + static std::shared_ptr make_instance(u32 controller_index, const std::array& location); + static u16 get_num_emu_devices(); + void control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) override; void interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) override; diff --git a/rpcs3/Emu/Io/Skylander.cpp b/rpcs3/Emu/Io/Skylander.cpp index 7f5b7628ec..0867918fd7 100644 --- a/rpcs3/Emu/Io/Skylander.cpp +++ b/rpcs3/Emu/Io/Skylander.cpp @@ -208,6 +208,16 @@ usb_device_skylander::~usb_device_skylander() { } +std::shared_ptr usb_device_skylander::make_instance(u32, const std::array& location) +{ + return std::make_shared(location); +} + +u16 usb_device_skylander::get_num_emu_devices() +{ + return 1; +} + void usb_device_skylander::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) { transfer->fake = true; @@ -342,7 +352,7 @@ void usb_device_skylander::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoin transfer->fake = true; transfer->expected_count = buf_size; transfer->expected_result = HC_CC_NOERR; - + if (endpoint == 0x02) { // Audio transfers are fairly quick(~1ms) diff --git a/rpcs3/Emu/Io/Skylander.h b/rpcs3/Emu/Io/Skylander.h index da7dc3dfa4..be67c8c3cc 100644 --- a/rpcs3/Emu/Io/Skylander.h +++ b/rpcs3/Emu/Io/Skylander.h @@ -46,6 +46,9 @@ public: usb_device_skylander(const std::array& location); ~usb_device_skylander(); + static std::shared_ptr make_instance(u32 controller_index, const std::array& location); + static u16 get_num_emu_devices(); + void control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) override; void interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) override; diff --git a/rpcs3/Emu/Io/Turntable.cpp b/rpcs3/Emu/Io/Turntable.cpp index 0ef979725e..eca1926825 100644 --- a/rpcs3/Emu/Io/Turntable.cpp +++ b/rpcs3/Emu/Io/Turntable.cpp @@ -5,6 +5,7 @@ #include "Emu/Cell/lv2/sys_usbd.h" #include "Emu/Io/turntable_config.h" #include "Input/pad_thread.h" +#include "Emu/system_config.h" LOG_CHANNEL(turntable_log, "TURN"); @@ -53,6 +54,16 @@ usb_device_turntable::~usb_device_turntable() { } +std::shared_ptr usb_device_turntable::make_instance(u32 controller_index, const std::array& location) +{ + return std::make_shared(controller_index, location); +} + +u16 usb_device_turntable::get_num_emu_devices() +{ + return static_cast(g_cfg.io.turntable.get()); +} + void usb_device_turntable::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) { transfer->fake = true; diff --git a/rpcs3/Emu/Io/Turntable.h b/rpcs3/Emu/Io/Turntable.h index 7ae0f51ead..f8186e9098 100644 --- a/rpcs3/Emu/Io/Turntable.h +++ b/rpcs3/Emu/Io/Turntable.h @@ -8,6 +8,9 @@ public: usb_device_turntable(u32 controller_index, const std::array& location); ~usb_device_turntable(); + static std::shared_ptr make_instance(u32 controller_index, const std::array& location); + static u16 get_num_emu_devices(); + void control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) override; void interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) override; diff --git a/rpcs3/Emu/Io/pad_types.h b/rpcs3/Emu/Io/pad_types.h index f48fbf7850..26a21f1f53 100644 --- a/rpcs3/Emu/Io/pad_types.h +++ b/rpcs3/Emu/Io/pad_types.h @@ -504,6 +504,8 @@ struct Pad u16 m_vendor_id{0}; u16 m_product_id{0}; + u64 m_disconnection_timer{0}; + s32 m_pressure_intensity_button_index{-1}; // Special button index. -1 if not set. bool m_pressure_intensity_button_pressed{}; // Last sensitivity button press state, used for toggle. bool m_pressure_intensity_toggled{}; // Whether the sensitivity is toggled on or off. diff --git a/rpcs3/Emu/Io/usio.cpp b/rpcs3/Emu/Io/usio.cpp index 077f6c7d62..e01deccf8a 100644 --- a/rpcs3/Emu/Io/usio.cpp +++ b/rpcs3/Emu/Io/usio.cpp @@ -127,6 +127,16 @@ usb_device_usio::~usb_device_usio() save_backup(); } +std::shared_ptr usb_device_usio::make_instance(u32, const std::array& location) +{ + return std::make_shared(location); +} + +u16 usb_device_usio::get_num_emu_devices() +{ + return 1; +} + void usb_device_usio::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) { transfer->fake = true; diff --git a/rpcs3/Emu/Io/usio.h b/rpcs3/Emu/Io/usio.h index f4cc93fc20..7a83e7a8ca 100644 --- a/rpcs3/Emu/Io/usio.h +++ b/rpcs3/Emu/Io/usio.h @@ -9,6 +9,9 @@ public: usb_device_usio(const std::array& location); ~usb_device_usio(); + static std::shared_ptr make_instance(u32 controller_index, const std::array& location); + static u16 get_num_emu_devices(); + void control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) override; void interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) override; diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index cb31823026..1423798366 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -62,6 +62,10 @@ #include "Emu/RSX/GSRender.h" +#ifdef LLVM_AVAILABLE +#include "llvm/Config/llvm-config.h" +#endif + LOG_CHANNEL(sys_log, "SYS"); // Preallocate 32 MiB @@ -345,6 +349,19 @@ extern void dump_executable(std::span data, const ppu_module* void Emulator::Init() { + // Log LLVM version + if (static bool logged_llvm = false; !logged_llvm) + { +#ifndef LLVM_AVAILABLE + sys_log.always()("LLVM version: Compiled without LLVM"); +#elif defined (LLVM_VERSION_STRING) + sys_log.always()("LLVM version: %s", LLVM_VERSION_STRING); +#else + sys_log.always()("LLVM version: Unknown"); +#endif + logged_llvm = true; + } + jit_runtime::initialize(); const std::string emu_dir = rpcs3::utils::get_emu_dir(); @@ -1100,47 +1117,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } else { - fs::file save{m_path, fs::isfile + fs::read}; - - if (m_path.ends_with(".SAVESTAT") && save && save.size() >= 8 && save.read() == "RPCS3SAV"_u64) - { - m_ar = std::make_shared(); - m_ar->set_reading_state(); - - m_ar->m_file_handler = make_uncompressed_serialization_file_handler(std::move(save)); - } - else if (save && m_path.ends_with(".zst")) - { - m_ar = std::make_shared(); - m_ar->set_reading_state(); - - m_ar->m_file_handler = make_compressed_zstd_serialization_file_handler(std::move(save)); - - if (m_ar->try_read().second != "RPCS3SAV"_u64) - { - m_ar.reset(); - } - else - { - m_ar->pos = 0; - } - } - else if (save && m_path.ends_with(".gz")) - { - m_ar = std::make_shared(); - m_ar->set_reading_state(); - - m_ar->m_file_handler = make_compressed_serialization_file_handler(std::move(save)); - - if (m_ar->try_read().second != "RPCS3SAV"_u64) - { - m_ar.reset(); - } - else - { - m_ar->pos = 0; - } - } + m_ar = make_savestate_reader(m_path); m_boot_source_type = CELL_GAME_GAMETYPE_SYS; } @@ -1172,18 +1149,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, if (m_ar) { - struct file_header - { - ENABLE_BITWISE_SERIALIZATION; - - nse_t magic; - bool LE_format; - bool state_inspection_support; - nse_t offset; - b8 flag_versions_is_following_data; - }; - - const auto header = m_ar->try_read().second; + const auto header = m_ar->try_read().second; if (header.magic != "RPCS3SAV"_u64) { @@ -1312,7 +1278,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, if (m_ar->m_max_data != m_ar->pos) { - fmt::throw_exception("TAR desrialization failed: read bytes: 0x%x, expected: 0x%x, path='%s', ar: %s", m_ar->pos - (m_ar->m_max_data - size), size, path, *m_ar); + fmt::throw_exception("TAR deserialization failed: read bytes: 0x%x, expected: 0x%x, path='%s', ar: %s", m_ar->pos - (m_ar->m_max_data - size), size, path, *m_ar); } m_ar->m_max_data = umax; diff --git a/rpcs3/Emu/savestate_utils.cpp b/rpcs3/Emu/savestate_utils.cpp index bb3e440564..c1b5c953fb 100644 --- a/rpcs3/Emu/savestate_utils.cpp +++ b/rpcs3/Emu/savestate_utils.cpp @@ -162,6 +162,60 @@ std::vector get_savestate_versioning_data(fs::file&& file, std::s return ver_data; } +std::shared_ptr make_savestate_reader(const std::string& path) +{ + std::shared_ptr ar; + + fs::file save{path, fs::isfile + fs::read}; + + if (!save) + { + return ar; + } + + if (path.ends_with(".SAVESTAT") && save.size() >= 8 && save.read() == "RPCS3SAV"_u64) + { + ar = std::make_shared(); + ar->set_reading_state(); + + ar->m_file_handler = make_uncompressed_serialization_file_handler(std::move(save)); + } + else if (path.ends_with(".zst")) + { + ar = std::make_shared(); + ar->set_reading_state(); + + ar->m_file_handler = make_compressed_zstd_serialization_file_handler(std::move(save)); + + if (ar->try_read().second != "RPCS3SAV"_u64) + { + ar.reset(); + } + else + { + ar->pos = 0; + } + } + else if (path.ends_with(".gz")) + { + ar = std::make_shared(); + ar->set_reading_state(); + + ar->m_file_handler = make_compressed_serialization_file_handler(std::move(save)); + + if (ar->try_read().second != "RPCS3SAV"_u64) + { + ar.reset(); + } + else + { + ar->pos = 0; + } + } + + return ar; +} + bool is_savestate_version_compatible(const std::vector& data, bool is_boot_check) { if (data.empty()) @@ -256,6 +310,15 @@ bool is_savestate_compatible(fs::file&& file, std::string_view filepath) return is_savestate_version_compatible(get_savestate_versioning_data(std::move(file), filepath), false); } +bool is_savestate_compatible(const std::string& filepath) +{ + if (fs::file file{filepath, fs::isfile + fs::read}) + { + return is_savestate_compatible(std::move(file), filepath); + } + return false; +} + std::vector read_used_savestate_versions() { std::vector used_serial; diff --git a/rpcs3/Emu/savestate_utils.hpp b/rpcs3/Emu/savestate_utils.hpp index 66d445369c..18028ae9ec 100644 --- a/rpcs3/Emu/savestate_utils.hpp +++ b/rpcs3/Emu/savestate_utils.hpp @@ -10,6 +10,16 @@ struct version_entry ENABLE_BITWISE_SERIALIZATION; }; +struct savestate_header +{ + ENABLE_BITWISE_SERIALIZATION; + + nse_t magic; + bool LE_format; + bool state_inspection_support; + nse_t offset; + b8 flag_versions_is_following_data; +}; struct hle_locks_t { @@ -27,9 +37,11 @@ struct hle_locks_t bool try_finalize(std::function test); }; +std::shared_ptr make_savestate_reader(const std::string& path); bool load_and_check_reserved(utils::serial& ar, usz size); bool is_savestate_version_compatible(const std::vector& data, bool is_boot_check); std::vector get_savestate_versioning_data(fs::file&& file, std::string_view filepath); bool is_savestate_compatible(fs::file&& file, std::string_view filepath); +bool is_savestate_compatible(const std::string& filepath); std::vector read_used_savestate_versions(); -std::string get_savestate_file(std::string_view title_id, std::string_view boot_path, s64 abs_id, s64 rel_id); \ No newline at end of file +std::string get_savestate_file(std::string_view title_id, std::string_view boot_path, s64 abs_id, s64 rel_id); diff --git a/rpcs3/Input/pad_thread.cpp b/rpcs3/Input/pad_thread.cpp index 70f0e25618..f6e4026873 100644 --- a/rpcs3/Input/pad_thread.cpp +++ b/rpcs3/Input/pad_thread.cpp @@ -56,6 +56,9 @@ struct pad_setting u32 port_status = 0; u32 device_capability = 0; u32 device_type = 0; + u32 class_type = 0; + u16 vendor_id = 0; + u16 product_id = 0; bool is_ldd_pad = false; }; @@ -86,6 +89,9 @@ void pad_thread::Init() m_pads[i]->m_port_status, m_pads[i]->m_device_capability, m_pads[i]->m_device_type, + m_pads[i]->m_class_type, + m_pads[i]->m_vendor_id, + m_pads[i]->m_product_id, m_pads[i]->ldd }; } @@ -96,6 +102,9 @@ void pad_thread::Init() CELL_PAD_STATUS_DISCONNECTED, CELL_PAD_CAPABILITY_PS3_CONFORMITY | CELL_PAD_CAPABILITY_PRESS_MODE | CELL_PAD_CAPABILITY_ACTUATOR, CELL_PAD_DEV_TYPE_STANDARD, + CELL_PAD_PCLASS_TYPE_STANDARD, + 0, + 0, false }; } @@ -197,6 +206,12 @@ void pad_thread::Init() { input_log.notice("Pad %d: config=\n%s", i, cfg->to_string()); } + + // If the user changes the emulated controller, then simulate unplugging and plugging in a new controller + if (m_pads_connected[i] && (pad_settings[i].class_type != pad->m_class_type || pad_settings[i].vendor_id != pad->m_vendor_id || pad_settings[i].product_id != pad->m_product_id)) + { + pad->m_disconnection_timer = get_system_time() + 30'000ull; + } } 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))) @@ -235,6 +250,25 @@ void pad_thread::update_pad_states() for (usz i = 0; i < m_pads.size(); i++) { const auto& pad = m_pads[i]; + + // Simulate unplugging and plugging in a new controller + if (pad && pad->m_disconnection_timer > 0) + { + const bool is_connected = pad->m_port_status & CELL_PAD_STATUS_CONNECTED; + const u64 now = get_system_time(); + + if (is_connected && now < pad->m_disconnection_timer) + { + pad->m_port_status &= ~CELL_PAD_STATUS_CONNECTED; + pad->m_port_status |= CELL_PAD_STATUS_ASSIGN_CHANGES; + } + else if (!is_connected && now >= pad->m_disconnection_timer) + { + pad->m_port_status |= CELL_PAD_STATUS_CONNECTED + CELL_PAD_STATUS_ASSIGN_CHANGES; + pad->m_disconnection_timer = 0; + } + } + const bool connected = pad && !pad->is_fake_pad && !!(pad->m_port_status & CELL_PAD_STATUS_CONNECTED); if (m_pads_connected[i] == connected) @@ -603,6 +637,8 @@ void pad_thread::UnregisterLddPad(u32 handle) ensure(handle < m_pads.size()); m_pads[handle]->ldd = false; + m_pads[handle]->m_port_status &= ~CELL_PAD_STATUS_CONNECTED; + m_pads[handle]->m_port_status |= CELL_PAD_STATUS_ASSIGN_CHANGES; num_ldd_pad--; } diff --git a/rpcs3/main.cpp b/rpcs3/main.cpp index 894115ecc8..a6078bf9e5 100644 --- a/rpcs3/main.cpp +++ b/rpcs3/main.cpp @@ -665,6 +665,7 @@ int main(int argc, char** argv) logs::stored_message qt{(strcmp(QT_VERSION_STR, qVersion()) != 0) ? sys_log.error : sys_log.notice}; qt.text = fmt::format("Qt version: Compiled against Qt %s | Run-time uses Qt %s", QT_VERSION_STR, qVersion()); + // Write current time logs::stored_message time{sys_log.always()}; time.text = fmt::format("Current Time: %s", std::chrono::system_clock::now()); diff --git a/rpcs3/rpcs3qt/gui_application.cpp b/rpcs3/rpcs3qt/gui_application.cpp index 9f5dc53cb7..e25c0627f4 100644 --- a/rpcs3/rpcs3qt/gui_application.cpp +++ b/rpcs3/rpcs3qt/gui_application.cpp @@ -21,6 +21,7 @@ #include "Emu/Audio/audio_utils.h" #include "Emu/Io/Null/null_camera_handler.h" #include "Emu/Io/Null/null_music_handler.h" +#include "Emu/Cell/lv2/sys_usbd.h" #include "Emu/vfs_config.h" #include "util/init_mutex.hpp" #include "util/console.h" @@ -54,7 +55,8 @@ #endif #ifdef _WIN32 -#include "Windows.h" +#include +#include #endif LOG_CHANNEL(gui_log, "GUI"); @@ -73,6 +75,12 @@ gui_application::~gui_application() #ifdef WITH_DISCORD_RPC discord::shutdown(); #endif +#ifdef _WIN32 + if (m_device_notification_handle && !UnregisterDeviceNotification(m_device_notification_handle)) + { + gui_log.error("UnregisterDeviceNotification() failed: %s", fmt::win_error{GetLastError(), nullptr}); + } +#endif } bool gui_application::Init() @@ -196,6 +204,19 @@ bool gui_application::Init() // Install native event filter #ifdef _WIN32 // Currently only needed for raw mouse input on windows installNativeEventFilter(&m_native_event_filter); + + // Enable usb device hotplug events + // Currently only needed for hotplug on windows, as libusb handles other platforms + DEV_BROADCAST_DEVICEINTERFACE notification_filter {}; + notification_filter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE); + notification_filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; + notification_filter.dbcc_classguid = GUID_DEVINTERFACE_USB_DEVICE; + + m_device_notification_handle = RegisterDeviceNotification(reinterpret_cast(m_main_window->winId()), ¬ification_filter, DEVICE_NOTIFY_WINDOW_HANDLE); + if (!m_device_notification_handle ) + { + gui_log.error("RegisterDeviceNotification() failed: %s", fmt::win_error{GetLastError(), nullptr}); + } #endif return true; @@ -1180,15 +1201,24 @@ void gui_application::OnAppStateChanged(Qt::ApplicationState state) bool gui_application::native_event_filter::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, [[maybe_unused]] void* message, [[maybe_unused]] qintptr* result) { #ifdef _WIN32 - if (!Emu.IsRunning() && !g_raw_mouse_handler) + if (!Emu.IsRunning() && !Emu.IsStarting() && !g_raw_mouse_handler) { return false; } if (eventType == "windows_generic_MSG") { - if (MSG* msg = static_cast(message); msg && (msg->message == WM_INPUT || msg->message == WM_KEYDOWN || msg->message == WM_KEYUP)) + if (MSG* msg = static_cast(message); msg && (msg->message == WM_INPUT || msg->message == WM_KEYDOWN || msg->message == WM_KEYUP || msg->message == WM_DEVICECHANGE)) { + if (msg->message == WM_DEVICECHANGE && (msg->wParam == DBT_DEVICEARRIVAL || msg->wParam == DBT_DEVICEREMOVECOMPLETE)) + { + if (Emu.IsRunning() || Emu.IsStarting()) + { + handle_hotplug_event(msg->wParam == DBT_DEVICEARRIVAL); + } + return false; + } + if (auto* handler = g_fxo->try_get(); handler && handler->type == mouse_handler::raw) { static_cast(handler)->handle_native_event(*msg); diff --git a/rpcs3/rpcs3qt/gui_application.h b/rpcs3/rpcs3qt/gui_application.h index aa43425b31..f766c3b9c2 100644 --- a/rpcs3/rpcs3qt/gui_application.h +++ b/rpcs3/rpcs3qt/gui_application.h @@ -19,6 +19,10 @@ #include #include +#ifdef _WIN32 +#include "Windows.h" +#endif + class gs_frame; class main_window; class gui_settings; @@ -117,7 +121,9 @@ private: u64 m_pause_delayed_tag = 0; typename Emulator::stop_counter_t m_emu_focus_out_emulation_id{}; bool m_is_pause_on_focus_loss_active = false; - +#ifdef _WIN32 + HDEVNOTIFY m_device_notification_handle {}; +#endif private Q_SLOTS: void OnChangeStyleSheetRequest(); void OnShortcutChange(); diff --git a/rpcs3/rpcs3qt/gui_settings.h b/rpcs3/rpcs3qt/gui_settings.h index 82c6770d00..81cfe6bcc1 100644 --- a/rpcs3/rpcs3qt/gui_settings.h +++ b/rpcs3/rpcs3qt/gui_settings.h @@ -120,6 +120,9 @@ namespace gui const gui_save rg_freeze = gui_save(main_window, "recentGamesFrozen", false); const gui_save rg_entries = gui_save(main_window, "recentGamesNames", QVariant::fromValue(q_pair_list())); + const gui_save rs_freeze = gui_save(main_window, "recentSavestatesFrozen", false); + const gui_save rs_entries = gui_save(main_window, "recentSavestatesNames", QVariant::fromValue(q_pair_list())); + const gui_save ib_skip_version = gui_save(main_window, "infoBoxSkipVersion", ""); const gui_save ib_pkg_success = gui_save(main_window, "infoBoxEnabledInstallPKG", true); const gui_save ib_pup_success = gui_save(main_window, "infoBoxEnabledInstallPUP", true); diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index e23d9b32bc..e4cccd3307 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -63,6 +63,7 @@ #include "Emu/System.h" #include "Emu/system_utils.hpp" #include "Emu/system_config.h" +#include "Emu/savestate_utils.hpp" #include "Crypto/unpkg.h" #include "Crypto/unself.h" @@ -234,9 +235,9 @@ bool main_window::Init([[maybe_unused]] bool with_cli_boot) show(); // needs to be done before creating the thumbnail toolbar // enable play options if a recent game exists - const bool enable_play_last = !m_recent_game_acts.isEmpty() && m_recent_game_acts.first(); + const bool enable_play_last = !m_recent_game.actions.isEmpty() && m_recent_game.actions.first(); - const QString start_tooltip = enable_play_last ? tr("Play %0").arg(m_recent_game_acts.first()->text()) : tr("Play"); + const QString start_tooltip = enable_play_last ? tr("Play %0").arg(m_recent_game.actions.first()->text()) : tr("Play"); if (enable_play_last) { @@ -510,9 +511,9 @@ void main_window::OnPlayOrPause() show_boot_error(error); } } - else if (!m_recent_game_acts.isEmpty()) + else if (!m_recent_game.actions.isEmpty()) { - BootRecentAction(m_recent_game_acts.first()); + BootRecentAction(m_recent_game.actions.first(), false); } return; @@ -607,7 +608,7 @@ void main_window::Boot(const std::string& path, const std::string& title_id, boo { gui_log.success("Boot successful."); - AddRecentAction(gui::Recent_Game(qstr(Emu.GetBoot()), qstr(Emu.GetTitleAndTitleID()))); + AddRecentAction(gui::Recent_Game(qstr(Emu.GetBoot()), qstr(Emu.GetTitleAndTitleID())), is_savestate_compatible(path)); if (refresh_list) { @@ -2190,55 +2191,59 @@ void main_window::OnEnableDiscInsert(bool enabled) const ui->insertDiscAct->setEnabled(enabled); } -void main_window::BootRecentAction(const QAction* act) +void main_window::BootRecentAction(const QAction* act, bool is_savestate) { if (Emu.IsRunning()) { return; } + recent_game_wrapper& rgw = is_savestate ? m_recent_save : m_recent_game; + QMenu* menu = is_savestate ? ui->bootRecentSavestatesMenu : ui->bootRecentMenu; + const QString pth = act->data().toString(); - const std::string path = sstr(pth); + const std::string path = pth.toStdString(); QString name; bool contains_path = false; int idx = -1; - for (int i = 0; i < m_rg_entries.count(); i++) + for (int i = 0; i < rgw.entries.count(); i++) { - if (::at32(m_rg_entries, i).first == pth) + const auto& entry = rgw.entries[i]; + if (entry.first == pth) { idx = i; contains_path = true; - name = ::at32(m_rg_entries, idx).second; + name = entry.second; break; } } // path is invalid: remove action from list return - if ((contains_path && name.isEmpty()) || (!QFileInfo(pth).isDir() && !QFileInfo(pth).isFile())) + if ((contains_path && name.isEmpty()) || !fs::exists(path)) { if (contains_path) { // clear menu of actions - for (QAction* action : m_recent_game_acts) + for (QAction* action : rgw.actions) { - ui->bootRecentMenu->removeAction(action); + menu->removeAction(action); } // remove action from list - m_rg_entries.removeAt(idx); - m_recent_game_acts.removeAt(idx); + rgw.entries.removeAt(idx); + rgw.actions.removeAt(idx); - m_gui_settings->SetValue(gui::rg_entries, gui_settings::List2Var(m_rg_entries)); + m_gui_settings->SetValue(is_savestate ? gui::rs_entries : gui::rg_entries, gui_settings::List2Var(rgw.entries)); gui_log.error("Recent Game not valid, removed from Boot Recent list: %s", path); // refill menu with actions - for (int i = 0; i < m_recent_game_acts.count(); i++) + for (int i = 0; i < rgw.actions.count(); i++) { - m_recent_game_acts[i]->setShortcut(tr("Ctrl+%1").arg(i + 1)); - m_recent_game_acts[i]->setToolTip(::at32(m_rg_entries, i).second); - ui->bootRecentMenu->addAction(m_recent_game_acts[i]); + rgw.actions[i]->setShortcut(tr("Ctrl+%1").arg(i + 1)); + rgw.actions[i]->setToolTip(::at32(rgw.entries, i).second); + menu->addAction(rgw.actions[i]); } gui_log.warning("Boot Recent list refreshed"); @@ -2253,19 +2258,21 @@ void main_window::BootRecentAction(const QAction* act) Boot(path, "", true); } -QAction* main_window::CreateRecentAction(const q_string_pair& entry, const uint& sc_idx) +QAction* main_window::CreateRecentAction(const q_string_pair& entry, u32 sc_idx, bool is_savestate) { + recent_game_wrapper& rgw = is_savestate ? m_recent_save : m_recent_game; + // if path is not valid remove from list if (entry.second.isEmpty() || (!QFileInfo(entry.first).isDir() && !QFileInfo(entry.first).isFile())) { - if (m_rg_entries.contains(entry)) + if (rgw.entries.contains(entry)) { gui_log.warning("Recent Game not valid, removing from Boot Recent list: %s", entry.first); - const int idx = m_rg_entries.indexOf(entry); - m_rg_entries.removeAt(idx); + const int idx = rgw.entries.indexOf(entry); + rgw.entries.removeAt(idx); - m_gui_settings->SetValue(gui::rg_entries, gui_settings::List2Var(m_rg_entries)); + m_gui_settings->SetValue(is_savestate ? gui::rs_entries : gui::rg_entries, gui_settings::List2Var(rgw.entries)); } return nullptr; } @@ -2290,69 +2297,74 @@ QAction* main_window::CreateRecentAction(const q_string_pair& entry, const uint& } // connect boot - connect(act, &QAction::triggered, this, [act, this]() {BootRecentAction(act); }); + connect(act, &QAction::triggered, this, [this, act, is_savestate](){ BootRecentAction(act, is_savestate); }); return act; } -void main_window::AddRecentAction(const q_string_pair& entry) +void main_window::AddRecentAction(const q_string_pair& entry, bool is_savestate) { + QAction* freezeAction = is_savestate ? ui->freezeRecentSavestatesAct : ui->freezeRecentAct; + // don't change list on freeze - if (ui->freezeRecentAct->isChecked()) + if (freezeAction->isChecked()) { return; } // create new action, return if not valid - QAction* act = CreateRecentAction(entry, 1); + QAction* act = CreateRecentAction(entry, 1, is_savestate); if (!act) { return; } + recent_game_wrapper& rgw = is_savestate ? m_recent_save : m_recent_game; + QMenu* menu = is_savestate ? ui->bootRecentSavestatesMenu : ui->bootRecentMenu; + // clear menu of actions - for (QAction* action : m_recent_game_acts) + for (QAction* action : rgw.actions) { - ui->bootRecentMenu->removeAction(action); + menu->removeAction(action); } // If path already exists, remove it in order to get it to beginning. Also remove duplicates. - for (int i = m_rg_entries.count() - 1; i >= 0; --i) + for (int i = rgw.entries.count() - 1; i >= 0; --i) { - if (m_rg_entries[i].first == entry.first) + if (rgw.entries[i].first == entry.first) { - m_rg_entries.removeAt(i); - m_recent_game_acts.removeAt(i); + rgw.entries.removeAt(i); + rgw.actions.removeAt(i); } } // remove oldest action at the end if needed - if (m_rg_entries.count() == 9) + if (rgw.entries.count() == 9) { - m_rg_entries.removeLast(); - m_recent_game_acts.removeLast(); + rgw.entries.removeLast(); + rgw.actions.removeLast(); } - else if (m_rg_entries.count() > 9) + else if (rgw.entries.count() > 9) { gui_log.error("Recent games entrylist too big"); } - if (m_rg_entries.count() < 9) + if (rgw.entries.count() < 9) { // add new action at the beginning - m_rg_entries.prepend(entry); - m_recent_game_acts.prepend(act); + rgw.entries.prepend(entry); + rgw.actions.prepend(act); } // refill menu with actions - for (int i = 0; i < m_recent_game_acts.count(); i++) + for (int i = 0; i < rgw.actions.count(); i++) { - m_recent_game_acts[i]->setShortcut(tr("Ctrl+%1").arg(i + 1)); - m_recent_game_acts[i]->setToolTip(::at32(m_rg_entries, i).second); - ui->bootRecentMenu->addAction(m_recent_game_acts[i]); + rgw.actions[i]->setShortcut(tr("Ctrl+%1").arg(i + 1)); + rgw.actions[i]->setToolTip(::at32(rgw.entries, i).second); + menu->addAction(rgw.actions[i]); } - m_gui_settings->SetValue(gui::rg_entries, gui_settings::List2Var(m_rg_entries)); + m_gui_settings->SetValue(is_savestate ? gui::rs_entries : gui::rg_entries, gui_settings::List2Var(rgw.entries)); } void main_window::UpdateLanguageActions(const QStringList& language_codes, const QString& language_code) @@ -2638,26 +2650,59 @@ void main_window::CreateConnects() } }); + connect(ui->bootRecentSavestatesMenu, &QMenu::aboutToShow, this, [this]() + { + // Enable/Disable Recent Savestates List + const bool stopped = Emu.IsStopped(); + for (QAction* act : ui->bootRecentSavestatesMenu->actions()) + { + if (act != ui->freezeRecentSavestatesAct && act != ui->clearRecentSavestatesAct) + { + act->setEnabled(stopped); + } + } + }); + connect(ui->clearRecentAct, &QAction::triggered, this, [this]() { if (ui->freezeRecentAct->isChecked()) { return; } - m_rg_entries.clear(); - for (QAction* act : m_recent_game_acts) + m_recent_game.entries.clear(); + for (QAction* act : m_recent_game.actions) { ui->bootRecentMenu->removeAction(act); } - m_recent_game_acts.clear(); + m_recent_game.actions.clear(); m_gui_settings->SetValue(gui::rg_entries, gui_settings::List2Var(q_pair_list())); }); + connect(ui->clearRecentSavestatesAct, &QAction::triggered, this, [this]() + { + if (ui->freezeRecentSavestatesAct->isChecked()) + { + return; + } + m_recent_save.entries.clear(); + for (QAction* act : m_recent_save.actions) + { + ui->bootRecentSavestatesMenu->removeAction(act); + } + m_recent_save.actions.clear(); + m_gui_settings->SetValue(gui::rs_entries, gui_settings::List2Var(q_pair_list())); + }); + connect(ui->freezeRecentAct, &QAction::triggered, this, [this](bool checked) { m_gui_settings->SetValue(gui::rg_freeze, checked); }); + connect(ui->freezeRecentSavestatesAct, &QAction::triggered, this, [this](bool checked) + { + m_gui_settings->SetValue(gui::rs_freeze, checked); + }); + connect(ui->bootInstallPkgAct, &QAction::triggered, this, [this] {InstallPackages(); }); connect(ui->bootInstallPupAct, &QAction::triggered, this, [this] {InstallPup(); }); @@ -3530,9 +3575,9 @@ void main_window::CreateDockWindows() ui->toolbar_start->setIcon(m_icon_restart); ui->toolbar_start->setText(tr("Restart")); } - else if (!m_recent_game_acts.isEmpty()) // Get last played game + else if (!m_recent_game.actions.isEmpty()) // Get last played game { - tooltip = tr("Play %0").arg(m_recent_game_acts.first()->text()); + tooltip = tr("Play %0").arg(m_recent_game.actions.first()->text()); } else { @@ -3584,35 +3629,45 @@ void main_window::ConfigureGuiFromSettings() m_mw->restoreState(m_gui_settings->GetValue(gui::mw_mwState).toByteArray()); ui->freezeRecentAct->setChecked(m_gui_settings->GetValue(gui::rg_freeze).toBool()); - m_rg_entries = gui_settings::Var2List(m_gui_settings->GetValue(gui::rg_entries)); + ui->freezeRecentSavestatesAct->setChecked(m_gui_settings->GetValue(gui::rs_freeze).toBool()); + m_recent_game.entries = gui_settings::Var2List(m_gui_settings->GetValue(gui::rg_entries)); + m_recent_save.entries = gui_settings::Var2List(m_gui_settings->GetValue(gui::rs_entries)); - // clear recent games menu of actions - for (QAction* act : m_recent_game_acts) + const auto update_recent_games_menu = [this](bool is_savestate) { - ui->bootRecentMenu->removeAction(act); - } - m_recent_game_acts.clear(); + recent_game_wrapper& rgw = is_savestate ? m_recent_save : m_recent_game; + QMenu* menu = is_savestate ? ui->bootRecentSavestatesMenu : ui->bootRecentMenu; - // Fill the recent games menu - for (int i = 0; i < m_rg_entries.count(); i++) - { - // adjust old unformatted entries (avoid duplication) - m_rg_entries[i] = gui::Recent_Game(m_rg_entries[i].first, m_rg_entries[i].second); - - // create new action - QAction* act = CreateRecentAction(m_rg_entries[i], i + 1); - - // add action to menu - if (act) + // clear recent games menu of actions + for (QAction* act : rgw.actions) { - m_recent_game_acts.append(act); - ui->bootRecentMenu->addAction(act); + menu->removeAction(act); } - else + rgw.actions.clear(); + + // Fill the recent games menu + for (int i = 0; i < rgw.entries.count(); i++) { - i--; // list count is now an entry shorter so we have to repeat the same index in order to load all other entries + // adjust old unformatted entries (avoid duplication) + rgw.entries[i] = gui::Recent_Game(rgw.entries[i].first, rgw.entries[i].second); + + // create new action + QAction* act = CreateRecentAction(rgw.entries[i], i + 1, is_savestate); + + // add action to menu + if (act) + { + rgw.actions.append(act); + menu->addAction(act); + } + else + { + i--; // list count is now an entry shorter so we have to repeat the same index in order to load all other entries + } } - } + }; + update_recent_games_menu(false); + update_recent_games_menu(true); ui->showLogAct->setChecked(m_gui_settings->GetValue(gui::mw_logger).toBool()); ui->showGameListAct->setChecked(m_gui_settings->GetValue(gui::mw_gamelist).toBool()); @@ -4147,14 +4202,19 @@ void main_window::dropEvent(QDropEvent* event) Emu.GracefulShutdown(false); - if (const auto error = Emu.BootGame(sstr(drop_paths.first()), "", true); error != game_boot_result::no_errors) + const std::string path = drop_paths.first().toStdString(); + + if (const auto error = Emu.BootGame(path, "", true); error != game_boot_result::no_errors) { - gui_log.error("Boot failed: reason: %s, path: %s", error, drop_paths.first()); + gui_log.error("Boot failed: reason: %s, path: %s", error, path); show_boot_error(error); } else { - gui_log.success("Elf Boot from drag and drop done: %s", drop_paths.first()); + gui_log.success("Elf Boot from drag and drop done: %s", path); + + AddRecentAction(gui::Recent_Game(QString::fromStdString(path), QString::fromStdString(Emu.GetTitleAndTitleID())), is_savestate_compatible(path)); + m_game_list_frame->Refresh(true); } break; diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h index a2a8f25678..daaf2848aa 100644 --- a/rpcs3/rpcs3qt/main_window.h +++ b/rpcs3/rpcs3qt/main_window.h @@ -175,17 +175,22 @@ private: drop_type IsValidFile(const QMimeData& md, QStringList* drop_paths = nullptr); void AddGamesFromDirs(QStringList&& paths); - QAction* CreateRecentAction(const q_string_pair& entry, const uint& sc_idx); - void BootRecentAction(const QAction* act); - void AddRecentAction(const q_string_pair& entry); + QAction* CreateRecentAction(const q_string_pair& entry, u32 sc_idx, bool is_savestate); + void BootRecentAction(const QAction* act, bool is_savestate); + void AddRecentAction(const q_string_pair& entry, bool is_savestate); void UpdateLanguageActions(const QStringList& language_codes, const QString& language); void UpdateFilterActions(); static QString GetCurrentTitle(); - q_pair_list m_rg_entries; - QList m_recent_game_acts; + struct recent_game_wrapper + { + q_pair_list entries; + QList actions; + }; + recent_game_wrapper m_recent_game {}; + recent_game_wrapper m_recent_save {}; std::shared_ptr m_selected_game; diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index adb66d8bcf..3ddf7b8b1a 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -202,10 +202,19 @@ + + + Boot Recent Savestate + + + + + + @@ -1387,6 +1396,16 @@ Operating System + + + List Clear + + + + + List Freeze + + diff --git a/rpcs3/util/serialization_ext.hpp b/rpcs3/util/serialization_ext.hpp index 75b6212890..51071e7188 100644 --- a/rpcs3/util/serialization_ext.hpp +++ b/rpcs3/util/serialization_ext.hpp @@ -187,7 +187,6 @@ private: void initialize(utils::serial& ar); void stream_data_prepare_thread_op(); void file_writer_thread_op(); - void blocked_compressed_write(const std::vector& data); }; template requires (std::is_same_v, fs::file>)