diff --git a/CMakeLists.txt b/CMakeLists.txt index b75da04d6..d1979a934 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -539,6 +539,8 @@ set(RANDOM_LIB src/core/libraries/random/random.cpp set(USBD_LIB src/core/libraries/usbd/usbd.cpp src/core/libraries/usbd/usbd.h src/core/libraries/usbd/usb_backend.h + src/core/libraries/usbd/emulated/infinity.cpp + src/core/libraries/usbd/emulated/infinity.h src/core/libraries/usbd/emulated/skylander.cpp src/core/libraries/usbd/emulated/skylander.h ) diff --git a/src/core/libraries/usbd/emulated/infinity.cpp b/src/core/libraries/usbd/emulated/infinity.cpp new file mode 100644 index 000000000..af8e13dcc --- /dev/null +++ b/src/core/libraries/usbd/emulated/infinity.cpp @@ -0,0 +1,392 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "infinity.h" + +#include + +namespace Libraries::Usbd { + +InfinityBase::InfinityBase() {} + +void InfinityBase::LoadFigure(std::string file_name, u8 pad, u8 slot) { + Common::FS::IOFile file(file_name, Common::FS::FileAccessMode::ReadWrite); + std::array data; + ASSERT(file.Read(data) == data.size()); + LoadInfinityFigure(data, std::move(file), slot); +} + +void InfinityBase::RemoveFigure(u8 pad, u8 slot, bool full_remove) { + std::lock_guard lock(infinity_mutex); + InfinityFigure& figure = infinity_figures[slot]; + + if (!figure.present) { + return; + } + + slot = DeriveFigurePosition(slot); + if (slot == 0) { + return; + } + + figure.present = false; + + std::array figure_change_response = {0xab, 0x04, slot, 0x09, figure.order_added, 0x01}; + figure_change_response[6] = GenerateChecksum(figure_change_response, 6); + m_figure_added_removed_responses.push(figure_change_response); + + figure.Save(); + figure.infFile.Close(); +} + +void InfinityBase::LoadInfinityFigure(const std::array& buf, + Common::FS::IOFile file, u8 position) { + std::lock_guard lock(infinity_mutex); + u8 order_added; + + InfinityFigure& figure = infinity_figures[position]; + + figure.infFile = std::move(file); + memcpy(figure.data.data(), buf.data(), figure.data.size()); + figure.present = true; + if (figure.order_added == 255) { + figure.order_added = m_figure_order; + m_figure_order++; + } + order_added = figure.order_added; + + position = DeriveFigurePosition(position); + if (position == 0) { + return; + } + + std::array figure_change_response = {0xab, 0x04, position, 0x09, order_added, 0x00}; + figure_change_response[6] = GenerateChecksum(figure_change_response, 6); + m_figure_added_removed_responses.push(figure_change_response); +} + +void InfinityBase::GetBlankResponse(u8 sequence, std::array& reply_buf) { + reply_buf[0] = 0xaa; + reply_buf[1] = 0x01; + reply_buf[2] = sequence; + reply_buf[3] = GenerateChecksum(reply_buf, 3); +} + +void InfinityBase::DescrambleAndSeed(u8* buf, u8 sequence, std::array& reply_buf) { + u64 value = u64(buf[4]) << 56 | u64(buf[5]) << 48 | u64(buf[6]) << 40 | u64(buf[7]) << 32 | + u64(buf[8]) << 24 | u64(buf[9]) << 16 | u64(buf[10]) << 8 | u64(buf[11]); + u32 seed = Descramble(value); + GenerateSeed(seed); + GetBlankResponse(sequence, reply_buf); +} + +void InfinityBase::GetNextAndScramble(u8 sequence, std::array& reply_buf) { + const u32 next_random = GetNext(); + const u64 scrambled_next_random = Scramble(next_random, 0); + reply_buf = {0xAA, 0x09, sequence}; + reply_buf[3] = u8((scrambled_next_random >> 56) & 0xFF); + reply_buf[4] = u8((scrambled_next_random >> 48) & 0xFF); + reply_buf[5] = u8((scrambled_next_random >> 40) & 0xFF); + reply_buf[6] = u8((scrambled_next_random >> 32) & 0xFF); + reply_buf[7] = u8((scrambled_next_random >> 24) & 0xFF); + reply_buf[8] = u8((scrambled_next_random >> 16) & 0xFF); + reply_buf[9] = u8((scrambled_next_random >> 8) & 0xFF); + reply_buf[10] = u8(scrambled_next_random & 0xFF); + reply_buf[11] = GenerateChecksum(reply_buf, 11); +} + +void InfinityBase::GetPresentFigures(u8 sequence, std::array& reply_buf) { + int x = 3; + for (u8 i = 0; i < infinity_figures.size(); i++) { + u8 slot = i == 0 ? 0x10 : (i < 4) ? 0x20 : 0x30; + if (infinity_figures[i].present) { + reply_buf[x] = slot + infinity_figures[i].order_added; + reply_buf[x + 1] = 0x09; + x += 2; + } + } + reply_buf[0] = 0xaa; + reply_buf[1] = x - 2; + reply_buf[2] = sequence; + reply_buf[x] = GenerateChecksum(reply_buf, x); +} + +void InfinityBase::QueryBlock(u8 fig_num, u8 block, std::array& reply_buf, u8 sequence) { + std::lock_guard lock(infinity_mutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + reply_buf[0] = 0xaa; + reply_buf[1] = 0x12; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + const u8 file_block = (block == 0) ? 1 : (block * 4); + if (figure.present && file_block < 20) { + memcpy(&reply_buf[4], figure.data.data() + (16 * file_block), 16); + } + reply_buf[20] = GenerateChecksum(reply_buf, 20); +} + +void InfinityBase::WriteBlock(u8 fig_num, u8 block, const u8* to_write_buf, + std::array& reply_buf, u8 sequence) { + std::lock_guard lock(infinity_mutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + reply_buf[0] = 0xaa; + reply_buf[1] = 0x02; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + const u8 file_block = (block == 0) ? 1 : (block * 4); + if (figure.present && file_block < 20) { + memcpy(figure.data.data() + (file_block * 16), to_write_buf, 16); + figure.Save(); + } + reply_buf[4] = GenerateChecksum(reply_buf, 4); +} + +void InfinityBase::GetFigureIdentifier(u8 fig_num, u8 sequence, std::array& reply_buf) { + std::lock_guard lock(infinity_mutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + reply_buf[0] = 0xaa; + reply_buf[1] = 0x09; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + + if (figure.present) { + memcpy(&reply_buf[4], figure.data.data(), 7); + } + reply_buf[11] = GenerateChecksum(reply_buf, 11); +} + +std::optional> InfinityBase::PopAddedRemovedResponse() { + if (m_figure_added_removed_responses.empty()) + return std::nullopt; + + std::array response = m_figure_added_removed_responses.front(); + m_figure_added_removed_responses.pop(); + return response; +} + +u8 InfinityBase::GenerateChecksum(const std::array& data, int num_of_bytes) const { + int checksum = 0; + for (int i = 0; i < num_of_bytes; i++) { + checksum += data[i]; + } + return (checksum & 0xFF); +} + +u32 InfinityBase::Descramble(u64 num_to_descramble) { + u64 mask = 0x8E55AA1B3999E8AA; + u32 ret = 0; + + for (int i = 0; i < 64; i++) { + if (mask & 0x8000000000000000) { + ret = (ret << 1) | (num_to_descramble & 0x01); + } + + num_to_descramble >>= 1; + mask <<= 1; + } + + return ret; +} + +u64 InfinityBase::Scramble(u32 num_to_scramble, u32 garbage) { + u64 mask = 0x8E55AA1B3999E8AA; + u64 ret = 0; + + for (int i = 0; i < 64; i++) { + ret <<= 1; + + if ((mask & 1) != 0) { + ret |= (num_to_scramble & 1); + num_to_scramble >>= 1; + } else { + ret |= (garbage & 1); + garbage >>= 1; + } + + mask >>= 1; + } + + return ret; +} + +void InfinityBase::GenerateSeed(u32 seed) { + random_a = 0xF1EA5EED; + random_b = seed; + random_c = seed; + random_d = seed; + + for (int i = 0; i < 23; i++) { + GetNext(); + } +} + +u32 InfinityBase::GetNext() { + u32 a = random_a; + u32 b = random_b; + u32 c = random_c; + u32 ret = std::rotl(random_b, 27); + + const u32 temp = (a + ((ret ^ 0xFFFFFFFF) + 1)); + b ^= std::rotl(c, 17); + a = random_d; + c += a; + ret = b + temp; + a += temp; + + random_c = a; + random_a = b; + random_b = c; + random_d = ret; + + return ret; +} + +InfinityFigure& InfinityBase::GetFigureByOrder(u8 order_added) { + for (u8 i = 0; i < infinity_figures.size(); i++) { + if (infinity_figures[i].order_added == order_added) { + return infinity_figures[i]; + } + } + return infinity_figures[0]; +} + +u8 InfinityBase::DeriveFigurePosition(u8 position) { + switch (position) { + case 0: + case 1: + case 2: + return 1; + case 3: + case 4: + case 5: + return 2; + case 6: + case 7: + case 8: + return 3; + + default: + return 0; + } +} + +libusb_endpoint_descriptor* InfinityBackend::FillEndpointDescriptorPair() { + return m_endpoint_descriptors.data(); +} + +libusb_interface_descriptor* InfinityBackend::FillInterfaceDescriptor( + libusb_endpoint_descriptor* descs) { + m_interface_descriptors[0].endpoint = descs; + return m_interface_descriptors.data(); +} + +libusb_config_descriptor* InfinityBackend::FillConfigDescriptor(libusb_interface* inter) { + m_config_descriptors[0].interface = inter; + return m_config_descriptors.data(); +} + +libusb_device_descriptor* InfinityBackend::FillDeviceDescriptor() { + return m_device_descriptors.data(); +} + +libusb_transfer_status InfinityBackend::HandleAsyncTransfer(libusb_transfer* transfer) { + switch (transfer->endpoint) { + case 0x81: { + // Respond after FF command + std::optional> response = m_infinity_base->PopAddedRemovedResponse(); + if (response) { + memcpy(transfer->buffer, response.value().data(), 0x20); + } else if (!m_queries.empty()) { + memcpy(transfer->buffer, m_queries.front().data(), 0x20); + m_queries.pop(); + } + break; + } + case 0x01: { + const u8 command = transfer->buffer[2]; + const u8 sequence = transfer->buffer[3]; + LOG_INFO(Lib_Usbd, "Infinity Backend Transfer command: {:x}", command); + + std::array q_result{}; + + switch (command) { + case 0x80: { + q_result = {0xaa, 0x15, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x03, 0x02, 0x09, 0x09, 0x43, + 0x20, 0x32, 0x62, 0x36, 0x36, 0x4b, 0x34, 0x99, 0x67, 0x31, 0x93, 0x8c}; + break; + } + case 0x81: { + // Initiate Challenge + m_infinity_base->DescrambleAndSeed(transfer->buffer, sequence, q_result); + break; + } + case 0x83: { + // Challenge Response + m_infinity_base->GetNextAndScramble(sequence, q_result); + break; + } + case 0x90: + case 0x92: + case 0x93: + case 0x95: + case 0x96: { + // Color commands + m_infinity_base->GetBlankResponse(sequence, q_result); + break; + } + case 0xA1: { + // Get Present Figures + m_infinity_base->GetPresentFigures(sequence, q_result); + break; + } + case 0xA2: { + // Read Block from Figure + m_infinity_base->QueryBlock(transfer->buffer[4], transfer->buffer[5], q_result, + sequence); + break; + } + case 0xA3: { + // Write block to figure + m_infinity_base->WriteBlock(transfer->buffer[4], transfer->buffer[5], + &transfer->buffer[7], q_result, sequence); + break; + } + case 0xB4: { + // Get figure ID + m_infinity_base->GetFigureIdentifier(transfer->buffer[4], sequence, q_result); + break; + } + case 0xB5: { + // Get status? + m_infinity_base->GetBlankResponse(sequence, q_result); + break; + } + default: + LOG_ERROR(Lib_Usbd, "Unhandled Infinity Query: {}", command); + break; + } + + m_queries.push(q_result); + break; + } + default: + LOG_ERROR(Lib_Usbd, "Unhandled Infinity Endpoint: {}", transfer->endpoint); + break; + } + return LIBUSB_TRANSFER_COMPLETED; +} + +void InfinityFigure::Save() { + if (!infFile.IsOpen()) + return; + + infFile.Seek(0); + infFile.Write(data); +} +} // namespace Libraries::Usbd \ No newline at end of file diff --git a/src/core/libraries/usbd/emulated/infinity.h b/src/core/libraries/usbd/emulated/infinity.h new file mode 100644 index 000000000..4c8a5305f --- /dev/null +++ b/src/core/libraries/usbd/emulated/infinity.h @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common/io_file.h" +#include "core/libraries/usbd/usb_backend.h" + +namespace Libraries::Usbd { + +constexpr u16 INFINITY_BLOCK_COUNT = 0x14; +constexpr u16 INFINITY_BLOCK_SIZE = 0x10; +constexpr u16 INFINITY_FIGURE_SIZE = INFINITY_BLOCK_COUNT * INFINITY_BLOCK_SIZE; +constexpr u8 MAX_INFINITY_FIGURES = 9; + +struct InfinityFigure final { + Common::FS::IOFile infFile; + std::array data{}; + bool present = false; + u8 order_added = 255; + void Save(); +}; + +class InfinityBase final : public UsbEmulatedImpl { +public: + InfinityBase(); + ~InfinityBase() override = default; + + void GetBlankResponse(u8 sequence, std::array& reply_buf); + void DescrambleAndSeed(u8* buf, u8 sequence, std::array& reply_buf); + void GetNextAndScramble(u8 sequence, std::array& reply_buf); + void GetPresentFigures(u8 sequence, std::array& reply_buf); + void QueryBlock(u8 fig_num, u8 block, std::array& reply_buf, u8 sequence); + void WriteBlock(u8 fig_num, u8 block, const u8* to_write_buf, std::array& reply_buf, + u8 sequence); + void GetFigureIdentifier(u8 fig_num, u8 sequence, std::array& reply_buf); + std::optional> PopAddedRemovedResponse(); + + void LoadFigure(std::string file_name, u8 pad, u8 slot) override; + void RemoveFigure(u8 pad, u8 slot, bool full_remove) override; + void MoveFigure(u8 new_pad, u8 new_index, u8 old_pad, u8 old_index) override {} + void TempRemoveFigure(u8 index) override {} + void CancelRemoveFigure(u8 index) override {} + + void LoadInfinityFigure(const std::array& buf, Common::FS::IOFile file, + u8 position); + +protected: + std::mutex infinity_mutex; + std::array infinity_figures; + +private: + u8 GenerateChecksum(const std::array& data, int num_of_bytes) const; + u32 Descramble(u64 num_to_descramble); + u64 Scramble(u32 num_to_scramble, u32 garbage); + void GenerateSeed(u32 seed); + u32 GetNext(); + InfinityFigure& GetFigureByOrder(u8 order_added); + u8 DeriveFigurePosition(u8 position); + + u32 random_a = 0; + u32 random_b = 0; + u32 random_c = 0; + u32 random_d = 0; + + u8 m_figure_order = 0; + std::queue> m_figure_added_removed_responses; +}; + +class InfinityBackend final : public UsbEmulatedBackend { +protected: + libusb_endpoint_descriptor* FillEndpointDescriptorPair() override; + libusb_interface_descriptor* FillInterfaceDescriptor( + libusb_endpoint_descriptor* descs) override; + libusb_config_descriptor* FillConfigDescriptor(libusb_interface* inter) override; + libusb_device_descriptor* FillDeviceDescriptor() override; + + s32 ControlTransfer(libusb_device_handle* dev_handle, u8 bmRequestType, u8 bRequest, u16 wValue, + u16 wIndex, u8* data, u16 wLength, u32 timeout) override { + return LIBUSB_SUCCESS; + } + + libusb_transfer_status HandleAsyncTransfer(libusb_transfer* transfer) override; + + std::shared_ptr GetImplRef() override { + return m_infinity_base; + } + +private: + std::shared_ptr m_infinity_base = std::make_shared(); + + std::array m_endpoint_out_extra = {0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x1d, 0x00}; + std::vector m_endpoint_descriptors = { + {0x7, 0x5, 0x81, 0x3, 0x20, 0x1, 0x0, 0x0}, + {0x7, 0x5, 0x1, 0x3, 0x20, 0x1, 0x0, 0x0, m_endpoint_out_extra.data(), 9}}; + std::vector m_interface_descriptors = { + {0x9, 0x4, 0x0, 0x0, 0x2, 0x3, 0x0, 0x0, 0x0}}; + std::vector m_config_descriptors = { + {0x9, 0x2, 0x29, 0x1, 0x1, 0x0, 0x80, 0xFA}}; + std::vector m_device_descriptors = { + {0x12, 0x1, 0x200, 0x0, 0x0, 0x0, 0x20, 0x0E6F, 0x0129, 0x200, 0x1, 0x2, 0x3, 0x1}}; + + std::queue> m_queries; +}; +} // namespace Libraries::Usbd \ No newline at end of file diff --git a/src/core/libraries/usbd/usbd.h b/src/core/libraries/usbd/usbd.h index 60a3e753b..f189e0de2 100644 --- a/src/core/libraries/usbd/usbd.h +++ b/src/core/libraries/usbd/usbd.h @@ -4,6 +4,7 @@ #pragma once #include "common/types.h" +#include "emulated/infinity.h" #include "emulated/skylander.h" #include "usb_backend.h" @@ -35,7 +36,7 @@ using SceUsbdTransferCallback = void PS4_SYSV_ABI (*)(SceUsbdTransfer* transfer) // TODO: implement emulated devices using SkylandersPortalBackend = SkylanderBackend; -using InfinityBaseBackend = UsbRealBackend; +using InfinityBaseBackend = InfinityBackend; using DimensionsToypadBackend = UsbRealBackend; enum class SceUsbdSpeed : u32 {