From 6850e2e5a8524180fb7cb014ff3703ed2bb7b689 Mon Sep 17 00:00:00 2001 From: AurisDSP <235008503+AurisDSP@users.noreply.github.com> Date: Sat, 9 May 2026 07:27:01 -0400 Subject: [PATCH 1/5] Apple Silicon: consolidate JIT W^X handling with RAII guards Address review feedback on #18701 (cc @elad335): combine the SPU worker fix from #18701, the SPRX Loader fix from #18703, and three additional similar W^X leaks discovered while auditing the codebase for the same pattern. Use Allman-style braces to match RPCS3 coding style. Background: On AArch64 Apple Silicon, MAP_JIT pages enforce W^X per-thread. pthread_jit_write_protect_np(false) enables write mode and pthread_jit_write_protect_np(true) restores execute mode. When code takes an early return or throws between these calls, the thread is left in write mode, which can cause segfaults on subsequent code fetches or inconsistent state at thread teardown. Fixes applied (all gated on __APPLE__): 1. Emu/Cell/SPUCommonRecompiler.cpp - SPU cache worker thread Add RAII guard so execute mode is restored on worker exit. 2. Emu/System.cpp - SPRX Loader thread Enter write mode (was missing entirely) so ppu_initialize() and ppu_precompile() can write to MAP_JIT pages, and pair with an RAII guard. Reproducer: Red Dead Redemption (BLUS30418) crashes ~12s into boot at 0x300010000 without this fix. 3. Emu/Cell/SPULLVMRecompiler.cpp - SPU LLVM compile path The compile function enters write mode, then has an early "return nullptr" path on rebuild_ubertrampoline failure that skipped the explicit restore. Add RAII guard so execute mode is restored on every exit path. The existing explicit restore before the cache-flush asm directives is preserved. 4. Emu/Cell/PPUThread.cpp - PPU LLVM worker thread (operator()) Worker entered write mode but never restored it on operator() return. Add RAII guard. 5. Emu/Cell/PPUThread.cpp - ppu_initialize() main path This scope alternates write/execute mode and contains an early "return compiled_new" at the empty-jits check plus a final return that both leak write mode. Add RAII guard so execute mode is always restored on exit. Intermediate explicit transitions for the symbol-resolver invocation are preserved. No behavioral change on x86_64 or non-Apple ARM64 (all changes are inside #ifdef __APPLE__ / #if defined(__APPLE__)). Supersedes #18703. --- rpcs3/Emu/Cell/PPUThread.cpp | 24 ++++++++++++++++++++++++ rpcs3/Emu/Cell/SPUCommonRecompiler.cpp | 13 +++++++++++++ rpcs3/Emu/Cell/SPULLVMRecompiler.cpp | 12 ++++++++++++ rpcs3/Emu/System.cpp | 18 ++++++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/rpcs3/Emu/Cell/PPUThread.cpp b/rpcs3/Emu/Cell/PPUThread.cpp index f5d91cc519..c43ac5d73c 100644 --- a/rpcs3/Emu/Cell/PPUThread.cpp +++ b/rpcs3/Emu/Cell/PPUThread.cpp @@ -5289,7 +5289,19 @@ bool ppu_initialize(const ppu_module& info, bool check_only, u64 file_s thread_ctrl::scoped_priority low_prio(-1); #ifdef __APPLE__ + // Apple Silicon W^X: PPU LLVM worker enables write mode for + // JIT memory. Pair it with an RAII guard so execute mode + // is restored on every exit path (return, exception, etc.) + // to keep per-thread state consistent at teardown. pthread_jit_write_protect_np(false); + + struct jit_write_guard + { + ~jit_write_guard() + { + pthread_jit_write_protect_np(true); + } + } _jit_guard; #endif for (u32 i = work_cv++; i < workload.size(); i = work_cv++, g_progr_pdone++) { @@ -5421,7 +5433,19 @@ bool ppu_initialize(const ppu_module& info, bool check_only, u64 file_s // Jit can be null if the loop doesn't ever enter. #ifdef __APPLE__ + // Apple Silicon W^X: this scope toggles write/execute mode multiple + // times below. Use an RAII guard so execute mode is always restored + // on every exit path, including the early "return compiled_new" at + // the empty-jits check and the normal return at function end. pthread_jit_write_protect_np(false); + + struct jit_write_guard + { + ~jit_write_guard() + { + pthread_jit_write_protect_np(true); + } + } _jit_guard; #endif // Try to patch all single and unregistered BLRs with the same function (TODO: Maybe generalize it into PIC code detection and patching) ppu_intrp_func_t BLR_func = nullptr; diff --git a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp index 8b4bd15e26..dffca21cae 100644 --- a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp +++ b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp @@ -857,7 +857,20 @@ void spu_cache::initialize(bool build_existing_cache) named_thread_group workers("SPU Worker ", worker_count, [&]() -> uint { #ifdef __APPLE__ + // Apple Silicon W^X: enable JIT write mode for this worker and + // pair it with an RAII guard so execute mode is restored on + // every exit path (return, exception, etc.). Leaving a worker + // in write mode at teardown can leave per-thread state + // inconsistent on AArch64. pthread_jit_write_protect_np(false); + + struct jit_write_guard + { + ~jit_write_guard() + { + pthread_jit_write_protect_np(true); + } + } _jit_guard; #endif // Set low priority thread_ctrl::scoped_priority low_prio(-1); diff --git a/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp b/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp index 5b63ca80cc..927d7ac187 100644 --- a/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp +++ b/rpcs3/Emu/Cell/SPULLVMRecompiler.cpp @@ -3463,7 +3463,19 @@ public: } #if defined(__APPLE__) + // Apple Silicon W^X: enter write mode for JIT memory and pair + // it with an RAII guard so execute mode is restored on every + // exit path (the early "return nullptr" below would otherwise + // leave the thread in write mode permanently). pthread_jit_write_protect_np(false); + + struct jit_write_guard + { + ~jit_write_guard() + { + pthread_jit_write_protect_np(true); + } + } _jit_guard; #endif if (g_cfg.core.spu_debug) diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 199aad38ee..29016a408a 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -1835,6 +1835,24 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, g_fxo->init("SPRX Loader"sv, [this, dir_queue, is_fast = m_precompilation_option.is_fast]() mutable { +#ifdef __APPLE__ + // Apple Silicon W^X: this thread invokes ppu_initialize() + // and ppu_precompile(), which write into MAP_JIT pages. + // Without enabling write mode here, these writes segfault + // before the game can boot (reproducible: RDR BLUS30418 + // crashes ~12s into boot at 0x300010000). Pair the enable + // with an RAII guard so execute mode is restored on every + // exit path (return, exception, etc.). + pthread_jit_write_protect_np(false); + + struct jit_write_guard + { + ~jit_write_guard() + { + pthread_jit_write_protect_np(true); + } + } _jit_guard; +#endif std::vector*> mod_list; if (auto& _main = *ensure(g_fxo->try_get>()); !_main.path.empty()) From b34eba2fb3ca6878a62ac7cbe67aaefd6cadbe0c Mon Sep 17 00:00:00 2001 From: schm1dtmac Date: Sat, 9 May 2026 13:17:32 +0100 Subject: [PATCH 2/5] Revert PPUThread changes Causes GOW3 segfaults at the end of PPU linking --- rpcs3/Emu/Cell/PPUThread.cpp | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/rpcs3/Emu/Cell/PPUThread.cpp b/rpcs3/Emu/Cell/PPUThread.cpp index c43ac5d73c..f5d91cc519 100644 --- a/rpcs3/Emu/Cell/PPUThread.cpp +++ b/rpcs3/Emu/Cell/PPUThread.cpp @@ -5289,19 +5289,7 @@ bool ppu_initialize(const ppu_module& info, bool check_only, u64 file_s thread_ctrl::scoped_priority low_prio(-1); #ifdef __APPLE__ - // Apple Silicon W^X: PPU LLVM worker enables write mode for - // JIT memory. Pair it with an RAII guard so execute mode - // is restored on every exit path (return, exception, etc.) - // to keep per-thread state consistent at teardown. pthread_jit_write_protect_np(false); - - struct jit_write_guard - { - ~jit_write_guard() - { - pthread_jit_write_protect_np(true); - } - } _jit_guard; #endif for (u32 i = work_cv++; i < workload.size(); i = work_cv++, g_progr_pdone++) { @@ -5433,19 +5421,7 @@ bool ppu_initialize(const ppu_module& info, bool check_only, u64 file_s // Jit can be null if the loop doesn't ever enter. #ifdef __APPLE__ - // Apple Silicon W^X: this scope toggles write/execute mode multiple - // times below. Use an RAII guard so execute mode is always restored - // on every exit path, including the early "return compiled_new" at - // the empty-jits check and the normal return at function end. pthread_jit_write_protect_np(false); - - struct jit_write_guard - { - ~jit_write_guard() - { - pthread_jit_write_protect_np(true); - } - } _jit_guard; #endif // Try to patch all single and unregistered BLRs with the same function (TODO: Maybe generalize it into PIC code detection and patching) ppu_intrp_func_t BLR_func = nullptr; From 29b4577fdb1e9d168097ad6149c2e1772f936cdc Mon Sep 17 00:00:00 2001 From: brian218 Date: Sun, 10 May 2026 12:36:35 +0800 Subject: [PATCH 3/5] =?UTF-8?q?USIO:=20Implemented=20BanaPassport=20(?= =?UTF-8?q?=E3=83=90=E3=83=8A=E3=83=91=E3=82=B9=E3=83=9D=E3=83=BC=E3=83=88?= =?UTF-8?q?)=20card=20reader=20emulation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rpcs3/Emu/Io/usio.cpp | 232 +++++++++++++++++++++++++++++++++++-- rpcs3/Emu/Io/usio.h | 4 + rpcs3/Emu/Io/usio_config.h | 2 + 3 files changed, 231 insertions(+), 7 deletions(-) diff --git a/rpcs3/Emu/Io/usio.cpp b/rpcs3/Emu/Io/usio.cpp index 21dc241d81..f707ae8e6a 100644 --- a/rpcs3/Emu/Io/usio.cpp +++ b/rpcs3/Emu/Io/usio.cpp @@ -32,6 +32,7 @@ void fmt_class_string::format(std::string& out, u64 arg) case usio_btn::tekken_button3: return "Tekken Button 3"; case usio_btn::tekken_button4: return "Tekken Button 4"; case usio_btn::tekken_button5: return "Tekken Button 5"; + case usio_btn::card_tapping: return "Card Tapping"; case usio_btn::count: return "Count"; } @@ -42,6 +43,7 @@ void fmt_class_string::format(std::string& out, u64 arg) struct usio_memory { std::vector backup_memory; + std::array, g_cfg_usio.players.size()> card_data{}; usio_memory() = default; usio_memory(const usio_memory&) = delete; @@ -175,6 +177,16 @@ void usb_device_usio::load_backup() } usio_backup_file.read(memory.backup_memory.data(), file_size); + + for (usz i = 0; i < memory.card_data.size(); i++) + { + if (fs::file usio_card_file; + usio_card_file.open(fmt::format("%s/caches/usio_card_p%d.bin", rpcs3::utils::get_hdd1_dir(), i + 1), fs::read) && + usio_card_file.size() == memory.card_data[i].size()) + { + usio_card_file.read(memory.card_data[i].data(), memory.card_data[i].size()); + } + } } void usb_device_usio::save_backup() @@ -261,6 +273,10 @@ void usb_device_usio::translate_input_taiko() if (pressed) std::memcpy(input_buf.data() + 34 + offset, &c_hit, sizeof(u16)); break; + case usio_btn::card_tapping: + if (pressed) + tap_card(player); + break; default: break; } @@ -271,6 +287,8 @@ void usb_device_usio::translate_input_taiko() digital_input |= 0x80; }; + for (usz i = 0; i < m_io_status.size(); i++) + m_io_status[i].card_tapped = false; for (usz i = 0; i < g_cfg_usio.players.size(); i++) translate_from_pad(i, i); @@ -384,6 +402,10 @@ void usb_device_usio::translate_input_tekken() if (pressed) input |= 0x80000000ULL << shift; break; + case usio_btn::card_tapping: + if (pressed) + tap_card(player); + break; default: break; } @@ -398,6 +420,8 @@ void usb_device_usio::translate_input_tekken() } }; + for (usz i = 0; i < m_io_status.size(); i++) + m_io_status[i].card_tapped = false; for (usz i = 0; i < g_cfg_usio.players.size(); i++) translate_from_pad(i, i); @@ -414,6 +438,194 @@ void usb_device_usio::translate_input_tekken() response = std::move(input_buf); } +void usb_device_usio::emulate_card_reader(std::vector& buf, u16 reg) +{ + static std::array, 2> pending_response = {}; + usz reader_index = 0; + + const auto calculate_checksum = [](bool check, std::vector& data) -> bool + { + if (data.size() < 0x06) + return false; + + const usz data_end = data.size() - 2; + u8 sum = data[3] + data[4]; + + for (usz i = 5; i < data_end; i++) + sum -= data[i]; + + if (check) + return *reinterpret_cast*>(&data[data_end]) == sum; + + *reinterpret_cast*>(&data[data_end]) = sum; + return true; + }; + + switch (reg) + { + case 0x0080: + case 0x0090: + { + reader_index = reg == 0x0080 ? 0 : 1; + buf = {0x02, 0x03, 0x00, 0x00, 0xFF, 0x0F, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x10, 0x00}; + *reinterpret_cast*>(buf.data() + 2) = ::narrow(pending_response[reader_index].size()); + break; + } + case 0x7000: + case 0x7800: + { + reader_index = reg == 0x7000 ? 0 : 1; + buf = std::move(pending_response[reader_index]); + pending_response[reader_index].clear(); // Ensure its empty state after being moved + break; + } + case 0x7400: + case 0x7C00: + { + if (!calculate_checksum(true, buf)) + break; + reader_index = reg == 0x7400 ? 0 : 1; + const auto& status = ::at32(m_io_status, reader_index); + const usz card_player = reader_index * 2 + status.card_index; + const u8 payload_length = buf[3]; + const u8 command = buf[4]; + const u8* const payload = &buf[6]; + switch (command) + { + case 0xE8: + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x0D, 0xF3, 0xD5, 0x07, 0xDC, 0xF4, 0x3F, 0x11, 0x4D, 0x85, 0x61, 0xF1, 0x26, 0x6A, 0x87, 0xC9, 0x00}; + break; + } + case 0xEE: + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x0A, 0xF6, 0xD5, 0x07, 0xFF, 0x3F, 0x0E, 0xF1, 0xFF, 0x3F, 0x0E, 0xF1, 0xAA, 0x00}; + break; + } + case 0xF1: + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x03, 0xFD, 0xD5, 0x41, 0x00, 0xEA, 0x00}; + break; + } + case 0xF2: + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x02, 0xFE, 0xD5, 0x33, 0xF8, 0x00}; + break; + } + case 0xF7: + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x03, 0xFD, 0xD5, 0x4B, 0x00, 0xE0, 0x00}; + break; + } + case 0xFA: + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x02, 0xFE, 0xD5, 0x33, 0xF8, 0x00}; + break; + } + case 0xFB: + { + if (payload_length >= 5) + { + if (*reinterpret_cast*>(&payload[0]) == 0x0140) + { + if (payload[3] < 4) + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x13, 0xED, 0xD5, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEA, 0x00}; + std::memcpy(pending_response[reader_index].data() + 8, g_fxo->get().card_data[card_player].data() + payload[3] * 0x10, 0x10); + } + else + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x03, 0xFD, 0xD5, 0x41, 0x13, 0xD7, 0x00}; + } + } + else + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x03, 0xFD, 0xD5, 0x09, 0x00, 0x22, 0x00}; + } + } + break; + } + case 0xFC: + { + if (payload_length >= 2) + { + switch (payload[0]) + { + case 0x52: + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x04, 0xFC, 0xD5, 0x53, 0x01, 0x00, 0xD7, 0x00}; + break; + case 0x0E: + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x02, 0xFE, 0xD5, 0x0F, 0x1C, 0x00}; + break; + case 0x4A: + if (status.card_tapped) + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x0C, 0xF4, 0xD5, 0x4B, 0x01, 0x01, 0x00, 0x04, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0xCE, 0x00}; + std::memcpy(pending_response[reader_index].data() + 0x13, g_fxo->get().card_data[card_player].data(), 4); + } + else + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x03, 0xFD, 0xD5, 0x4B, 0x00, 0xE0, 0x00}; + } + break; + case 0x32: + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x02, 0xFE, 0xD5, 0x33, 0xF8, 0x00}; + break; + default: + break; + } + } + break; + } + case 0xFD: + { + if (payload_length >= 2) + { + switch (payload[0]) + { + case 0x18: + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x02, 0xFE, 0xD5, 0x19, 0x12, 0x00}; + break; + case 0x12: + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x02, 0xFE, 0xD5, 0x13, 0x18, 0x00}; + break; + default: + break; + } + } + break; + } + case 0xFE: + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x05, 0xFB, 0xD5, 0x0D, 0x00, 0x06, 0x00, 0x18, 0x00}; + break; + } + case 0xFF: + { + pending_response[reader_index] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00}; + break; + } + default: + { + usio_log.trace("Unhandled card reader command: 0x%02X", command); + break; + } + } + calculate_checksum(false, pending_response[reader_index]); + break; + } + default: + break; + } +} + +void usb_device_usio::tap_card(usz player) +{ + auto& status = ::at32(m_io_status, player / 2); + status.card_tapped = true; + status.card_index = player % 2; +} + void usb_device_usio::usio_write(u8 channel, u16 reg, std::vector& data) { const auto get_u16 = [&](std::string_view usio_func) -> u16 @@ -461,6 +673,16 @@ void usb_device_usio::usio_write(u8 channel, u16 reg, std::vector& data) usio_log.trace("SetHopperRequest(Hopper: %d, Limit: 0x%04X)", (reg - 0x4A) / 0x10, get_u16("SetHopperLimit")); break; } + case 0x0080: + case 0x008D: + case 0x0090: + case 0x009D: + case 0x7400: + case 0x7C00: + { + emulate_card_reader(data, reg); + break; + } default: { usio_log.trace("Unhandled channel 0 register write(reg: 0x%04X, size: 0x%04X, data: %s)", reg, data.size(), fmt::buf_to_hexstring(data.data(), data.size())); @@ -502,15 +724,11 @@ void usb_device_usio::usio_read(u8 channel, u16 reg, u16 size) break; } case 0x0080: - { - // Card reader check - 1 - response = {0x02, 0x03, 0x06, 0x00, 0xFF, 0x0F, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x10, 0x00}; - break; - } + case 0x0090: case 0x7000: + case 0x7800: { - // Card reader check - 2 - // No data returned + emulate_card_reader(response, reg); break; } case 0x1000: diff --git a/rpcs3/Emu/Io/usio.h b/rpcs3/Emu/Io/usio.h index 7a83e7a8ca..d88032ed5a 100644 --- a/rpcs3/Emu/Io/usio.h +++ b/rpcs3/Emu/Io/usio.h @@ -20,6 +20,8 @@ private: void save_backup(); void translate_input_taiko(); void translate_input_tekken(); + void emulate_card_reader(std::vector& buf, u16 reg); + void tap_card(usz player); void usio_write(u8 channel, u16 reg, std::vector& data); void usio_read(u8 channel, u16 reg, u16 size); void usio_init(u8 channel, u16 reg, u16 size); @@ -34,7 +36,9 @@ private: bool test_on = false; bool test_key_pressed = false; bool coin_key_pressed = false; + bool card_tapped = false; le_t coin_counter = 0; + usz card_index = 0; }; std::array m_io_status; diff --git a/rpcs3/Emu/Io/usio_config.h b/rpcs3/Emu/Io/usio_config.h index 4c5fe6f017..0a0ecaa454 100644 --- a/rpcs3/Emu/Io/usio_config.h +++ b/rpcs3/Emu/Io/usio_config.h @@ -21,6 +21,7 @@ enum class usio_btn tekken_button3, tekken_button4, tekken_button5, + card_tapping, count }; @@ -46,6 +47,7 @@ struct cfg_usio final : public emulated_pad_config cfg_pad_btn tekken_button3{this, "Tekken Button 3", usio_btn::tekken_button3, pad_button::cross}; cfg_pad_btn tekken_button4{this, "Tekken Button 4", usio_btn::tekken_button4, pad_button::circle}; cfg_pad_btn tekken_button5{this, "Tekken Button 5", usio_btn::tekken_button5, pad_button::R1}; + cfg_pad_btn card_tapping{this, "Card Tapping", usio_btn::card_tapping, pad_button::L1}; }; struct cfg_usios final : public emulated_pads_config From c0b358003f813e28d7902cd65251c3506847619a Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 10 May 2026 16:27:18 +0300 Subject: [PATCH 4/5] Add basic guidance on AI usage for contributors --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index ac6e3cd594..8ccf4f12ce 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,16 @@ If you want to contribute as a developer, please take a look at the following pa You should also contact any of the developers in the forums or in the Discord server to learn more about the current state of the emulator. +### AI Use + +Use of AI tools for research and reverse engineering purposes is permitted. However, contributors are expected to fully own and understand all code they submit. Any communication with the team — including code, code comments, and GitHub comments — must come from the human contributor, not an AI agent acting autonomously. + +We have unfortunately seen a rise in untested and unverified AI-generated slop being submitted to this project. This wastes maintainer time and, in worse cases, such changes get merged and break functionality for all users. Repeated violations will result in a ban from the repository. Please be respectful of everyone's time. + +**Pull requests opened by AI agents or automated tools must include a disclosure in the PR description** stating the scope of AI involvement — which parts were AI-generated and what human testing or review was performed prior to submission. PRs that omit this disclosure may be closed without review. + +If you are unsure about your work, open a discussion issue to talk it through with the team, or reach out to a maintainer on [Discord](https://discord.gg/RPCS3). + ## Building See [BUILDING.md](BUILDING.md) for more information about how to setup an environment to build RPCS3. From cd7cb1cc11fdcae484fc6b2d447be96e045fed1b Mon Sep 17 00:00:00 2001 From: Arsh Kumar Singh Date: Sun, 10 May 2026 20:55:00 +0530 Subject: [PATCH 5/5] Qt: include territory in language menu labels (#18704) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes the GUI language dropdown to show territory variants when translation files include country codes. **Before:** "Portuguese" for both `pt_BR` and `pt_PT` **After:** "Portuguese (Brazil)" and "Portuguese (Portugal)" Also fixes the same ambiguity for Chinese and any other language with country variants. ## Implementation Uses `QLocale::territoryToString()` alongside the existing `QLocale::languageToString()` to construct display labels. When no territory exists (e.g., `en`, `ja`), the label is unchanged. ## Edge cases - No-territory locales (`en`, `ja`): unchanged — `QLocale::territoryToString(AnyTerritory)` returns empty, guard preserves plain language name - Territory names are locale-translated (not hardcoded English) — they follow the current UI language, same as `languageToString()` - `zh_CN` → "Chinese (China)" / `zh_TW` → "Chinese (Taiwan)" (territory-based, unlike the PS3 system language dropdown which uses Simplified/Traditional script names — this is appropriate for the GUI translator selector context) ## Test Plan - [ ] Language menu shows "Portuguese (Brazil)" when `rpcs3_pt_BR.qm` is present - [ ] Language menu shows "Chinese (China)" when `rpcs3_zh_CN.qm` is present - [ ] Plain languages (`en`, `ja`) continue to show without parens ---
Review note: territory strings in UI locale Territory names render in the current UI language (not forced English). This matches `QLocale::languageToString()` behavior and is consistent with how the rest of the menu renders. If hardcoded English labels are preferred, that can be added after review.
Fixes #18215 --- rpcs3/rpcs3qt/main_window.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 97081aca4a..28f76a63bc 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -2298,11 +2298,19 @@ void main_window::UpdateLanguageActions(const QStringList& language_codes, const { const QLocale locale = QLocale(code); const QString locale_name = QLocale::languageToString(locale.language()); + const QString territory = QLocale::territoryToString(locale.territory()); + + const bool is_unique = std::count_if(language_codes.cbegin(), language_codes.cend(), [&locale_name](const QString& code) + { + return locale_name == QLocale::languageToString(QLocale(code).language()); + }) == 1; + + const QString display_name = (!is_unique && !territory.isEmpty()) ? QString("%1 (%2)").arg(locale_name, territory) : locale_name; // create new action - QAction* act = new QAction(locale_name, this); + QAction* act = new QAction(display_name, this); act->setData(code); - act->setToolTip(locale_name); + act->setToolTip(display_name); act->setCheckable(true); act->setChecked(code == language_code);