diff --git a/Source/Core/Core/IOS/USB/Bluetooth/RealtekFirmwareLoader.cpp b/Source/Core/Core/IOS/USB/Bluetooth/RealtekFirmwareLoader.cpp index 30bcad24d9..5ef2c17425 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/RealtekFirmwareLoader.cpp +++ b/Source/Core/Core/IOS/USB/Bluetooth/RealtekFirmwareLoader.cpp @@ -9,6 +9,7 @@ #include "Core/IOS/USB/Bluetooth/RealtekFirmwareLoader.h" #include +#include #include "Common/BitUtils.h" #include "Common/FileUtil.h" @@ -32,6 +33,10 @@ constexpr u16 RTL_ROM_LMP_8922A = 0x8922; constexpr std::string_view RTL_EPATCH_SIGNATURE = "Realtech"; constexpr std::string_view RTL_EPATCH_SIGNATURE_V2 = "RTBTCore"; +constexpr u32 RTL_PATCH_SNIPPETS = 0x01; +constexpr u32 RTL_PATCH_DUMMY_HEADER = 0x02; +constexpr u32 RTL_PATCH_SECURITY_HEADER = 0x03; + #pragma pack(push, 1) struct rtl_vendor_cmd { @@ -45,6 +50,35 @@ struct rtl_epatch_header u16 num_patches; }; static_assert(sizeof(rtl_epatch_header) == 8 + 4 + 2); +struct rtl_epatch_header_v2 +{ + std::array signature; + std::array fw_version; + u32 num_sections; +}; +static_assert(sizeof(rtl_epatch_header_v2) == 8 + 8 + 4); +struct rtl_section +{ + u32 opcode; + u32 len; +}; +static_assert(sizeof(rtl_section) == 8); +struct rtl_section_hdr +{ + u16 num_subsecs; + u16 reserved; +}; +static_assert(sizeof(rtl_section_hdr) == 4); +struct rtl_subsec_hdr +{ + u8 eco; + u8 prio; + // Only used with RTL_PATCH_SECURITY_HEADER opcode. + u8 key_id; + u8 reserved; + u32 len; +}; +static_assert(sizeof(rtl_subsec_hdr) == 8); struct rtl_download_cmd { static constexpr int DATA_LEN = 252; @@ -179,11 +213,10 @@ bool IsKnownRealtekBluetoothDevice(u16 vid, u16 pid) return std::ranges::find(realtek_ids, std::pair(vid, pid)) != std::end(realtek_ids); } -// Returns the data to be loaded, or an empty buffer on error. -static Common::UniqueBuffer ParseFirmware(std::span fw_data, - std::span cfg_data, - const hci_read_local_ver_rp& local_ver, - u8 rom_version) +static Common::UniqueBuffer ParseFirmwareV1(std::span fw_data, + std::span cfg_data, + const hci_read_local_ver_rp& local_ver, + u8 rom_version) { if (fw_data.size() < sizeof(rtl_epatch_header)) { @@ -192,20 +225,6 @@ static Common::UniqueBuffer ParseFirmware(std::span fw_data, } const rtl_epatch_header epatch_info = Common::BitCastPtr(fw_data.data()); - if (std::memcmp(epatch_info.signature.data(), RTL_EPATCH_SIGNATURE.data(), - epatch_info.signature.size()) != 0) - { - if (std::memcmp(epatch_info.signature.data(), RTL_EPATCH_SIGNATURE_V2.data(), - epatch_info.signature.size()) == 0) - { - ERROR_LOG_FMT(IOS_WIIMOTE, "EPATCH v2 not implemented. Please report device to developers."); - } - else - { - ERROR_LOG_FMT(IOS_WIIMOTE, "Bad EPATCH signature"); - } - return {}; - } const auto fw_version = epatch_info.fw_version; const auto num_patches = epatch_info.num_patches; @@ -315,6 +334,203 @@ static Common::UniqueBuffer ParseFirmware(std::span fw_data, return {}; } +// Sorted subspans. +// Linux's btrtl.c seems to sort by ascending priority with duplicates in reverse-insertion order. +// I don't know if that's intentional or necessary, but it's replicated with std::greater sorting +// and reverse iteration when concatenating. +using SubsectionData = std::multimap, std::greater<>>; + +static bool ParseSection(SubsectionData* subsection_data, u32 opcode, std::span data, + u8 rom_version, u8 key_id) +{ + if (data.size() < sizeof(rtl_section_hdr)) + return false; + + const rtl_section_hdr header = Common::BitCastPtr(data.data()); + auto const* ptr = data.data() + sizeof(header); + auto const* const end_ptr = data.data() + data.size(); + + const auto subsec_count = header.num_subsecs; + for (u32 i = 0; i != subsec_count; ++i) + { + if (end_ptr - ptr < unsigned(sizeof(rtl_subsec_hdr))) + return false; + + const rtl_subsec_hdr subsec = Common::BitCastPtr(ptr); + ptr += sizeof(subsec); + + DEBUG_LOG_FMT(IOS_WIIMOTE, "subsec: eco=0x{:02x}, prio=0x{:02x}, len=0x{:08x}", subsec.eco, + subsec.prio, subsec.len); + + if (end_ptr - ptr < subsec.len) + return false; + + const auto subsec_span = std::span{ptr, ptr + subsec.len}; + ptr += subsec.len; + + if (subsec.eco != rom_version + 1) + continue; + + if (opcode == RTL_PATCH_SECURITY_HEADER && subsec.key_id != key_id) + continue; + + subsection_data->emplace(subsec.prio, subsec_span); + } + + return true; +} + +static Common::UniqueBuffer ParseFirmwareV2(std::span fw_data, + std::span cfg_data, u8 rom_version, + u8 key_id) +{ + auto const log_undersized_error = [] { ERROR_LOG_FMT(IOS_WIIMOTE, "Undersized firmware file"); }; + + // Linux's btrtl.c cuts off the last 7 bytes. + // Presumably there is some signature at the end of the file. + fw_data = fw_data.first(fw_data.size() - 7); + + if (fw_data.size() < sizeof(rtl_epatch_header_v2)) + { + log_undersized_error(); + return {}; + } + + const rtl_epatch_header_v2 header = Common::BitCastPtr(fw_data.data()); + + const auto section_count = header.num_sections; + + INFO_LOG_FMT(IOS_WIIMOTE, "fw_version={:08x}-{:08x} num_sections={}", header.fw_version[0], + header.fw_version[1], section_count); + + const auto* ptr = fw_data.data() + sizeof(rtl_epatch_header_v2); + const auto* const end_ptr = fw_data.data() + fw_data.size(); + + SubsectionData subsection_data; + + for (u32 i = 0; i != section_count; ++i) + { + if (end_ptr - ptr < unsigned(sizeof(rtl_section))) + { + log_undersized_error(); + return {}; + } + + const rtl_section section = Common::BitCastPtr(ptr); + ptr += sizeof(rtl_section); + + if (end_ptr - ptr < section.len) + { + log_undersized_error(); + return {}; + } + + const auto section_span = std::span{ptr, ptr + section.len}; + ptr += section_span.size(); + + const auto opcode = section.opcode; + DEBUG_LOG_FMT(IOS_WIIMOTE, "opcode 0x{:04x}", opcode); + + switch (opcode) + { + case RTL_PATCH_SECURITY_HEADER: + // If key_id from chip is zero, ignore all security headers. + if (key_id == 0x00) + break; + [[fallthrough]]; + case RTL_PATCH_SNIPPETS: + case RTL_PATCH_DUMMY_HEADER: + { + if (!ParseSection(&subsection_data, opcode, section_span, rom_version, key_id)) + { + log_undersized_error(); + return {}; + } + break; + } + default: + break; + } + } + + std::size_t total_length = 0; + for (const auto& span : subsection_data | std::ranges::views::values) + total_length += span.size(); + + if (total_length == 0) + return {}; + + // Concatenate the subsection and config data. + Common::UniqueBuffer result{total_length + cfg_data.size()}; + std::ranges::copy(subsection_data | std::ranges::views::values | std::views::reverse | + std::views::join, + result.begin()); + std::ranges::copy(cfg_data, result.end() - cfg_data.size()); + return result; +} + +static std::optional> ReadReg16(LibUSBBluetoothAdapter& adapter, + const rtl_vendor_cmd& cmd) +{ + std::array read_result{}; + if (!adapter.SendBlockingCommand( + Common::AsU8Span(HCICommandPayload<0xfc61, rtl_vendor_cmd>{.command = cmd}), + Common::AsWritableU8Span(read_result))) + { + return std::nullopt; + } + + if (read_result[0] != 0x00) + { + ERROR_LOG_FMT(IOS_WIIMOTE, "ReadReg16 status: 0x{:02x}", read_result[0]); + return std::nullopt; + } + + return std::array{read_result[1], read_result[2]}; +} + +// Returns the data to be loaded, or an empty buffer on error. +static Common::UniqueBuffer ParseFirmware(std::string_view fw_data, + std::span cfg_data, + const hci_read_local_ver_rp& local_ver, + LibUSBBluetoothAdapter& adapter) +{ + // Read ROM version. + rtl_rom_version_evt read_rom_ver{}; + if (!adapter.SendBlockingCommand(Common::AsU8Span(hci_cmd_hdr_t{0xfc6d, 0}), + Common::AsWritableU8Span(read_rom_ver))) + { + return {}; + } + if (read_rom_ver.status != 0x00) + { + ERROR_LOG_FMT(IOS_WIIMOTE, "Bad ROM version status: 0x{:02x}", read_rom_ver.status); + return {}; + } + + const u8 rom_version = read_rom_ver.version; + INFO_LOG_FMT(IOS_WIIMOTE, "ROM version: 0x{:02x}", rom_version); + + // EPATCH V1: + if (fw_data.starts_with(RTL_EPATCH_SIGNATURE)) + return ParseFirmwareV1(fw_data, cfg_data, local_ver, rom_version); + + // Read Key ID. + static constexpr rtl_vendor_cmd RTL_SEC_PROJ = {{0x10, 0xa4, 0x0d, 0x00, 0xb0}}; + const auto read_key_id = ReadReg16(adapter, RTL_SEC_PROJ); + if (!read_key_id.has_value()) + return {}; + const u8 key_id = (*read_key_id)[0]; + INFO_LOG_FMT(IOS_WIIMOTE, "Key ID: 0x{:02x}", key_id); + + // EPATCH V2: + if (fw_data.starts_with(RTL_EPATCH_SIGNATURE_V2)) + return ParseFirmwareV2(fw_data, cfg_data, rom_version, key_id); + + ERROR_LOG_FMT(IOS_WIIMOTE, "Bad EPATCH signature"); + return {}; +} + // Loads the provided data on the adapter. static bool LoadFirmware(LibUSBBluetoothAdapter& adapter, std::span data) { @@ -505,6 +721,14 @@ bool InitializeRealtekBluetoothDevice(LibUSBBluetoothAdapter& adapter) return false; } + // FYI: Linux's btrtl.c does additional poorly documented ReadReg16 for RTL_ROM_LMP_8822B + // If someone comes across one of these devices they may need to implement this additional logic. + + // FYI: Linux tries to first load a "_v2.bin" file for RTL8852C. + // Both "rtl8852cu_fw.bin" and "rtl8852cu_fw_v2.bin" exist in the linux-firmware repository. + // I don't know why they don't just put EPATCH v2 in the normal _fw.bin like every other device. + // Additional logic could be added to better support this uncommon device if wanted. + const auto firmware_name = GetFirmwareName(local_ver); // If firmware has already been loaded then the subversion changes and we won't find a match. if (firmware_name.empty()) @@ -556,26 +780,10 @@ bool InitializeRealtekBluetoothDevice(LibUSBBluetoothAdapter& adapter) return LoadFirmware(adapter, fw_data); } - // Read ROM version. - rtl_rom_version_evt read_rom_ver{}; - if (!adapter.SendBlockingCommand(Common::AsU8Span(hci_cmd_hdr_t{0xfc6d, 0}), - Common::AsWritableU8Span(read_rom_ver))) - { - return false; - } - if (read_rom_ver.status != 0x00) - { - ERROR_LOG_FMT(IOS_WIIMOTE, "Bad ROM version status: 0x{:02x}", read_rom_ver.status); - return false; - } - - const u8 rom_version = read_rom_ver.version; - INFO_LOG_FMT(IOS_WIIMOTE, "ROM version: 0x{:02x}", rom_version); - // Apparently only certain devices require the config binary. We load it whenever present. std::string config_data; File::ReadFileToString(config_path, config_data); - const auto firmware = ParseFirmware(fw_data, config_data, local_ver, rom_version); + const auto firmware = ParseFirmware(fw_data, config_data, local_ver, adapter); return LoadFirmware(adapter, firmware); }