From f81a5a5cb1cd30678061bff31b2156090abe2a57 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sun, 21 Dec 2025 13:12:43 +0100 Subject: [PATCH 01/59] sceNpClans: cleanup --- rpcs3/Emu/Cell/Modules/sceNp.h | 2 +- rpcs3/Emu/Cell/Modules/sceNpClans.cpp | 160 +++---- rpcs3/Emu/NP/clans_client.cpp | 603 +++++++++++++------------- rpcs3/Emu/NP/clans_client.h | 61 ++- rpcs3/emucore.vcxproj | 2 + rpcs3/emucore.vcxproj.filters | 12 + rpcs3/rpcs3.vcxproj.filters | 6 +- 7 files changed, 420 insertions(+), 426 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/sceNp.h b/rpcs3/Emu/Cell/Modules/sceNp.h index 6f29a2f8a9..88dd2d816b 100644 --- a/rpcs3/Emu/Cell/Modules/sceNp.h +++ b/rpcs3/Emu/Cell/Modules/sceNp.h @@ -1265,7 +1265,7 @@ struct SceNpCommunicationId // OnlineId structure struct SceNpOnlineId { - char data[16 + 1]; // char term; + char data[SCE_NET_NP_ONLINEID_MAX_LENGTH + 1]; // char term; char dummy[3]; }; diff --git a/rpcs3/Emu/Cell/Modules/sceNpClans.cpp b/rpcs3/Emu/Cell/Modules/sceNpClans.cpp index 64ac967d8e..1a9491ae6e 100644 --- a/rpcs3/Emu/Cell/Modules/sceNpClans.cpp +++ b/rpcs3/Emu/Cell/Modules/sceNpClans.cpp @@ -144,9 +144,9 @@ error_code sceNpClansCreateRequest(vm::ptr handle, u64 } auto& clans_manager = g_fxo->get(); - + s32 reqId = 0; - SceNpClansError res = clans_manager.client->create_request(&reqId); + const SceNpClansError res = clans_manager.client->create_request(reqId); if (res != SCE_NP_CLANS_SUCCESS) { return res; @@ -167,8 +167,8 @@ error_code sceNpClansDestroyRequest(SceNpClansRequestHandle handle) } auto& clans_manager = g_fxo->get(); - - SceNpClansError res = clans_manager.client->destroy_request(handle); + + const SceNpClansError res = clans_manager.client->destroy_request(handle); if (res != SCE_NP_CLANS_SUCCESS) { return res; @@ -220,7 +220,7 @@ error_code sceNpClansCreateClan(SceNpClansRequestHandle handle, vm::cptr n std::string tag_str; vm::read_string(tag.addr(), SCE_NP_CLANS_CLAN_TAG_MAX_LENGTH, tag_str); - SceNpClansError res = clans_manager.client->create_clan(nph, handle, name_str, tag_str, clanId); + const SceNpClansError res = clans_manager.client->create_clan(nph, handle, name_str, tag_str, clanId); if (res != SCE_NP_CLANS_SUCCESS) { return res; @@ -246,7 +246,7 @@ error_code sceNpClansDisbandClan(SceNpClansRequestHandle handle, SceNpClanId cla auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpClansError res = clans_manager.client->disband_dlan(nph, handle, clanId); + const SceNpClansError res = clans_manager.client->disband_dlan(nph, handle, clanId); if (res != SCE_NP_CLANS_SUCCESS) { return res; @@ -283,13 +283,13 @@ error_code sceNpClansGetClanList(SceNpClansRequestHandle handle, vm::cptr host_clanList(SCE_NP_CLANS_PAGING_REQUEST_PAGE_MAX); SceNpClansPagingResult host_pageResult = {}; - SceNpClansError ret = clans_manager.client->get_clan_list(nph, handle, &host_paging, host_clanList, &host_pageResult); + const SceNpClansError ret = clans_manager.client->get_clan_list(nph, handle, host_paging, host_clanList, host_pageResult); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -297,9 +297,9 @@ error_code sceNpClansGetClanList(SceNpClansRequestHandle handle, vm::cptr 0) { - std::memcpy(clanList.get_ptr(), host_clanList, sizeof(SceNpClansEntry) * host_pageResult.count); + std::memcpy(clanList.get_ptr(), host_clanList.data(), sizeof(SceNpClansEntry) * host_pageResult.count); } - std::memcpy(pageResult.get_ptr(), &host_pageResult, sizeof(SceNpClansPagingResult)); + *pageResult = host_pageResult; return CELL_OK; } @@ -383,16 +383,15 @@ error_code sceNpClansSearchByName(SceNpClansRequestHandle handle, vm::cptr host_results(SCE_NP_CLANS_PAGING_REQUEST_PAGE_MAX); SceNpClansPagingResult host_pageResult = {}; - SceNpClansError ret = clans_manager.client->clan_search(handle, &host_paging, &host_search, host_results, &host_pageResult); + const SceNpClansError ret = clans_manager.client->clan_search(handle, host_paging, host_search, host_results, host_pageResult); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -400,9 +399,9 @@ error_code sceNpClansSearchByName(SceNpClansRequestHandle handle, vm::cptr 0) { - std::memcpy(results.get_ptr(), host_results, sizeof(SceNpClansClanBasicInfo) * host_pageResult.count); + std::memcpy(results.get_ptr(), host_results.data(), sizeof(SceNpClansClanBasicInfo) * host_pageResult.count); } - std::memcpy(pageResult.get_ptr(), &host_pageResult, sizeof(SceNpClansPagingResult)); + *pageResult = host_pageResult; return CELL_OK; } @@ -425,14 +424,14 @@ error_code sceNpClansGetClanInfo(SceNpClansRequestHandle handle, SceNpClanId cla auto& clans_manager = g_fxo->get(); SceNpClansClanInfo host_info = {}; - - SceNpClansError ret = clans_manager.client->get_clan_info(handle, clanId, &host_info); + + const SceNpClansError ret = clans_manager.client->get_clan_info(handle, clanId, host_info); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; } - - std::memcpy(info.get_ptr(), &host_info, sizeof(SceNpClansClanInfo)); + + *info = host_info; return CELL_OK; } @@ -455,10 +454,9 @@ error_code sceNpClansUpdateClanInfo(SceNpClansRequestHandle handle, SceNpClanId auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpClansUpdatableClanInfo host_info = {}; - std::memcpy(&host_info, info.get_ptr(), sizeof(SceNpClansUpdatableClanInfo)); + const SceNpClansUpdatableClanInfo host_info = *info; - SceNpClansError ret = clans_manager.client->update_clan_info(nph, handle, clanId, &host_info); + const SceNpClansError ret = clans_manager.client->update_clan_info(nph, handle, clanId, host_info); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -495,13 +493,13 @@ error_code sceNpClansGetMemberList(SceNpClansRequestHandle handle, SceNpClanId c SceNpClansPagingRequest host_paging = {}; if (paging) { - std::memcpy(&host_paging, paging.get_ptr(), sizeof(SceNpClansPagingRequest)); + host_paging = *paging; } - SceNpClansMemberEntry host_memList_addr[SCE_NP_CLANS_PAGING_REQUEST_PAGE_MAX] = {}; + std::vector host_memList_addr(SCE_NP_CLANS_PAGING_REQUEST_PAGE_MAX); SceNpClansPagingResult host_pageResult = {}; - SceNpClansError ret = clans_manager.client->get_member_list(nph, handle, clanId, &host_paging, status, host_memList_addr, &host_pageResult); + const SceNpClansError ret = clans_manager.client->get_member_list(nph, handle, clanId, host_paging, status, host_memList_addr, host_pageResult); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -509,9 +507,9 @@ error_code sceNpClansGetMemberList(SceNpClansRequestHandle handle, SceNpClanId c if (memList && host_pageResult.count > 0) { - std::memcpy(memList.get_ptr(), host_memList_addr, sizeof(SceNpClansMemberEntry) * host_pageResult.count); + std::memcpy(memList.get_ptr(), host_memList_addr.data(), sizeof(SceNpClansMemberEntry) * host_pageResult.count); } - std::memcpy(pageResult.get_ptr(), &host_pageResult, sizeof(SceNpClansPagingResult)); + *pageResult = host_pageResult; return CELL_OK; } @@ -533,18 +531,17 @@ error_code sceNpClansGetMemberInfo(SceNpClansRequestHandle handle, SceNpClanId c auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpId host_npid = {}; - std::memcpy(&host_npid, npid.get_ptr(), sizeof(SceNpId)); + const SceNpId host_npid = *npid; SceNpClansMemberEntry host_memInfo = {}; - SceNpClansError ret = clans_manager.client->get_member_info(nph, handle, clanId, host_npid, &host_memInfo); + const SceNpClansError ret = clans_manager.client->get_member_info(nph, handle, clanId, host_npid, host_memInfo); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; } - std::memcpy(memInfo.get_ptr(), &host_memInfo, sizeof(SceNpClansMemberEntry)); + *memInfo = host_memInfo; return CELL_OK; } @@ -566,10 +563,9 @@ error_code sceNpClansUpdateMemberInfo(SceNpClansRequestHandle handle, SceNpClanI auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpClansUpdatableMemberInfo host_info = {}; - std::memcpy(&host_info, info.get_ptr(), sizeof(SceNpClansUpdatableMemberInfo)); + const SceNpClansUpdatableMemberInfo host_info = *info; - SceNpClansError ret = clans_manager.client->update_member_info(nph, handle, clanId, &host_info); + const SceNpClansError ret = clans_manager.client->update_member_info(nph, handle, clanId, host_info); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -595,10 +591,9 @@ error_code sceNpClansChangeMemberRole(SceNpClansRequestHandle handle, SceNpClanI auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpId host_npid = {}; - std::memcpy(&host_npid, npid.get_ptr(), sizeof(SceNpId)); + const SceNpId host_npid = *npid; - SceNpClansError ret = clans_manager.client->change_member_role(nph, handle, clanId, host_npid, static_cast(role)); + const SceNpClansError ret = clans_manager.client->change_member_role(nph, handle, clanId, host_npid, static_cast(role)); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -650,7 +645,7 @@ error_code sceNpClansJoinClan(SceNpClansRequestHandle handle, SceNpClanId clanId auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpClansError ret = clans_manager.client->join_clan(nph, handle, clanId); + const SceNpClansError ret = clans_manager.client->join_clan(nph, handle, clanId); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -671,7 +666,7 @@ error_code sceNpClansLeaveClan(SceNpClansRequestHandle handle, SceNpClanId clanI auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpClansError ret = clans_manager.client->leave_clan(nph, handle, clanId); + const SceNpClansError ret = clans_manager.client->leave_clan(nph, handle, clanId); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -705,16 +700,15 @@ error_code sceNpClansKickMember(SceNpClansRequestHandle handle, SceNpClanId clan auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpId host_npid = {}; - std::memcpy(&host_npid, npid.get_ptr(), sizeof(SceNpId)); + const SceNpId host_npid = *npid; SceNpClansMessage host_message = {}; if (message) { - std::memcpy(&host_message, message.get_ptr(), sizeof(SceNpClansMessage)); + host_message = *message; } - SceNpClansError ret = clans_manager.client->kick_member(nph, handle, clanId, host_npid, &host_message); + const SceNpClansError ret = clans_manager.client->kick_member(nph, handle, clanId, host_npid, host_message); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -748,16 +742,15 @@ error_code sceNpClansSendInvitation(SceNpClansRequestHandle handle, SceNpClanId auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpId host_npid = {}; - std::memcpy(&host_npid, npid.get_ptr(), sizeof(SceNpId)); + const SceNpId host_npid = *npid; SceNpClansMessage host_message = {}; if (message) { - std::memcpy(&host_message, message.get_ptr(), sizeof(SceNpClansMessage)); + host_message = *message; } - SceNpClansError ret = clans_manager.client->send_invitation(nph, handle, clanId, host_npid, &host_message); + const SceNpClansError ret = clans_manager.client->send_invitation(nph, handle, clanId, host_npid, host_message); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -783,10 +776,9 @@ error_code sceNpClansCancelInvitation(SceNpClansRequestHandle handle, SceNpClanI auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpId host_npid = {}; - std::memcpy(&host_npid, npid.get_ptr(), sizeof(SceNpId)); + const SceNpId host_npid = *npid; - SceNpClansError ret = clans_manager.client->cancel_invitation(nph, handle, clanId, host_npid); + const SceNpClansError ret = clans_manager.client->cancel_invitation(nph, handle, clanId, host_npid); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -818,15 +810,10 @@ error_code sceNpClansSendInvitationResponse(SceNpClansRequestHandle handle, SceN SceNpClansMessage host_message = {}; if (message) { - std::memcpy(&host_message, message.get_ptr(), sizeof(SceNpClansMessage)); + host_message = *message; } - if (message) - { - std::memcpy(&host_message, message.get_ptr(), sizeof(SceNpClansMessage)); - } - - SceNpClansError ret = clans_manager.client->send_invitation_response(nph, handle, clanId, &host_message, accept); + const SceNpClansError ret = clans_manager.client->send_invitation_response(nph, handle, clanId, host_message, accept); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -858,10 +845,10 @@ error_code sceNpClansSendMembershipRequest(SceNpClansRequestHandle handle, u32 c SceNpClansMessage host_message = {}; if (message) { - std::memcpy(&host_message, message.get_ptr(), sizeof(SceNpClansMessage)); + host_message = *message; } - SceNpClansError ret = clans_manager.client->request_membership(nph, handle, clanId, &host_message); + const SceNpClansError ret = clans_manager.client->request_membership(nph, handle, clanId, host_message); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -882,8 +869,7 @@ error_code sceNpClansCancelMembershipRequest(SceNpClansRequestHandle handle, Sce auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpClansError ret = clans_manager.client->cancel_request_membership(nph, handle, clanId); - + const SceNpClansError ret = clans_manager.client->cancel_request_membership(nph, handle, clanId); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -917,16 +903,15 @@ error_code sceNpClansSendMembershipResponse(SceNpClansRequestHandle handle, SceN auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpId host_npid = {}; - std::memcpy(&host_npid, npid.get_ptr(), sizeof(SceNpId)); + const SceNpId host_npid = *npid; SceNpClansMessage host_message = {}; if (message) { - std::memcpy(&host_message, message.get_ptr(), sizeof(SceNpClansMessage)); + host_message = *message; } - SceNpClansError ret = clans_manager.client->send_membership_response(nph, handle, clanId, host_npid, &host_message, allow); + const SceNpClansError ret = clans_manager.client->send_membership_response(nph, handle, clanId, host_npid, host_message, allow); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -963,13 +948,13 @@ error_code sceNpClansGetBlacklist(SceNpClansRequestHandle handle, SceNpClanId cl SceNpClansPagingRequest host_paging = {}; if (paging) { - std::memcpy(&host_paging, paging.get_ptr(), sizeof(SceNpClansPagingRequest)); + host_paging = *paging; } - SceNpClansBlacklistEntry host_blacklist[SCE_NP_CLANS_PAGING_REQUEST_PAGE_MAX] = {}; + std::vector host_blacklist(SCE_NP_CLANS_PAGING_REQUEST_PAGE_MAX); SceNpClansPagingResult host_pageResult = {}; - SceNpClansError ret = clans_manager.client->get_blacklist(nph, handle, clanId, &host_paging, host_blacklist, &host_pageResult); + const SceNpClansError ret = clans_manager.client->get_blacklist(nph, handle, clanId, host_paging, host_blacklist, host_pageResult); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -977,9 +962,9 @@ error_code sceNpClansGetBlacklist(SceNpClansRequestHandle handle, SceNpClanId cl if (bl && host_pageResult.count > 0) { - std::memcpy(bl.get_ptr(), host_blacklist, sizeof(SceNpClansBlacklistEntry) * host_pageResult.count); + std::memcpy(bl.get_ptr(), host_blacklist.data(), sizeof(SceNpClansBlacklistEntry) * host_pageResult.count); } - std::memcpy(pageResult.get_ptr(), &host_pageResult, sizeof(SceNpClansPagingResult)); + *pageResult = host_pageResult; return CELL_OK; } @@ -1001,10 +986,9 @@ error_code sceNpClansAddBlacklistEntry(SceNpClansRequestHandle handle, SceNpClan auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpId host_member = {}; - std::memcpy(&host_member, member.get_ptr(), sizeof(SceNpId)); + const SceNpId host_member = *member; - SceNpClansError ret = clans_manager.client->add_blacklist_entry(nph, handle, clanId, host_member); + const SceNpClansError ret = clans_manager.client->add_blacklist_entry(nph, handle, clanId, host_member); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -1030,10 +1014,9 @@ error_code sceNpClansRemoveBlacklistEntry(SceNpClansRequestHandle handle, SceNpC auto& nph = g_fxo->get>(); auto& clans_manager = g_fxo->get(); - SceNpId host_member = {}; - std::memcpy(&host_member, member.get_ptr(), sizeof(SceNpId)); + const SceNpId host_member = *member; - SceNpClansError ret = clans_manager.client->remove_blacklist_entry(nph, handle, clanId, host_member); + const SceNpClansError ret = clans_manager.client->remove_blacklist_entry(nph, handle, clanId, host_member); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -1070,13 +1053,13 @@ error_code sceNpClansRetrieveAnnouncements(SceNpClansRequestHandle handle, SceNp SceNpClansPagingRequest host_paging = {}; if (paging) { - std::memcpy(&host_paging, paging.get_ptr(), sizeof(SceNpClansPagingRequest)); + host_paging = *paging; } - SceNpClansMessageEntry host_announcements[SCE_NP_CLANS_PAGING_REQUEST_PAGE_MAX] = {}; + std::vector host_announcements(SCE_NP_CLANS_PAGING_REQUEST_PAGE_MAX); SceNpClansPagingResult host_pageResult = {}; - SceNpClansError ret = clans_manager.client->retrieve_announcements(nph, handle, clanId, &host_paging, host_announcements, &host_pageResult); + const SceNpClansError ret = clans_manager.client->retrieve_announcements(nph, handle, clanId, host_paging, host_announcements, host_pageResult); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -1084,9 +1067,9 @@ error_code sceNpClansRetrieveAnnouncements(SceNpClansRequestHandle handle, SceNp if (mlist && host_pageResult.count > 0) { - std::memcpy(mlist.get_ptr(), host_announcements, sizeof(SceNpClansMessageEntry) * host_pageResult.count); + std::memcpy(mlist.get_ptr(), host_announcements.data(), sizeof(SceNpClansMessageEntry) * host_pageResult.count); } - std::memcpy(pageResult.get_ptr(), &host_pageResult, sizeof(SceNpClansPagingResult)); + *pageResult = host_pageResult; return CELL_OK; } @@ -1113,17 +1096,16 @@ error_code sceNpClansPostAnnouncement(SceNpClansRequestHandle handle, SceNpClanI auto& clans_manager = g_fxo->get(); auto& nph = g_fxo->get>(); - SceNpClansMessage host_announcement = {}; - std::memcpy(&host_announcement, message.get_ptr(), sizeof(SceNpClansMessage)); + const SceNpClansMessage host_announcement = *message; SceNpClansMessageData host_data = {}; if (data) { - std::memcpy(&host_data, data.get_ptr(), sizeof(SceNpClansMessageData)); + host_data = *data; } SceNpClansMessageId host_announcementId = 0; - SceNpClansError ret = clans_manager.client->post_announcement(nph, handle, clanId, &host_announcement, &host_data, duration, &host_announcementId); + const SceNpClansError ret = clans_manager.client->post_announcement(nph, handle, clanId, host_announcement, host_data, duration, host_announcementId); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; @@ -1146,7 +1128,7 @@ error_code sceNpClansRemoveAnnouncement(SceNpClansRequestHandle handle, SceNpCla auto& clans_manager = g_fxo->get(); auto& nph = g_fxo->get>(); - SceNpClansError ret = clans_manager.client->delete_announcement(nph, handle, clanId, mId); + const SceNpClansError ret = clans_manager.client->delete_announcement(nph, handle, clanId, mId); if (ret != SCE_NP_CLANS_SUCCESS) { return ret; diff --git a/rpcs3/Emu/NP/clans_client.cpp b/rpcs3/Emu/NP/clans_client.cpp index 7883901476..da3f506c91 100644 --- a/rpcs3/Emu/NP/clans_client.cpp +++ b/rpcs3/Emu/NP/clans_client.cpp @@ -128,18 +128,18 @@ void fmt_class_string::format(std::string& out, u64 arg namespace clan { - struct curl_memory - { + struct curl_memory + { char* response; size_t size; - }; + }; size_t clans_client::curl_write_callback(void* data, size_t size, size_t nmemb, void* clientp) { - size_t realsize = size * nmemb; + const size_t realsize = size * nmemb; std::vector* mem = static_cast*>(clientp); - size_t old_size = mem->size(); + const size_t old_size = mem->size(); mem->resize(old_size + realsize); memcpy(mem->data() + old_size, data, realsize); @@ -169,11 +169,11 @@ namespace clan CURL* curl = nullptr; // TODO: this was arbitrarily chosen -- see if there's a real amount - static const u32 SCE_NP_CLANS_MAX_CTX_NUM = 16; - - static const u32 id_base = 0xA001; - static const u32 id_step = 1; - static const u32 id_count = SCE_NP_CLANS_MAX_CTX_NUM; + static constexpr u32 SCE_NP_CLANS_MAX_CTX_NUM = 16; + + static constexpr u32 id_base = 0xA001; + static constexpr u32 id_step = 1; + static constexpr u32 id_count = SCE_NP_CLANS_MAX_CTX_NUM; SAVESTATE_INIT_POS(55); }; @@ -187,7 +187,7 @@ namespace clan idm::clear(); } - SceNpClansError clans_client::create_request(s32* req_id) + SceNpClansError clans_client::create_request(s32& req_id) { if (!g_cfg.net.clans_enabled) { @@ -201,14 +201,14 @@ namespace clan return SceNpClansError::SCE_NP_CLANS_ERROR_EXCEEDS_MAX; } - auto ctx = idm::get_unlocked(id); + const auto ctx = idm::get_unlocked(id); if (!ctx || !ctx->curl) { idm::remove(id); return SceNpClansError::SCE_NP_CLANS_ERROR_NOT_INITIALIZED; } - *req_id = id; + req_id = id; return SceNpClansError::SCE_NP_CLANS_SUCCESS; } @@ -221,7 +221,7 @@ namespace clan return SceNpClansError::SCE_NP_CLANS_ERROR_INVALID_ARGUMENT; } - SceNpClansError clans_client::send_request(u32 req_id, ClanRequestAction action, ClanManagerOperationType op_type, pugi::xml_document* xml_body, pugi::xml_document* out_response) + SceNpClansError clans_client::send_request(u32 req_id, ClanRequestAction action, ClanManagerOperationType op_type, const pugi::xml_document& xml_body, pugi::xml_document& out_response) { auto ctx = idm::get_unlocked(req_id); @@ -231,20 +231,20 @@ namespace clan CURL* curl = ctx->curl; ClanRequestType req_type = ClanRequestType::FUNC; - pugi::xml_node clan = xml_body->child("clan"); + const pugi::xml_node clan = xml_body.child("clan"); if (clan && clan.child("ticket")) { req_type = ClanRequestType::SEC; } - std::string host = g_cfg_clans.get_host(); - std::string protocol = g_cfg_clans.get_use_https() ? "https" : "http"; - std::string url = fmt::format("%s://%s/clan_manager_%s/%s/%s", protocol, host, op_type, req_type, action); + const std::string host = g_cfg_clans.get_host(); + const std::string protocol = g_cfg_clans.get_use_https() ? "https" : "http"; + const std::string url = fmt::format("%s://%s/clan_manager_%s/%s/%s", protocol, host, op_type, req_type, action); std::ostringstream oss; - xml_body->save(oss, "\t", 8U); + xml_body.save(oss, "\t", 8U); - std::string xml = oss.str(); + const std::string xml = oss.str(); char err_buf[CURL_ERROR_SIZE]; err_buf[0] = '\0'; @@ -264,11 +264,10 @@ namespace clan curl_easy_setopt(curl, CURLOPT_POSTFIELDS, xml.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, xml.size()); - CURLcode res = curl_easy_perform(curl); + const CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { - out_response = nullptr; clan_log.error("curl_easy_perform() failed: %s", curl_easy_strerror(res)); clan_log.error("Error buffer: %s", err_buf); return SCE_NP_CLANS_ERROR_BAD_REQUEST; @@ -276,22 +275,22 @@ namespace clan response_buffer.push_back('\0'); - pugi::xml_parse_result xml_res = out_response->load_string(response_buffer.data()); + const pugi::xml_parse_result xml_res = out_response.load_string(response_buffer.data()); if (!xml_res) { clan_log.error("XML parsing failed: %s", xml_res.description()); return SCE_NP_CLANS_ERROR_BAD_RESPONSE; } - pugi::xml_node clan_result = out_response->child("clan"); + const pugi::xml_node clan_result = out_response.child("clan"); if (!clan_result) return SCE_NP_CLANS_ERROR_BAD_RESPONSE; - pugi::xml_attribute result = clan_result.attribute("result"); + const pugi::xml_attribute result = clan_result.attribute("result"); if (!result) return SCE_NP_CLANS_ERROR_BAD_RESPONSE; - std::string result_str = result.as_string(); + const std::string result_str = result.as_string(); if (result_str != "00") return static_cast(0x80022800 | std::stoul(result_str, nullptr, 16)); @@ -331,16 +330,16 @@ namespace clan std::vector ticket_bytes(1024); uint32_t ticket_size = UINT32_MAX; - Base64_Encode_NoNl(clan_ticket.data(), clan_ticket.size(), ticket_bytes.data(), &ticket_size); - std::string ticket_str = std::string(reinterpret_cast(ticket_bytes.data()), ticket_size); + Base64_Encode_NoNl(clan_ticket.data(), static_cast(clan_ticket.size()), ticket_bytes.data(), &ticket_size); + const std::string ticket_str = std::string(reinterpret_cast(ticket_bytes.data()), ticket_size); return ticket_str; } #pragma region Outgoing API Requests - SceNpClansError clans_client::get_clan_list(np::np_handler& nph, u32 req_id, SceNpClansPagingRequest* paging, SceNpClansEntry* clan_list, SceNpClansPagingResult* page_result) + SceNpClansError clans_client::get_clan_list(np::np_handler& nph, u32 req_id, const SceNpClansPagingRequest& paging, std::vector& clan_list, SceNpClansPagingResult& page_result) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -348,47 +347,47 @@ namespace clan pugi::xml_node clan = doc.append_child("clan"); clan.append_child("ticket").text().set(ticket.c_str()); - clan.append_child("start").text().set(paging->startPos); - clan.append_child("max").text().set(paging->max); + clan.append_child("start").text().set(paging.startPos); + clan.append_child("max").text().set(paging.max); pugi::xml_document response = pugi::xml_document(); - SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetClanList, ClanManagerOperationType::VIEW, &doc, &response); + const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetClanList, ClanManagerOperationType::VIEW, doc, response); if (clan_res != SCE_NP_CLANS_SUCCESS) return clan_res; - pugi::xml_node clan_result = response.child("clan"); - pugi::xml_node list = clan_result.child("list"); + const pugi::xml_node clan_result = response.child("clan"); + const pugi::xml_node list = clan_result.child("list"); - pugi::xml_attribute results = list.attribute("results"); - uint32_t results_count = results.as_uint(); + const pugi::xml_attribute results = list.attribute("results"); + const uint32_t results_count = results.as_uint(); - pugi::xml_attribute total = list.attribute("total"); - uint32_t total_count = total.as_uint(); + const pugi::xml_attribute total = list.attribute("total"); + const uint32_t total_count = total.as_uint(); int i = 0; for (pugi::xml_node info = list.child("info"); info; info = info.next_sibling("info"), i++) { - pugi::xml_attribute id = info.attribute("id"); - uint32_t clan_id = id.as_uint(); + const pugi::xml_attribute id = info.attribute("id"); + const uint32_t clan_id = id.as_uint(); - pugi::xml_node name = info.child("name"); - std::string name_str = name.text().as_string(); + const pugi::xml_node name = info.child("name"); + const std::string name_str = name.text().as_string(); - pugi::xml_node tag = info.child("tag"); - std::string tag_str = tag.text().as_string(); + const pugi::xml_node tag = info.child("tag"); + const std::string tag_str = tag.text().as_string(); - pugi::xml_node role = info.child("role"); - int32_t role_int = role.text().as_uint(); + const pugi::xml_node role = info.child("role"); + const int32_t role_int = role.text().as_uint(); - pugi::xml_node status = info.child("status"); - uint32_t status_int = status.text().as_uint(); + const pugi::xml_node status = info.child("status"); + const uint32_t status_int = status.text().as_uint(); - pugi::xml_node onlinename = info.child("onlinename"); - std::string onlinename_str = onlinename.text().as_string(); + const pugi::xml_node onlinename = info.child("onlinename"); + const std::string onlinename_str = onlinename.text().as_string(); - pugi::xml_node members = info.child("members"); - uint32_t members_int = members.text().as_uint(); + const pugi::xml_node members = info.child("members"); + const uint32_t members_int = members.text().as_uint(); SceNpClansEntry entry = SceNpClansEntry{ .info = SceNpClansClanBasicInfo{ @@ -402,41 +401,41 @@ namespace clan .status = static_cast(status_int)}; strcpy_trunc(entry.info.name, name_str); - strcpy_trunc(entry.info.tag, tag_str); + strcpy_trunc(entry.info.tag, tag_str); - clan_list[i] = entry; + ::at32(clan_list, i) = std::move(entry); i++; } - *page_result = SceNpClansPagingResult{ + page_result = SceNpClansPagingResult{ .count = results_count, .total = total_count}; return SCE_NP_CLANS_SUCCESS; } - SceNpClansError clans_client::get_clan_info(u32 req_id, SceNpClanId clan_id, SceNpClansClanInfo* clan_info) + SceNpClansError clans_client::get_clan_info(u32 req_id, SceNpClanId clan_id, SceNpClansClanInfo& clan_info) { pugi::xml_document doc = pugi::xml_document(); pugi::xml_node clan = doc.append_child("clan"); clan.append_child("id").text().set(clan_id); pugi::xml_document response = pugi::xml_document(); - SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetClanInfo, ClanManagerOperationType::VIEW, &doc, &response); + const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetClanInfo, ClanManagerOperationType::VIEW, doc, response); if (clan_res != SCE_NP_CLANS_SUCCESS) return clan_res; - pugi::xml_node clan_result = response.child("clan"); - pugi::xml_node info = clan_result.child("info"); + const pugi::xml_node clan_result = response.child("clan"); + const pugi::xml_node info = clan_result.child("info"); - std::string name_str = info.child("name").text().as_string(); - std::string tag_str = info.child("tag").text().as_string(); - uint32_t members_int = info.child("members").text().as_uint(); - std::string date_created_str = info.child("date-created").text().as_string(); - std::string description_str = info.child("description").text().as_string(); + const std::string name_str = info.child("name").text().as_string(); + const std::string tag_str = info.child("tag").text().as_string(); + const uint32_t members_int = info.child("members").text().as_uint(); + const std::string date_created_str = info.child("date-created").text().as_string(); + const std::string description_str = info.child("description").text().as_string(); - *clan_info = SceNpClansClanInfo{ + clan_info = SceNpClansClanInfo{ .info = SceNpClansClanBasicInfo{ .clanId = clan_id, .numMembers = members_int, @@ -447,16 +446,16 @@ namespace clan .description = "", }}; - strcpy_trunc(clan_info->info.name, name_str); - strcpy_trunc(clan_info->info.tag, tag_str); - strcpy_trunc(clan_info->updatable.description, description_str); + strcpy_trunc(clan_info.info.name, name_str); + strcpy_trunc(clan_info.info.tag, tag_str); + strcpy_trunc(clan_info.updatable.description, description_str); return SCE_NP_CLANS_SUCCESS; } - SceNpClansError clans_client::get_member_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id, SceNpClansMemberEntry* mem_info) + SceNpClansError clans_client::get_member_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMemberEntry& mem_info) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -464,22 +463,22 @@ namespace clan pugi::xml_node clan = doc.append_child("clan"); clan.append_child("ticket").text().set(ticket.c_str()); clan.append_child("id").text().set(clan_id); - - std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); - clan.append_child("jid").text().set(jid_str.c_str()); + + const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); + clan.append_child("jid").text().set(jid_str.c_str()); pugi::xml_document response = pugi::xml_document(); - SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetMemberInfo, ClanManagerOperationType::VIEW, &doc, &response); + const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetMemberInfo, ClanManagerOperationType::VIEW, doc, response); if (clan_res != SCE_NP_CLANS_SUCCESS) return clan_res; - pugi::xml_node clan_result = response.child("clan"); - pugi::xml_node member_info = clan_result.child("info"); + const pugi::xml_node clan_result = response.child("clan"); + const pugi::xml_node member_info = clan_result.child("info"); - pugi::xml_attribute member_jid = member_info.attribute("jid"); - std::string member_jid_str = member_jid.as_string(); - std::string member_username = fmt::split(member_jid_str, {"@"})[0]; + const pugi::xml_attribute member_jid = member_info.attribute("jid"); + const std::string member_jid_str = member_jid.as_string(); + const std::string member_username = fmt::split(member_jid_str, {"@"})[0]; SceNpId member_npid; if (strncmp(member_username.c_str(), nph.get_npid().handle.data, 16) == 0) @@ -491,18 +490,18 @@ namespace clan np::string_to_npid(member_username, member_npid); } - pugi::xml_node role = member_info.child("role"); - uint32_t role_int = role.text().as_uint(); + const pugi::xml_node role = member_info.child("role"); + const uint32_t role_int = role.text().as_uint(); - pugi::xml_node status = member_info.child("status"); - uint32_t status_int = status.text().as_uint(); + const pugi::xml_node status = member_info.child("status"); + const uint32_t status_int = status.text().as_uint(); - pugi::xml_node description = member_info.child("description"); - std::string description_str = description.text().as_string(); + const pugi::xml_node description = member_info.child("description"); + const std::string description_str = description.text().as_string(); - *mem_info = SceNpClansMemberEntry + mem_info = SceNpClansMemberEntry { - .npid = member_npid, + .npid = std::move(member_npid), .role = static_cast(role_int), .status = static_cast(status_int), .updatable = SceNpClansUpdatableMemberInfo{ @@ -510,47 +509,47 @@ namespace clan } }; - strcpy_trunc(mem_info->updatable.description, description_str); + strcpy_trunc(mem_info.updatable.description, description_str); return SCE_NP_CLANS_SUCCESS; } - SceNpClansError clans_client::get_member_list(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansPagingRequest* paging, SceNpClansMemberStatus /*status*/, SceNpClansMemberEntry* mem_list, SceNpClansPagingResult* page_result) - { - std::string ticket = get_clan_ticket(nph); + SceNpClansError clans_client::get_member_list(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansPagingRequest& paging, SceNpClansMemberStatus /*status*/, std::vector& mem_list, SceNpClansPagingResult& page_result) + { + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; - pugi::xml_document doc = pugi::xml_document(); + pugi::xml_document doc = pugi::xml_document(); pugi::xml_node clan = doc.append_child("clan"); clan.append_child("ticket").text().set(ticket.c_str()); clan.append_child("id").text().set(clan_id); - clan.append_child("start").text().set(paging->startPos); - clan.append_child("max").text().set(paging->max); + clan.append_child("start").text().set(paging.startPos); + clan.append_child("max").text().set(paging.max); pugi::xml_document response = pugi::xml_document(); - SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetMemberList, ClanManagerOperationType::VIEW, &doc, &response); + const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetMemberList, ClanManagerOperationType::VIEW, doc, response); if (clan_res != SCE_NP_CLANS_SUCCESS) return clan_res; - pugi::xml_node clan_result = response.child("clan"); - pugi::xml_node list = clan_result.child("list"); + const pugi::xml_node clan_result = response.child("clan"); + const pugi::xml_node list = clan_result.child("list"); - pugi::xml_attribute results = list.attribute("results"); - uint32_t results_count = results.as_uint(); + const pugi::xml_attribute results = list.attribute("results"); + const uint32_t results_count = results.as_uint(); - pugi::xml_attribute total = list.attribute("total"); - uint32_t total_count = total.as_uint(); - - int i = 0; - for (pugi::xml_node member_info = list.child("info"); member_info; member_info = member_info.next_sibling("info")) - { - std::string member_jid = member_info.attribute("jid").as_string(); - std::string member_username = fmt::split(member_jid, {"@"})[0]; + const pugi::xml_attribute total = list.attribute("total"); + const uint32_t total_count = total.as_uint(); + + int i = 0; + for (pugi::xml_node member_info = list.child("info"); member_info; member_info = member_info.next_sibling("info")) + { + const std::string member_jid = member_info.attribute("jid").as_string(); + const std::string member_username = fmt::split(member_jid, {"@"})[0]; SceNpId member_npid; - if (strncmp(member_username.c_str(), nph.get_npid().handle.data, 16) == 0) + if (strncmp(member_username.c_str(), nph.get_npid().handle.data, SCE_NET_NP_ONLINEID_MAX_LENGTH) == 0) { member_npid = nph.get_npid(); } @@ -559,67 +558,67 @@ namespace clan np::string_to_npid(member_username, member_npid); } - uint32_t role_int = member_info.child("role").text().as_uint(); - uint32_t status_int = member_info.child("status").text().as_uint(); - std::string description_str = member_info.child("description").text().as_string(); + const uint32_t role_int = member_info.child("role").text().as_uint(); + const uint32_t status_int = member_info.child("status").text().as_uint(); + const std::string description_str = member_info.child("description").text().as_string(); - SceNpClansMemberEntry entry = SceNpClansMemberEntry - { - .npid = member_npid, - .role = static_cast(role_int), - .status = static_cast(status_int), - }; + SceNpClansMemberEntry entry = SceNpClansMemberEntry + { + .npid = std::move(member_npid), + .role = static_cast(role_int), + .status = static_cast(status_int), + }; strcpy_trunc(entry.updatable.description, description_str); - - mem_list[i] = entry; - i++; - } - *page_result = SceNpClansPagingResult - { + ::at32(mem_list, i) = std::move(entry); + i++; + } + + page_result = SceNpClansPagingResult + { .count = results_count, .total = total_count - }; + }; return SCE_NP_CLANS_SUCCESS; } - SceNpClansError clans_client::get_blacklist(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansPagingRequest* paging, SceNpClansBlacklistEntry* bl, SceNpClansPagingResult* page_result) - { - std::string ticket = get_clan_ticket(nph); + SceNpClansError clans_client::get_blacklist(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansPagingRequest& paging, std::vector& bl, SceNpClansPagingResult& page_result) + { + const std::string ticket = get_clan_ticket(nph); - pugi::xml_document doc = pugi::xml_document(); - pugi::xml_node clan = doc.append_child("clan"); - clan.append_child("ticket").text().set(ticket.c_str()); - clan.append_child("id").text().set(clan_id); - clan.append_child("start").text().set(paging->startPos); - clan.append_child("max").text().set(paging->max); + pugi::xml_document doc = pugi::xml_document(); + pugi::xml_node clan = doc.append_child("clan"); + clan.append_child("ticket").text().set(ticket.c_str()); + clan.append_child("id").text().set(clan_id); + clan.append_child("start").text().set(paging.startPos); + clan.append_child("max").text().set(paging.max); - pugi::xml_document response = pugi::xml_document(); - SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetBlacklist, ClanManagerOperationType::VIEW, &doc, &response); + pugi::xml_document response = pugi::xml_document(); + const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::GetBlacklist, ClanManagerOperationType::VIEW, doc, response); - if (clan_res != SCE_NP_CLANS_SUCCESS) - return clan_res; + if (clan_res != SCE_NP_CLANS_SUCCESS) + return clan_res; - pugi::xml_node clan_result = response.child("clan"); - pugi::xml_node list = clan_result.child("list"); + const pugi::xml_node clan_result = response.child("clan"); + const pugi::xml_node list = clan_result.child("list"); - pugi::xml_attribute results = list.attribute("results"); - uint32_t results_count = results.as_uint(); + const pugi::xml_attribute results = list.attribute("results"); + const uint32_t results_count = results.as_uint(); - pugi::xml_attribute total = list.attribute("total"); - uint32_t total_count = total.as_uint(); + const pugi::xml_attribute total = list.attribute("total"); + const uint32_t total_count = total.as_uint(); - int i = 0; - for (pugi::xml_node member = list.child("entry"); member; member = member.next_sibling("entry")) - { - pugi::xml_node member_jid = member.child("jid"); - std::string member_jid_str = member_jid.text().as_string(); - std::string member_username = fmt::split(member_jid_str, {"@"})[0]; + int i = 0; + for (pugi::xml_node member = list.child("entry"); member; member = member.next_sibling("entry")) + { + const pugi::xml_node member_jid = member.child("jid"); + const std::string member_jid_str = member_jid.text().as_string(); + const std::string member_username = fmt::split(member_jid_str, {"@"})[0]; SceNpId member_npid = {}; - if (strncmp(member_username.c_str(), nph.get_npid().handle.data, 16) == 0) + if (strncmp(member_username.c_str(), nph.get_npid().handle.data, SCE_NET_NP_ONLINEID_MAX_LENGTH) == 0) { member_npid = nph.get_npid(); } @@ -628,27 +627,27 @@ namespace clan np::string_to_npid(member_username.c_str(), member_npid); } - SceNpClansBlacklistEntry entry = SceNpClansBlacklistEntry - { - .entry = member_npid, - }; + SceNpClansBlacklistEntry entry = SceNpClansBlacklistEntry + { + .entry = std::move(member_npid), + }; - bl[i] = entry; - i++; - } + ::at32(bl, i) = std::move(entry); + i++; + } - *page_result = SceNpClansPagingResult - { - .count = results_count, - .total = total_count - }; + page_result = SceNpClansPagingResult + { + .count = results_count, + .total = total_count + }; - return SCE_NP_CLANS_SUCCESS; - } + return SCE_NP_CLANS_SUCCESS; + } - SceNpClansError clans_client::add_blacklist_entry(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id) + SceNpClansError clans_client::add_blacklist_entry(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -657,14 +656,14 @@ namespace clan clan.append_child("ticket").text().set(ticket.c_str()); clan.append_child("id").text().set(clan_id); - std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); + const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); clan.append_child("jid").text().set(jid_str.c_str()); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::RecordBlacklistEntry, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::RecordBlacklistEntry, ClanManagerOperationType::UPDATE, doc, response); } - SceNpClansError clans_client::remove_blacklist_entry(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id) + SceNpClansError clans_client::remove_blacklist_entry(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id) { std::string ticket = get_clan_ticket(nph); if (ticket.empty()) @@ -674,50 +673,50 @@ namespace clan pugi::xml_node clan = doc.append_child("clan"); clan.append_child("ticket").text().set(ticket.c_str()); clan.append_child("id").text().set(clan_id); - - std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); - clan.append_child("jid").text().set(jid_str.c_str()); + + const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); + clan.append_child("jid").text().set(jid_str.c_str()); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::DeleteBlacklistEntry, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::DeleteBlacklistEntry, ClanManagerOperationType::UPDATE, doc, response); } - SceNpClansError clans_client::clan_search(u32 req_id, SceNpClansPagingRequest* paging, SceNpClansSearchableName* search, SceNpClansClanBasicInfo* clan_list, SceNpClansPagingResult* page_result) + SceNpClansError clans_client::clan_search(u32 req_id, const SceNpClansPagingRequest& paging, const SceNpClansSearchableName& search, std::vector& clan_list, SceNpClansPagingResult& page_result) { pugi::xml_document doc = pugi::xml_document(); - pugi::xml_node clan = doc.append_child("clan"); - clan.append_child("start").text().set(paging->startPos); - clan.append_child("max").text().set(paging->max); + pugi::xml_node clan = doc.append_child("clan"); + clan.append_child("start").text().set(paging.startPos); + clan.append_child("max").text().set(paging.max); pugi::xml_node filter = clan.append_child("filter"); pugi::xml_node name = filter.append_child("name"); - - std::string op_name = fmt::format("%s", static_cast(static_cast(search->nameSearchOp))); + + const std::string op_name = fmt::format("%s", static_cast(static_cast(search.nameSearchOp))); name.append_attribute("op").set_value(op_name.c_str()); - name.append_attribute("value").set_value(search->name); + name.append_attribute("value").set_value(search.name); - pugi::xml_document response = pugi::xml_document(); - SceNpClansError clan_res = send_request(req_id, ClanRequestAction::ClanSearch, ClanManagerOperationType::VIEW, &doc, &response); + pugi::xml_document response = pugi::xml_document(); + const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::ClanSearch, ClanManagerOperationType::VIEW, doc, response); - if (clan_res != SCE_NP_CLANS_SUCCESS) - return clan_res; + if (clan_res != SCE_NP_CLANS_SUCCESS) + return clan_res; - pugi::xml_node clan_result = response.child("clan"); - pugi::xml_node list = clan_result.child("list"); + const pugi::xml_node clan_result = response.child("clan"); + const pugi::xml_node list = clan_result.child("list"); - pugi::xml_attribute results = list.attribute("results"); - uint32_t results_count = results.as_uint(); + const pugi::xml_attribute results = list.attribute("results"); + const uint32_t results_count = results.as_uint(); - pugi::xml_attribute total = list.attribute("total"); - uint32_t total_count = total.as_uint(); + const pugi::xml_attribute total = list.attribute("total"); + const uint32_t total_count = total.as_uint(); - int i = 0; - for (pugi::xml_node node = list.child("info"); node; node = node.next_sibling("info")) - { - uint32_t clan_id = node.attribute("id").as_uint(); - std::string name_str = node.child("name").text().as_string(); - std::string tag_str = node.child("tag").text().as_string(); - uint32_t members_int = node.child("members").text().as_uint(); + int i = 0; + for (pugi::xml_node node = list.child("info"); node; node = node.next_sibling("info")) + { + const uint32_t clan_id = node.attribute("id").as_uint(); + const std::string name_str = node.child("name").text().as_string(); + const std::string tag_str = node.child("tag").text().as_string(); + const uint32_t members_int = node.child("members").text().as_uint(); SceNpClansClanBasicInfo entry = SceNpClansClanBasicInfo { @@ -731,22 +730,22 @@ namespace clan strcpy_trunc(entry.name, name_str); strcpy_trunc(entry.tag, tag_str); - clan_list[i] = entry; - i++; - } + ::at32(clan_list, i) = std::move(entry); + i++; + } - *page_result = SceNpClansPagingResult - { - .count = results_count, - .total = total_count - }; + page_result = SceNpClansPagingResult + { + .count = results_count, + .total = total_count + }; - return SCE_NP_CLANS_SUCCESS; + return SCE_NP_CLANS_SUCCESS; } SceNpClansError clans_client::create_clan(np::np_handler& nph, u32 req_id, std::string_view name, std::string_view tag, vm::ptr clan_id) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -758,14 +757,14 @@ namespace clan clan.append_child("tag").text().set(tag.data()); pugi::xml_document response = pugi::xml_document(); - SceNpClansError clan_res = send_request(req_id, ClanRequestAction::CreateClan, ClanManagerOperationType::UPDATE, &doc, &response); + const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::CreateClan, ClanManagerOperationType::UPDATE, doc, response); if (clan_res != SCE_NP_CLANS_SUCCESS) return clan_res; - pugi::xml_node clan_result = response.child("clan"); - pugi::xml_attribute id = clan_result.attribute("id"); - uint32_t clan_id_int = id.as_uint(); + const pugi::xml_node clan_result = response.child("clan"); + const pugi::xml_attribute id = clan_result.attribute("id"); + const uint32_t clan_id_int = id.as_uint(); *clan_id = clan_id_int; @@ -774,7 +773,7 @@ namespace clan SceNpClansError clans_client::disband_dlan(np::np_handler& nph, u32 req_id, SceNpClanId clan_id) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -784,7 +783,7 @@ namespace clan clan.append_child("id").text().set(clan_id); pugi::xml_document response = pugi::xml_document(); - SceNpClansError clan_res = send_request(req_id, ClanRequestAction::DisbandClan, ClanManagerOperationType::UPDATE, &doc, &response); + const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::DisbandClan, ClanManagerOperationType::UPDATE, doc, response); if (clan_res != SCE_NP_CLANS_SUCCESS) return clan_res; @@ -792,9 +791,9 @@ namespace clan return SCE_NP_CLANS_SUCCESS; } - SceNpClansError clans_client::request_membership(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage* /*message*/) + SceNpClansError clans_client::request_membership(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage& /*message*/) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -804,12 +803,12 @@ namespace clan clan.append_child("id").text().set(clan_id); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::RequestMembership, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::RequestMembership, ClanManagerOperationType::UPDATE, doc, response); } SceNpClansError clans_client::cancel_request_membership(np::np_handler& nph, u32 req_id, SceNpClanId clan_id) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -819,12 +818,12 @@ namespace clan clan.append_child("id").text().set(clan_id); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::CancelRequestMembership, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::CancelRequestMembership, ClanManagerOperationType::UPDATE, doc, response); } - SceNpClansError clans_client::send_membership_response(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id, SceNpClansMessage* /*message*/, b8 allow) + SceNpClansError clans_client::send_membership_response(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMessage& /*message*/, b8 allow) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -832,17 +831,17 @@ namespace clan pugi::xml_node clan = doc.append_child("clan"); clan.append_child("ticket").text().set(ticket.c_str()); clan.append_child("id").text().set(clan_id); - - std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); - clan.append_child("jid").text().set(jid_str.c_str()); + + const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); + clan.append_child("jid").text().set(jid_str.c_str()); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, allow ? ClanRequestAction::AcceptMembershipRequest : ClanRequestAction::DeclineMembershipRequest, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, allow ? ClanRequestAction::AcceptMembershipRequest : ClanRequestAction::DeclineMembershipRequest, ClanManagerOperationType::UPDATE, doc, response); } - SceNpClansError clans_client::send_invitation(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id, SceNpClansMessage* /*message*/) + SceNpClansError clans_client::send_invitation(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMessage& /*message*/) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -850,17 +849,17 @@ namespace clan pugi::xml_node clan = doc.append_child("clan"); clan.append_child("ticket").text().set(ticket.c_str()); clan.append_child("id").text().set(clan_id); - - std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); - clan.append_child("jid").text().set(jid_str.c_str()); + + const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); + clan.append_child("jid").text().set(jid_str.c_str()); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::SendInvitation, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::SendInvitation, ClanManagerOperationType::UPDATE, doc, response); } - SceNpClansError clans_client::cancel_invitation(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id) + SceNpClansError clans_client::cancel_invitation(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -868,17 +867,17 @@ namespace clan pugi::xml_node clan = doc.append_child("clan"); clan.append_child("ticket").text().set(ticket.c_str()); clan.append_child("id").text().set(clan_id); - - std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); - clan.append_child("jid").text().set(jid_str.c_str()); + + const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); + clan.append_child("jid").text().set(jid_str.c_str()); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::CancelInvitation, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::CancelInvitation, ClanManagerOperationType::UPDATE, doc, response); } - SceNpClansError clans_client::send_invitation_response(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage* /*message*/, b8 accept) + SceNpClansError clans_client::send_invitation_response(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage& /*message*/, b8 accept) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -888,12 +887,12 @@ namespace clan clan.append_child("id").text().set(clan_id); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, accept ? ClanRequestAction::AcceptInvitation : ClanRequestAction::DeclineInvitation, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, accept ? ClanRequestAction::AcceptInvitation : ClanRequestAction::DeclineInvitation, ClanManagerOperationType::UPDATE, doc, response); } - SceNpClansError clans_client::update_member_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansUpdatableMemberInfo* info) + SceNpClansError clans_client::update_member_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansUpdatableMemberInfo& info) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -906,33 +905,33 @@ namespace clan role.text().set(nph.get_npid().handle.data); pugi::xml_node description = clan.append_child("description"); - description.text().set(info->description); - + description.text().set(info.description); + pugi::xml_node status = clan.append_child("bin-attr1"); byte bin_attr_1[SCE_NP_CLANS_MEMBER_BINARY_ATTRIBUTE1_MAX_SIZE * 2 + 1] = {0}; uint32_t bin_attr_1_size = UINT32_MAX; - Base64_Encode_NoNl(info->binAttr1, info->binData1Size, bin_attr_1, &bin_attr_1_size); + Base64_Encode_NoNl(info.binAttr1, info.binData1Size, bin_attr_1, &bin_attr_1_size); if (bin_attr_1_size == UINT32_MAX) return SCE_NP_CLANS_ERROR_INVALID_ARGUMENT; - - // `reinterpret_cast` used to let the compiler select the correct overload of `set` + + // `reinterpret_cast` used to let the compiler select the correct overload of `set` status.text().set(reinterpret_cast(bin_attr_1)); pugi::xml_node allow_msg = clan.append_child("allow-msg"); - allow_msg.text().set(static_cast(info->allowMsg)); + allow_msg.text().set(static_cast(info.allowMsg)); pugi::xml_node size = clan.append_child("size"); - size.text().set(info->binData1Size); + size.text().set(info.binData1Size); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::UpdateMemberInfo, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::UpdateMemberInfo, ClanManagerOperationType::UPDATE, doc, response); } - SceNpClansError clans_client::update_clan_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansUpdatableClanInfo* info) + SceNpClansError clans_client::update_clan_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansUpdatableClanInfo& info) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -944,15 +943,15 @@ namespace clan // TODO: implement binary and integer attributes (not implemented in server yet) pugi::xml_node description = clan.append_child("description"); - description.text().set(info->description); + description.text().set(info.description); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::UpdateClanInfo, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::UpdateClanInfo, ClanManagerOperationType::UPDATE, doc, response); } SceNpClansError clans_client::join_clan(np::np_handler& nph, u32 req_id, SceNpClanId clan_id) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -962,12 +961,12 @@ namespace clan clan.append_child("id").text().set(clan_id); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::JoinClan, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::JoinClan, ClanManagerOperationType::UPDATE, doc, response); } SceNpClansError clans_client::leave_clan(np::np_handler& nph, u32 req_id, SceNpClanId clan_id) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -977,12 +976,12 @@ namespace clan clan.append_child("id").text().set(clan_id); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::LeaveClan, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::LeaveClan, ClanManagerOperationType::UPDATE, doc, response); } - SceNpClansError clans_client::kick_member(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id, SceNpClansMessage* /*message*/) + SceNpClansError clans_client::kick_member(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMessage& /*message*/) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -990,17 +989,17 @@ namespace clan pugi::xml_node clan = doc.append_child("clan"); clan.append_child("ticket").text().set(ticket.c_str()); clan.append_child("id").text().set(clan_id); - - std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); - clan.append_child("jid").text().set(jid_str.c_str()); + + const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); + clan.append_child("jid").text().set(jid_str.c_str()); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::KickMember, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::KickMember, ClanManagerOperationType::UPDATE, doc, response); } - SceNpClansError clans_client::change_member_role(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id, SceNpClansMemberRole role) + SceNpClansError clans_client::change_member_role(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMemberRole role) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -1008,20 +1007,20 @@ namespace clan pugi::xml_node clan = doc.append_child("clan"); clan.append_child("ticket").text().set(ticket.c_str()); clan.append_child("id").text().set(clan_id); - - std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); - clan.append_child("jid").text().set(jid_str.c_str()); + + const std::string jid_str = fmt::format(JID_FORMAT, np_id.handle.data); + clan.append_child("jid").text().set(jid_str.c_str()); pugi::xml_node role_node = clan.append_child("role"); role_node.text().set(static_cast(role)); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::ChangeMemberRole, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::ChangeMemberRole, ClanManagerOperationType::UPDATE, doc, response); } - SceNpClansError clans_client::retrieve_announcements(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansPagingRequest* paging, SceNpClansMessageEntry* announcements, SceNpClansPagingResult* page_result) + SceNpClansError clans_client::retrieve_announcements(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansPagingRequest& paging, std::vector& announcements, SceNpClansPagingResult& page_result) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -1029,39 +1028,39 @@ namespace clan pugi::xml_node clan = doc.append_child("clan"); clan.append_child("ticket").text().set(ticket.c_str()); clan.append_child("id").text().set(clan_id); - clan.append_child("start").text().set(paging->startPos); - clan.append_child("max").text().set(paging->max); + clan.append_child("start").text().set(paging.startPos); + clan.append_child("max").text().set(paging.max); pugi::xml_document response = pugi::xml_document(); - SceNpClansError clan_res = send_request(req_id, ClanRequestAction::RetrieveAnnouncements, ClanManagerOperationType::VIEW, &doc, &response); + const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::RetrieveAnnouncements, ClanManagerOperationType::VIEW, doc, response); if (clan_res != SCE_NP_CLANS_SUCCESS) return clan_res; - pugi::xml_node clan_result = response.child("clan"); - pugi::xml_node list = clan_result.child("list"); + const pugi::xml_node clan_result = response.child("clan"); + const pugi::xml_node list = clan_result.child("list"); - pugi::xml_attribute results = list.attribute("results"); - uint32_t results_count = results.as_uint(); + const pugi::xml_attribute results = list.attribute("results"); + const uint32_t results_count = results.as_uint(); - pugi::xml_attribute total = list.attribute("total"); - uint32_t total_count = total.as_uint(); + const pugi::xml_attribute total = list.attribute("total"); + const uint32_t total_count = total.as_uint(); int i = 0; for (pugi::xml_node node = list.child("msg-info"); node; node = node.next_sibling("msg-info")) { - pugi::xml_attribute id = node.attribute("id"); - uint32_t msg_id = id.as_uint(); + const pugi::xml_attribute id = node.attribute("id"); + const uint32_t msg_id = id.as_uint(); - std::string subject_str = node.child("subject").text().as_string(); - std::string msg_str = node.child("msg").text().as_string(); - std::string author_jid = node.child("jid").text().as_string(); - std::string msg_date = node.child("msg-date").text().as_string(); + const std::string subject_str = node.child("subject").text().as_string(); + const std::string msg_str = node.child("msg").text().as_string(); + const std::string author_jid = node.child("jid").text().as_string(); + const std::string msg_date = node.child("msg-date").text().as_string(); SceNpId author_npid; - std::string author_username = fmt::split(author_jid, {"@"})[0]; + const std::string author_username = fmt::split(author_jid, {"@"})[0]; - if (strncmp(author_username.c_str(), nph.get_npid().handle.data, 16) == 0) + if (strncmp(author_username.c_str(), nph.get_npid().handle.data, SCE_NET_NP_ONLINEID_MAX_LENGTH) == 0) { author_npid = nph.get_npid(); } @@ -1079,18 +1078,18 @@ namespace clan .subject = "", .body = "", }, - .npid = author_npid, + .npid = std::move(author_npid), .postedBy = clan_id, }; strcpy_trunc(entry.message.subject, subject_str); strcpy_trunc(entry.message.body, msg_str); - announcements[i] = entry; + ::at32(announcements, i) = std::move(entry); i++; } - *page_result = SceNpClansPagingResult + page_result = SceNpClansPagingResult { .count = results_count, .total = total_count @@ -1099,9 +1098,9 @@ namespace clan return SCE_NP_CLANS_SUCCESS; } - SceNpClansError clans_client::post_announcement(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage* announcement, SceNpClansMessageData* /*data*/, u32 duration, SceNpClansMessageId* msg_id) + SceNpClansError clans_client::post_announcement(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansMessage& announcement, const SceNpClansMessageData& /*data*/, u32 duration, SceNpClansMessageId& msg_id) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -1111,30 +1110,30 @@ namespace clan clan.append_child("id").text().set(clan_id); pugi::xml_node subject = clan.append_child("subject"); - subject.text().set(announcement->subject); + subject.text().set(announcement.subject); pugi::xml_node msg = clan.append_child("msg"); - msg.text().set(announcement->body); + msg.text().set(announcement.body); pugi::xml_node expire_date = clan.append_child("expire-date"); expire_date.text().set(duration); pugi::xml_document response = pugi::xml_document(); - SceNpClansError clan_res = send_request(req_id, ClanRequestAction::PostAnnouncement, ClanManagerOperationType::UPDATE, &doc, &response); + const SceNpClansError clan_res = send_request(req_id, ClanRequestAction::PostAnnouncement, ClanManagerOperationType::UPDATE, doc, response); if (clan_res != SCE_NP_CLANS_SUCCESS) return clan_res; - pugi::xml_node clan_result = response.child("clan"); - pugi::xml_node msg_id_node = clan_result.child("id"); - *msg_id = msg_id_node.text().as_uint(); + const pugi::xml_node clan_result = response.child("clan"); + const pugi::xml_node msg_id_node = clan_result.child("id"); + msg_id = msg_id_node.text().as_uint(); return SCE_NP_CLANS_SUCCESS; } SceNpClansError clans_client::delete_announcement(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessageId announcement_id) { - std::string ticket = get_clan_ticket(nph); + const std::string ticket = get_clan_ticket(nph); if (ticket.empty()) return SCE_NP_CLANS_ERROR_SERVICE_UNAVAILABLE; @@ -1145,7 +1144,7 @@ namespace clan clan.append_child("msg-id").text().set(announcement_id); pugi::xml_document response = pugi::xml_document(); - return send_request(req_id, ClanRequestAction::DeleteAnnouncement, ClanManagerOperationType::UPDATE, &doc, &response); + return send_request(req_id, ClanRequestAction::DeleteAnnouncement, ClanManagerOperationType::UPDATE, doc, response); } } #pragma endregion diff --git a/rpcs3/Emu/NP/clans_client.h b/rpcs3/Emu/NP/clans_client.h index 68ed4a1cf1..d56fb4bbb9 100644 --- a/rpcs3/Emu/NP/clans_client.h +++ b/rpcs3/Emu/NP/clans_client.h @@ -25,15 +25,15 @@ namespace clan }; enum class ClanSearchFilterOperator : u8 - { - Equal, - NotEqual, - GreaterThan, - GreaterThanOrEqual, - LessThan, - LessThanOrEqual, - Like, - }; + { + Equal, + NotEqual, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual, + Like, + }; enum class ClanRequestAction { @@ -70,9 +70,8 @@ namespace clan { private: - static size_t curl_write_callback(void* data, size_t size, size_t nmemb, void* clientp); - SceNpClansError send_request(u32 reqId, ClanRequestAction action, ClanManagerOperationType type, pugi::xml_document* xml_body, pugi::xml_document* out_response); + SceNpClansError send_request(u32 reqId, ClanRequestAction action, ClanManagerOperationType type, const pugi::xml_document& xml_body, pugi::xml_document& out_response); /// @brief Forge and get a V2.1 Ticket for clan operations std::string get_clan_ticket(np::np_handler& nph); @@ -81,43 +80,43 @@ namespace clan clans_client(); ~clans_client(); - SceNpClansError create_request(s32* req_id); + SceNpClansError create_request(s32& req_id); SceNpClansError destroy_request(u32 req_id); - SceNpClansError clan_search(u32 req_id, SceNpClansPagingRequest* paging, SceNpClansSearchableName* search, SceNpClansClanBasicInfo* clan_list, SceNpClansPagingResult* page_result); + SceNpClansError clan_search(u32 req_id, const SceNpClansPagingRequest& paging, const SceNpClansSearchableName& search, std::vector& clan_list, SceNpClansPagingResult& page_result); SceNpClansError create_clan(np::np_handler& nph, u32 req_id, std::string_view name, std::string_view tag, vm::ptr clan_id); SceNpClansError disband_dlan(np::np_handler& nph, u32 req_id, SceNpClanId clan_id); - SceNpClansError get_clan_list(np::np_handler& nph, u32 req_id, SceNpClansPagingRequest* paging, SceNpClansEntry* clan_list, SceNpClansPagingResult* page_result); - SceNpClansError get_clan_info(u32 req_id, SceNpClanId clan_id, SceNpClansClanInfo* clan_info); + SceNpClansError get_clan_list(np::np_handler& nph, u32 req_id, const SceNpClansPagingRequest&, std::vector& clan_list, SceNpClansPagingResult& page_result); + SceNpClansError get_clan_info(u32 req_id, SceNpClanId clan_id, SceNpClansClanInfo& clan_info); - SceNpClansError get_member_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id, SceNpClansMemberEntry* mem_info); - SceNpClansError get_member_list(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansPagingRequest* paging, SceNpClansMemberStatus status, SceNpClansMemberEntry* mem_list, SceNpClansPagingResult* page_result); + SceNpClansError get_member_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMemberEntry& mem_info); + SceNpClansError get_member_list(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansPagingRequest& paging, SceNpClansMemberStatus status, std::vector& mem_list, SceNpClansPagingResult& page_result); - SceNpClansError get_blacklist(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansPagingRequest* paging, SceNpClansBlacklistEntry* bl, SceNpClansPagingResult* page_result); - SceNpClansError add_blacklist_entry(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id); - SceNpClansError remove_blacklist_entry(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id); + SceNpClansError get_blacklist(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansPagingRequest& paging, std::vector& bl, SceNpClansPagingResult& page_result); + SceNpClansError add_blacklist_entry(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id); + SceNpClansError remove_blacklist_entry(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id); - SceNpClansError request_membership(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage* message); + SceNpClansError request_membership(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage& message); SceNpClansError cancel_request_membership(np::np_handler& nph, u32 req_id, SceNpClanId clan_id); - SceNpClansError send_membership_response(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id, SceNpClansMessage* message, b8 allow); + SceNpClansError send_membership_response(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMessage& message, b8 allow); - SceNpClansError send_invitation(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id, SceNpClansMessage* message); - SceNpClansError cancel_invitation(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id); - SceNpClansError send_invitation_response(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage* message, b8 accept); + SceNpClansError send_invitation(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMessage& message); + SceNpClansError cancel_invitation(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id); + SceNpClansError send_invitation_response(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage& message, b8 accept); SceNpClansError join_clan(np::np_handler& nph, u32 req_id, SceNpClanId clan_id); SceNpClansError leave_clan(np::np_handler& nph, u32 req_id, SceNpClanId clan_id); - SceNpClansError update_member_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansUpdatableMemberInfo* info); - SceNpClansError update_clan_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansUpdatableClanInfo* info); + SceNpClansError update_member_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansUpdatableMemberInfo& info); + SceNpClansError update_clan_info(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansUpdatableClanInfo& info); - SceNpClansError kick_member(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id, SceNpClansMessage* message); - SceNpClansError change_member_role(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpId np_id, SceNpClansMemberRole role); + SceNpClansError kick_member(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMessage& message); + SceNpClansError change_member_role(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpId& np_id, SceNpClansMemberRole role); - SceNpClansError retrieve_announcements(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansPagingRequest* paging, SceNpClansMessageEntry* announcements, SceNpClansPagingResult* page_result); - SceNpClansError post_announcement(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessage* announcement, SceNpClansMessageData* data, u32 duration, SceNpClansMessageId* announcement_id); + SceNpClansError retrieve_announcements(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansPagingRequest& paging, std::vector& announcements, SceNpClansPagingResult& page_result); + SceNpClansError post_announcement(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, const SceNpClansMessage& announcement, const SceNpClansMessageData& data, u32 duration, SceNpClansMessageId& announcement_id); SceNpClansError delete_announcement(np::np_handler& nph, u32 req_id, SceNpClanId clan_id, SceNpClansMessageId announcement_id); }; } // namespace clan diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index 63f080c5c9..cd5e1cedf3 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -627,6 +627,8 @@ + + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index 17ccca3792..7e2c398090 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -1396,6 +1396,12 @@ Emu\Io + + Emu\NP + + + Emu\NP + @@ -2806,6 +2812,12 @@ Emu\Io + + Emu\NP + + + Emu\NP + diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 2e9340b01b..470ab36812 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -915,9 +915,6 @@ Gui\rpcn - - Gui\clans - Gui\message dialog @@ -1245,6 +1242,9 @@ rpcs3 + + Gui\rpcn + From df6a3d01dfebf875dda08efe6d22b93b1a5b5d3a Mon Sep 17 00:00:00 2001 From: RipleyTom Date: Wed, 24 Dec 2025 01:55:47 +0100 Subject: [PATCH 02/59] Clean UsbTransfer state --- rpcs3/Emu/Cell/lv2/sys_usbd.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp index 78183293e7..ad5808e0f7 100644 --- a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp @@ -878,7 +878,9 @@ std::pair usb_handler_thread::get_free_transfer() u32 transfer_id = get_free_transfer_id(); auto& transfer = get_transfer(transfer_id); - transfer.busy = true; + + libusb_transfer* const transfer_buf = transfer.transfer; + transfer = {.transfer_id = transfer_id, .transfer = transfer_buf, .busy = true}; return {transfer_id, transfer}; } From 847a147002dcac3fb90dedf836d91bc9ae0d10b8 Mon Sep 17 00:00:00 2001 From: RipleyTom Date: Wed, 24 Dec 2025 02:11:03 +0100 Subject: [PATCH 03/59] Improve usb timing accuracy --- rpcs3/Emu/Cell/lv2/sys_usbd.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp index ad5808e0f7..ba9beba3ab 100644 --- a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp @@ -636,6 +636,8 @@ void usb_handler_thread::operator()() // Process asynchronous requests that are pending libusb_handle_events_timeout_completed(ctx, &lusb_tv, nullptr); + u64 delay = 1'000; + // Process fake transfers if (!fake_transfers.empty()) { @@ -650,6 +652,13 @@ void usb_handler_thread::operator()() if (transfer->expected_time > timestamp) { + const u64 diff_time = transfer->expected_time - timestamp; + + if (diff_time < delay) + { + delay = diff_time; + } + ++it; continue; } @@ -668,7 +677,7 @@ void usb_handler_thread::operator()() if (handled_devices.empty()) thread_ctrl::wait_for(500'000); else - thread_ctrl::wait_for(1'000); + thread_ctrl::wait_for(delay); } } From 77aa5d4bbfc9ab572b678f872bf8083e0dc0725e Mon Sep 17 00:00:00 2001 From: schm1dtmac Date: Wed, 24 Dec 2025 19:21:57 +0000 Subject: [PATCH 04/59] =?UTF-8?q?[macOS]=20Don=E2=80=99t=20compile=20in=20?= =?UTF-8?q?GL-related=20code=20or=20link=20GLEW?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This seems sensible enough since macOS’s archaic, deprecated OpenGL implementation isn’t supported by RPCS3 anyways. Also fixes some CI fails caused by GLEW as a consequence. --- .ci/build-mac-arm64.sh | 2 +- .ci/build-mac.sh | 2 +- 3rdparty/CMakeLists.txt | 6 ++---- rpcs3/Emu/CMakeLists.txt | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.ci/build-mac-arm64.sh b/.ci/build-mac-arm64.sh index 49b82737c7..043e421d80 100755 --- a/.ci/build-mac-arm64.sh +++ b/.ci/build-mac-arm64.sh @@ -6,7 +6,7 @@ export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 export HOMEBREW_NO_ENV_HINTS=1 export HOMEBREW_NO_INSTALL_CLEANUP=1 -brew install -f --overwrite --quiet googletest opencv@4 ffmpeg@5 "llvm@$LLVM_COMPILER_VER" glew sdl3 vulkan-headers vulkan-loader +brew install -f --overwrite --quiet googletest opencv@4 ffmpeg@5 "llvm@$LLVM_COMPILER_VER" sdl3 vulkan-headers vulkan-loader brew unlink --quiet ffmpeg qtbase qtsvg qtdeclarative brew link -f --quiet "llvm@$LLVM_COMPILER_VER" ffmpeg@5 diff --git a/.ci/build-mac.sh b/.ci/build-mac.sh index 6328ce05d3..e391e3e575 100755 --- a/.ci/build-mac.sh +++ b/.ci/build-mac.sh @@ -10,7 +10,7 @@ brew install -f --overwrite --quiet ccache "llvm@$LLVM_COMPILER_VER" brew link -f --overwrite --quiet "llvm@$LLVM_COMPILER_VER" # shellcheck disable=SC3009 arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -arch -x86_64 /usr/local/bin/brew install -f --overwrite --quiet python@3.14 opencv@4 ffmpeg@5 "llvm@$LLVM_COMPILER_VER" glew sdl3 vulkan-headers vulkan-loader +arch -x86_64 /usr/local/bin/brew install -f --overwrite --quiet python@3.14 opencv@4 ffmpeg@5 "llvm@$LLVM_COMPILER_VER" sdl3 vulkan-headers vulkan-loader arch -x86_64 /usr/local/bin/brew unlink --quiet ffmpeg qtbase qtsvg qtdeclarative arch -x86_64 /usr/local/bin/brew link -f --overwrite --quiet "llvm@$LLVM_COMPILER_VER" ffmpeg@5 diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index a800ba1dd5..3b2dc05f1f 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -107,7 +107,7 @@ add_subdirectory(yaml-cpp) # OpenGL -if (NOT ANDROID) +if (NOT ANDROID AND NOT APPLE) find_package(OpenGL REQUIRED OPTIONAL_COMPONENTS EGL) add_library(3rdparty_opengl INTERFACE) @@ -119,8 +119,6 @@ if (NOT ANDROID) else() target_link_libraries(3rdparty_opengl INTERFACE dxgi.lib d2d1.lib dwrite.lib) endif() - elseif(APPLE) - target_link_libraries(3rdparty_opengl INTERFACE OpenGL::GL OpenGL::GLU) else() target_link_libraries(3rdparty_opengl INTERFACE OpenGL::GL OpenGL::GLU OpenGL::GLX) endif() @@ -335,7 +333,7 @@ endif() # GLEW add_library(3rdparty_glew INTERFACE) -if(NOT MSVC AND NOT ANDROID) +if(NOT MSVC AND NOT ANDROID AND NOT APPLE) find_package(GLEW REQUIRED) target_link_libraries(3rdparty_glew INTERFACE GLEW::GLEW) endif() diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index defa1ef670..3769071ad7 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -545,7 +545,7 @@ target_sources(rpcs3_emu PRIVATE RSX/rsx_vertex_data.cpp ) -if(NOT ANDROID) +if(NOT ANDROID AND NOT APPLE) target_sources(rpcs3_emu PRIVATE RSX/GL/GLCommonDecompiler.cpp RSX/GL/GLCompute.cpp From 739c178aacc37acb61e8e34ddb108e4822ff7a40 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 27 Dec 2025 15:49:10 +0100 Subject: [PATCH 05/59] Qt/cheats: don't accept imported cheats unless valid --- Utilities/cheat_info.cpp | 2 +- rpcs3/rpcs3qt/cheat_manager.cpp | 25 ++++++++++++++++++++----- rpcs3/rpcs3qt/cheat_manager.h | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Utilities/cheat_info.cpp b/Utilities/cheat_info.cpp index cc8934f15a..7745d26732 100644 --- a/Utilities/cheat_info.cpp +++ b/Utilities/cheat_info.cpp @@ -34,7 +34,7 @@ bool cheat_info::from_str(std::string_view cheat_line) s64 val64 = 0; if (cheat_vec.size() != 5 || !try_to_int64(&val64, cheat_vec[2], 0, cheat_type_max - 1)) { - log_cheat.fatal("Failed to parse cheat line"); + log_cheat.error("Failed to parse cheat line: '%s'", cheat_line); return false; } diff --git a/rpcs3/rpcs3qt/cheat_manager.cpp b/rpcs3/rpcs3qt/cheat_manager.cpp index a24a2cd5fc..fa8d0b4d58 100644 --- a/rpcs3/rpcs3qt/cheat_manager.cpp +++ b/rpcs3/rpcs3qt/cheat_manager.cpp @@ -129,16 +129,27 @@ void cheat_engine::save() const cheat_file.write(out.c_str(), out.size()); } -void cheat_engine::import_cheats_from_str(std::string_view str_cheats) +bool cheat_engine::import_cheats_from_str(std::string_view str_cheats) { const auto cheats_vec = fmt::split_sv(str_cheats, {"^^^"}); + std::vector valid_cheats; + for (const auto& cheat_line : cheats_vec) { cheat_info new_cheat; - if (new_cheat.from_str(cheat_line)) - cheats[new_cheat.game][new_cheat.offset] = new_cheat; + if (!new_cheat.from_str(cheat_line)) + return false; + + valid_cheats.push_back(std::move(new_cheat)); } + + for (const cheat_info& new_cheat : valid_cheats) + { + cheats[new_cheat.game][new_cheat.offset] = new_cheat; + } + + return true; } std::string cheat_engine::export_cheats_to_str() const @@ -677,7 +688,7 @@ cheat_manager_dialog::cheat_manager_dialog(QWidget* parent) { const int row = sel->row(); - if (rows.count(row)) + if (rows.contains(row)) continue; g_cheat.erase(tbl_cheats->item(row, cheat_table_columns::title)->text().toStdString(), tbl_cheats->item(row, cheat_table_columns::offset)->data(Qt::UserRole).toUInt()); @@ -690,7 +701,11 @@ cheat_manager_dialog::cheat_manager_dialog(QWidget* parent) connect(import_cheats, &QAction::triggered, [this]() { QClipboard* clipboard = QGuiApplication::clipboard(); - g_cheat.import_cheats_from_str(clipboard->text().toStdString()); + if (!g_cheat.import_cheats_from_str(clipboard->text().toStdString())) + { + QMessageBox::warning(this, tr("Failure"), tr("Failed to import cheats.")); + return; + } update_cheat_list(); }); diff --git a/rpcs3/rpcs3qt/cheat_manager.h b/rpcs3/rpcs3qt/cheat_manager.h index b915faa8a2..c09f47428f 100644 --- a/rpcs3/rpcs3qt/cheat_manager.h +++ b/rpcs3/rpcs3qt/cheat_manager.h @@ -25,7 +25,7 @@ public: cheat_info* get(const std::string& game, const u32 offset); bool erase(const std::string& game, const u32 offset); - void import_cheats_from_str(std::string_view str_cheats); + bool import_cheats_from_str(std::string_view str_cheats); std::string export_cheats_to_str() const; void save() const; From 908c616783eea3b637e8bec8297775db8b830f72 Mon Sep 17 00:00:00 2001 From: Antonino Di Guardo <64427768+digant73@users.noreply.github.com> Date: Sat, 27 Dec 2025 17:54:03 +0100 Subject: [PATCH 06/59] Improve Manage menu (#17919) --- rpcs3/rpcs3qt/main_window.ui | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index c9c8c0645d..b62e26492e 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -281,11 +281,8 @@ - - - - + @@ -296,18 +293,35 @@ - + + + Network Services + + + + + + + + + Portals and Gates + + + + + + + + + - - - - + + - From 0ce686b1f1434f7e28fb580bebcfc229dbc4c3e6 Mon Sep 17 00:00:00 2001 From: oltolm Date: Mon, 22 Dec 2025 00:25:45 +0100 Subject: [PATCH 07/59] fix lld warning LNK4217 --- rpcs3/Emu/CMakeLists.txt | 1 + rpcs3/rpcs3.vcxproj | 6 +++--- rpcs3/rpcs3qt/curl_handle.h | 3 --- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index 3769071ad7..8754df84c1 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -666,6 +666,7 @@ target_link_libraries(rpcs3_emu 3rdparty::yaml-cpp 3rdparty::zlib 3rdparty::zstd + 3rdparty::libcurl ) if(APPLE) diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index d8d5053179..a824f65267 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -77,7 +77,7 @@ 4577;4467;4281;%(DisableSpecificWarnings) $(IntDir) MaxSpeed - _WINDOWS;UNICODE;WIN32;WIN64;WIN32_LEAN_AND_MEAN;HAVE_VULKAN;HAVE_OPENCV;CV_IGNORE_DEBUG_BUILD_GUARD;MINIUPNP_STATICLIB;ZLIB_CONST;AL_LIBTYPE_STATIC;WOLFSSL_USER_SETTINGS;HAVE_SDL3;WITH_DISCORD_RPC;QT_NO_DEBUG;QT_WIDGETS_LIB;QT_GUI_LIB;QT_CORE_LIB;NDEBUG;QT_CONCURRENT_LIB;QT_MULTIMEDIA_LIB;QT_MULTIMEDIAWIDGETS_LIB;QT_SVG_LIB;%(PreprocessorDefinitions) + _WINDOWS;UNICODE;WIN32;WIN64;WIN32_LEAN_AND_MEAN;HAVE_VULKAN;HAVE_OPENCV;CV_IGNORE_DEBUG_BUILD_GUARD;MINIUPNP_STATICLIB;ZLIB_CONST;AL_LIBTYPE_STATIC;WOLFSSL_USER_SETTINGS;HAVE_SDL3;WITH_DISCORD_RPC;QT_NO_DEBUG;QT_WIDGETS_LIB;QT_GUI_LIB;QT_CORE_LIB;NDEBUG;QT_CONCURRENT_LIB;QT_MULTIMEDIA_LIB;QT_MULTIMEDIAWIDGETS_LIB;QT_SVG_LIB;CURL_STATICLIB;%(PreprocessorDefinitions) false $(IntDir)vc$(PlatformToolsetVersion).pdb true @@ -138,7 +138,7 @@ 4577;4467;4281;%(DisableSpecificWarnings) $(IntDir) Disabled - _WINDOWS;UNICODE;WIN32;WIN64;WIN32_LEAN_AND_MEAN;HAVE_VULKAN;HAVE_OPENCV;CV_IGNORE_DEBUG_BUILD_GUARD;MINIUPNP_STATICLIB;ZLIB_CONST;AL_LIBTYPE_STATIC;WOLFSSL_USER_SETTINGS;HAVE_SDL3;QT_WIDGETS_LIB;QT_GUI_LIB;QT_CORE_LIB;QT_CONCURRENT_LIB;QT_MULTIMEDIA_LIB;QT_MULTIMEDIAWIDGETS_LIB;QT_SVG_LIB;%(PreprocessorDefinitions) + _WINDOWS;UNICODE;WIN32;WIN64;WIN32_LEAN_AND_MEAN;HAVE_VULKAN;HAVE_OPENCV;CV_IGNORE_DEBUG_BUILD_GUARD;MINIUPNP_STATICLIB;ZLIB_CONST;AL_LIBTYPE_STATIC;WOLFSSL_USER_SETTINGS;HAVE_SDL3;QT_WIDGETS_LIB;QT_GUI_LIB;QT_CORE_LIB;QT_CONCURRENT_LIB;QT_MULTIMEDIA_LIB;QT_MULTIMEDIAWIDGETS_LIB;QT_SVG_LIB;CURL_STATICLIB;%(PreprocessorDefinitions) false true true @@ -2230,4 +2230,4 @@ - \ No newline at end of file + diff --git a/rpcs3/rpcs3qt/curl_handle.h b/rpcs3/rpcs3qt/curl_handle.h index e0d4db22d3..090c198215 100644 --- a/rpcs3/rpcs3qt/curl_handle.h +++ b/rpcs3/rpcs3qt/curl_handle.h @@ -2,9 +2,6 @@ #include -#ifndef CURL_STATICLIB -#define CURL_STATICLIB -#endif #include namespace rpcs3::curl From fdbf25076ec991af2316623ce376c570459bf261 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 27 Dec 2025 18:56:36 +0100 Subject: [PATCH 08/59] Fix lld warning LNK4286 --- rpcs3/emucore.vcxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index cd5e1cedf3..357dfe238d 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -42,8 +42,8 @@ Use ..\3rdparty\miniupnp\miniupnp\miniupnpc\include;..\3rdparty\wolfssl\wolfssl;..\3rdparty\flatbuffers\include;..\3rdparty\libusb\libusb\libusb;..\3rdparty\yaml-cpp\yaml-cpp\include;..\3rdparty\SoundTouch\soundtouch\include;..\3rdparty\rtmidi\rtmidi;..\3rdparty\zlib\zlib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\llvm_build\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm_build\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm_build\include;$(VULKAN_SDK)\Include;..\3rdparty\zstd\zstd\lib;$(SolutionDir)3rdparty\fusion\fusion\Fusion;$(SolutionDir)3rdparty\wolfssl\extra\win32;$(SolutionDir)3rdparty\libsdl-org\SDL\include;$(SolutionDir)3rdparty\glslang\glslang;$(SolutionDir)3rdparty\curl\curl\include MaxSpeed - AL_LIBTYPE_STATIC;MINIUPNP_STATICLIB;HAVE_VULKAN;HAVE_SDL3;ZLIB_CONST;WOLFSSL_USER_SETTINGS;%(PreprocessorDefinitions) - AL_LIBTYPE_STATIC;MINIUPNP_STATICLIB;HAVE_VULKAN;HAVE_SDL3;ZLIB_CONST;WOLFSSL_USER_SETTINGS;%(PreprocessorDefinitions) + AL_LIBTYPE_STATIC;MINIUPNP_STATICLIB;HAVE_VULKAN;HAVE_SDL3;ZLIB_CONST;WOLFSSL_USER_SETTINGS;CURL_STATICLIB;%(PreprocessorDefinitions) + AL_LIBTYPE_STATIC;MINIUPNP_STATICLIB;HAVE_VULKAN;HAVE_SDL3;ZLIB_CONST;WOLFSSL_USER_SETTINGS;CURL_STATICLIB;%(PreprocessorDefinitions) %(AdditionalModuleDependencies) %(AdditionalModuleDependencies) From ce0a6da88e81506b8b59e8b3adbe6c74da1b6eab Mon Sep 17 00:00:00 2001 From: Florin9doi Date: Tue, 23 Dec 2025 22:12:28 +0200 Subject: [PATCH 09/59] Docs: Update clone instructions --- BUILDING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BUILDING.md b/BUILDING.md index 60b7046cb2..32cdc6cc03 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -108,9 +108,10 @@ Clone and initialize the repository ```bash git clone --recurse-submodules https://github.com/RPCS3/rpcs3.git cd rpcs3 +git submodule sync # This is automatically done by `git clone --recurse-submodules`, # but in case you forgot it, you can manually fetch submodules this way: -git submodule update --init +git submodule update --init --recursive ``` ### Windows From eafe0f76cfedc4820a95ddd72d6af70178e26990 Mon Sep 17 00:00:00 2001 From: Florin9doi Date: Tue, 23 Dec 2025 22:08:55 +0200 Subject: [PATCH 10/59] USB: Update Kamen Rider Summonride virtual portal - Increase ack time for interrupt packets - Add missing HID Descriptor Node --- rpcs3/Emu/Io/KamenRider.cpp | 60 +++++++++++++++++++++++----- rpcs3/Emu/Io/KamenRider.h | 10 ++--- rpcs3/rpcs3qt/kamen_rider_dialog.cpp | 4 +- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/rpcs3/Emu/Io/KamenRider.cpp b/rpcs3/Emu/Io/KamenRider.cpp index aaa4836f08..df7b86a07f 100644 --- a/rpcs3/Emu/Io/KamenRider.cpp +++ b/rpcs3/Emu/Io/KamenRider.cpp @@ -39,7 +39,7 @@ kamen_rider_figure& rider_gate::get_figure_by_uid(const std::array uid) return figures[7]; } -void rider_gate::get_blank_response(u8 command, u8 sequence, std::array& reply_buf) +void rider_gate::get_blank_response(std::array& reply_buf, u8 command, u8 sequence) { reply_buf = {0x55, 0x02, command, sequence}; reply_buf[4] = generate_checksum(reply_buf, 4); @@ -93,7 +93,7 @@ void rider_gate::query_block(std::array& reply_buf, u8 command, u8 seque reply_buf[21] = generate_checksum(reply_buf, 21); } -void rider_gate::write_block(std::array& replyBuf, u8 command, u8 sequence, const u8* uid, u8 sector, u8 block, const u8* to_write_buf) +void rider_gate::write_block(std::array& reply_buf, u8 command, u8 sequence, const u8* uid, u8 sector, u8 block, const u8* to_write_buf) { std::lock_guard lock(kamen_mutex); @@ -108,7 +108,7 @@ void rider_gate::write_block(std::array& replyBuf, u8 command, u8 sequen } } - get_blank_response(command, sequence, replyBuf); + get_blank_response(reply_buf, command, sequence); } std::optional> rider_gate::pop_added_removed_response() @@ -190,11 +190,50 @@ u8 rider_gate::load_figure(const std::array& buf, fs::file in_f usb_device_kamen_rider::usb_device_kamen_rider(const std::array& location) : usb_device_emulated(location) { - device = UsbDescriptorNode(USB_DESCRIPTOR_DEVICE, UsbDeviceDescriptor{0x200, 0x0, 0x0, 0x0, 0x40, 0x0E6F, 0x200A, 0x100, 0x1, 0x2, 0x3, 0x1}); - auto& config0 = device.add_node(UsbDescriptorNode(USB_DESCRIPTOR_CONFIG, UsbDeviceConfiguration{0x29, 0x1, 0x1, 0x0, 0x80, 0xFA})); - config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_INTERFACE, UsbDeviceInterface{0x0, 0x0, 0x2, 0x3, 0x0, 0x0, 0x0})); - config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_ENDPOINT, UsbDeviceEndpoint{0x81, 0x3, 0x40, 0x1})); - config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_ENDPOINT, UsbDeviceEndpoint{0x1, 0x3, 0x40, 0x1})); + device = UsbDescriptorNode(USB_DESCRIPTOR_DEVICE, UsbDeviceDescriptor{ + .bcdUSB = 0x0200, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = 0x40, + .idVendor = 0x0E6F, + .idProduct = 0x200A, + .bcdDevice = 0x0100, + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + .bNumConfigurations = 0x01}); + auto& config0 = device.add_node(UsbDescriptorNode(USB_DESCRIPTOR_CONFIG, UsbDeviceConfiguration{ + .wTotalLength = 0x0029, + .bNumInterfaces = 0x01, + .bConfigurationValue = 0x01, + .iConfiguration = 0x00, + .bmAttributes = 0x80, + .bMaxPower = 0xFA})); + config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_INTERFACE, UsbDeviceInterface{ + .bInterfaceNumber = 0x00, + .bAlternateSetting = 0x00, + .bNumEndpoints = 0x02, + .bInterfaceClass = 0x03, + .bInterfaceSubClass = 0x00, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00})); + config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_HID, UsbDeviceHID{ + .bcdHID = 0x0100, + .bCountryCode = 0x00, + .bNumDescriptors = 0x01, + .bDescriptorType = 0x22, + .wDescriptorLength = 0x001d})); + config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_ENDPOINT, UsbDeviceEndpoint{ + .bEndpointAddress = 0x81, + .bmAttributes = 0x03, + .wMaxPacketSize = 0x0040, + .bInterval = 0x1})); + config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_ENDPOINT, UsbDeviceEndpoint{ + .bEndpointAddress = 0x01, + .bmAttributes = 0x03, + .wMaxPacketSize = 0x0040, + .bInterval = 0x1})); } usb_device_kamen_rider::~usb_device_kamen_rider() @@ -227,7 +266,7 @@ void usb_device_kamen_rider::interrupt_transfer(u32 buf_size, u8* buf, u32 endpo if (endpoint == 0x81) { // Respond after FF command - transfer->expected_time = get_timestamp() + 1000; + transfer->expected_time = get_timestamp() + 22000; std::optional> response = g_ridergate.pop_added_removed_response(); if (response) { @@ -246,6 +285,7 @@ void usb_device_kamen_rider::interrupt_transfer(u32 buf_size, u8* buf, u32 endpo } else if (endpoint == 0x01) { + transfer->expected_time = get_timestamp() + 10; const u8 command = buf[2]; const u8 sequence = buf[3]; @@ -261,7 +301,7 @@ void usb_device_kamen_rider::interrupt_transfer(u32 buf_size, u8* buf, u32 endpo case 0xC0: case 0xC3: // Color Commands { - g_ridergate.get_blank_response(command, sequence, q_result); + g_ridergate.get_blank_response(q_result, command, sequence); break; } case 0xD0: // Tag List diff --git a/rpcs3/Emu/Io/KamenRider.h b/rpcs3/Emu/Io/KamenRider.h index 0e30024b06..6c4bea29ee 100644 --- a/rpcs3/Emu/Io/KamenRider.h +++ b/rpcs3/Emu/Io/KamenRider.h @@ -18,11 +18,11 @@ struct kamen_rider_figure class rider_gate { public: - void get_blank_response(u8 command, u8 sequence, std::array& reply_buf); - void wake_rider_gate(std::array& replyBuf, u8 command, u8 sequence); - void get_list_tags(std::array& replyBuf, u8 command, u8 sequence); - void query_block(std::array& replyBuf, u8 command, u8 sequence, const u8* uid, u8 sector, u8 block); - void write_block(std::array& replyBuf, u8 command, u8 sequence, const u8* uid, u8 sector, u8 block, const u8* to_write_buf); + void get_blank_response(std::array& reply_buf, u8 command, u8 sequence); + void wake_rider_gate(std::array& reply_buf, u8 command, u8 sequence); + void get_list_tags(std::array& reply_buf, u8 command, u8 sequence); + void query_block(std::array& reply_buf, u8 command, u8 sequence, const u8* uid, u8 sector, u8 block); + void write_block(std::array& reply_buf, u8 command, u8 sequence, const u8* uid, u8 sector, u8 block, const u8* to_write_buf); std::optional> pop_added_removed_response(); bool remove_figure(u8 position); diff --git a/rpcs3/rpcs3qt/kamen_rider_dialog.cpp b/rpcs3/rpcs3qt/kamen_rider_dialog.cpp index 61932f4cd1..8a4ef3e930 100644 --- a/rpcs3/rpcs3qt/kamen_rider_dialog.cpp +++ b/rpcs3/rpcs3qt/kamen_rider_dialog.cpp @@ -31,7 +31,7 @@ static const std::map, const std::string> li {{0x12, 0x30}, "Kamen Rider Wizard Fire"}, {{0x13, 0x40}, "Kamen Rider Fourze Light"}, {{0x14, 0x20}, "Kamen Rider 000 Water"}, - {{0x15, 0x10}, "Kamen Rider Double Wind"}, + {{0x15, 0x10}, "Kamen Rider W (Double) Wind"}, {{0x16, 0x50}, "Kamen Rider Decade Dark"}, {{0x17, 0x50}, "Kamen Rider Kiva Dark"}, {{0x18, 0x40}, "Kamen Rider Den-O Light"}, @@ -409,7 +409,7 @@ void kamen_rider_dialog::update_edits() } else { - display_string = QString(tr("Unknown (Id:%1 Var:%2)")).arg(fig_id).arg(fig_type); + display_string = QString(tr("Unknown (Id:%1 Type:%2)")).arg(fig_id).arg(fig_type); } } else From 80d7db520b4bb31d2ea6d824194168e9fac6a074 Mon Sep 17 00:00:00 2001 From: Florin9doi Date: Tue, 23 Dec 2025 22:15:28 +0200 Subject: [PATCH 11/59] USB: Remove unused method --- rpcs3/Emu/Io/Dimensions.cpp | 5 ----- rpcs3/Emu/Io/Dimensions.h | 1 - 2 files changed, 6 deletions(-) diff --git a/rpcs3/Emu/Io/Dimensions.cpp b/rpcs3/Emu/Io/Dimensions.cpp index e80a64c0c9..84a604a86e 100644 --- a/rpcs3/Emu/Io/Dimensions.cpp +++ b/rpcs3/Emu/Io/Dimensions.cpp @@ -700,8 +700,3 @@ void usb_device_dimensions::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoi break; } } - -void usb_device_dimensions::isochronous_transfer(UsbTransfer* transfer) -{ - usb_device_emulated::isochronous_transfer(transfer); -} diff --git a/rpcs3/Emu/Io/Dimensions.h b/rpcs3/Emu/Io/Dimensions.h index e2bfbd1e7f..d25fb8ed2e 100644 --- a/rpcs3/Emu/Io/Dimensions.h +++ b/rpcs3/Emu/Io/Dimensions.h @@ -76,7 +76,6 @@ public: 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; protected: std::queue> m_queries; From bdcde24e28dfbe538f9fb3f47003e38c50da32fd Mon Sep 17 00:00:00 2001 From: Florin9doi Date: Tue, 23 Dec 2025 22:09:08 +0200 Subject: [PATCH 12/59] USB: Remove a duplicate PSP entry --- rpcs3/Emu/Cell/lv2/sys_usbd.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp index ba9beba3ab..cf6339081e 100644 --- a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp @@ -246,7 +246,7 @@ private: {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, 0x01CB, 0x01CB, "PSP Type D", nullptr, nullptr}, // UsbPspCm {0x054C, 0x02D2, 0x02D2, "PSP Slim", nullptr, nullptr}, // 0x0900: "H050 USJ(C) PCB rev00", 0x0910: "USIO PCB rev00" @@ -261,9 +261,6 @@ private: // 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}, From 0536ad3b230d0012bfa203e9d751058a3d13e8fa Mon Sep 17 00:00:00 2001 From: Florin9doi Date: Tue, 23 Dec 2025 22:09:40 +0200 Subject: [PATCH 13/59] USB: Fix an incorrect log --- rpcs3/Emu/Io/TopShotElite.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcs3/Emu/Io/TopShotElite.cpp b/rpcs3/Emu/Io/TopShotElite.cpp index c241fe1ed4..0cbbd5287c 100644 --- a/rpcs3/Emu/Io/TopShotElite.cpp +++ b/rpcs3/Emu/Io/TopShotElite.cpp @@ -274,7 +274,7 @@ void usb_device_topshotelite::interrupt_transfer(u32 buf_size, u8* buf, u32 /*en if (m_controller_index >= g_cfg_topshotelite.players.size()) { - topshotelite_log.warning("Top Shot Fearmaster controllers are only supported for Player1 to Player%d", g_cfg_topshotelite.players.size()); + topshotelite_log.warning("Top Shot Elite controllers are only supported for Player1 to Player%d", g_cfg_topshotelite.players.size()); prepare_data(&ts, buf); return; } From 5e4c2433c1924f4c47309ae9dd380c3ee7d0ac7b Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sat, 13 Dec 2025 14:35:31 +0300 Subject: [PATCH 14/59] vk/gl: Mark unused FP outputs as unused --- rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp | 15 +++++++++++++-- rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp | 18 ++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp index 4cbd92eecd..0879e5e987 100644 --- a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp @@ -103,8 +103,19 @@ void GLFragmentDecompilerThread::insertOutputs(std::stringstream & OS) const auto reg_type = float_type ? "vec4" : getHalfTypeName(4); for (uint i = 0; i < std::size(table); ++i) { - if (m_parr.HasParam(PF_PARAM_NONE, reg_type, table[i].second)) - OS << "layout(location=" << i << ") out vec4 " << table[i].first << ";\n"; + if (!m_parr.HasParam(PF_PARAM_NONE, reg_type, table[i].second)) + { + continue; + } + + if (i >= m_prog.mrt_buffers_count) + { + // Dead writes. Declare as temp variables for DCE to clean up. + OS << "vec4 " << table[i].first << "; // Unused\n"; + continue; + } + + OS << "layout(location=" << i << ") out vec4 " << table[i].first << ";\n"; } } diff --git a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp index ea9d7cd0f1..ecca27f0df 100644 --- a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp @@ -150,17 +150,27 @@ void VKFragmentDecompilerThread::insertOutputs(std::stringstream & OS) { "ocol3", m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r4" : "h8" }, }; - //NOTE: We do not skip outputs, the only possible combinations are a(0), b(0), ab(0,1), abc(0,1,2), abcd(0,1,2,3) + // NOTE: We do not skip outputs, the only possible combinations are a(0), b(0), ab(0,1), abc(0,1,2), abcd(0,1,2,3) u8 output_index = 0; const bool float_type = (m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS) || !device_props.has_native_half_support; const auto reg_type = float_type ? "vec4" : getHalfTypeName(4); for (uint i = 0; i < std::size(table); ++i) { - if (m_parr.HasParam(PF_PARAM_NONE, reg_type, table[i].second)) + if (!m_parr.HasParam(PF_PARAM_NONE, reg_type, table[i].second)) { - OS << "layout(location=" << std::to_string(output_index++) << ") " << "out vec4 " << table[i].first << ";\n"; - vk_prog->output_color_masks[i] = -1; + continue; } + + if (i >= m_prog.mrt_buffers_count) + { + // Dead writes. Declare as temp variables for DCE to clean up. + OS << "vec4 " << table[i].first << "; // Unused\n"; + vk_prog->output_color_masks[i] = 0; + continue; + } + + OS << "layout(location=" << std::to_string(output_index++) << ") " << "out vec4 " << table[i].first << ";\n"; + vk_prog->output_color_masks[i] = -1; } } From 308adca20657215bd8d191dfd6bc56c87c243a1b Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sat, 13 Dec 2025 14:37:01 +0300 Subject: [PATCH 15/59] rsx/fp: Stop referencing wpos when the value is just getting discarded. --- .../RSX/Program/FragmentProgramDecompiler.cpp | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.cpp b/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.cpp index 94b92ce98e..1ba30e45b3 100644 --- a/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.cpp +++ b/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.cpp @@ -551,18 +551,22 @@ template std::string FragmentProgramDecompiler::GetSRC(T src) { std::string ret; u32 precision_modifier = 0; + u32 register_index = umax; if constexpr (std::is_same_v) { precision_modifier = src1.src0_prec_mod; + register_index = 0; } else if constexpr (std::is_same_v) { precision_modifier = src1.src1_prec_mod; + register_index = 1; } else if constexpr (std::is_same_v) { precision_modifier = src1.src2_prec_mod; + register_index = 2; } switch (src.reg_type) @@ -645,18 +649,29 @@ template std::string FragmentProgramDecompiler::GetSRC(T src) { // TEX0 - TEX9 // Texcoord 2d mask seems to reset the last 2 arguments to 0 and w if set + + // Opt: Skip emitting w dependency unless w coord is actually being sampled + ensure(register_index != umax); + const auto lane_mask = FP::get_src_vector_lane_mask_shuffled(m_prog, m_instruction, register_index); + const auto touches_z = !!(lane_mask & (1u << 2)); + const bool touches_w = !!(lane_mask & (1u << 3)); + const u8 texcoord = u8(register_id) - 4; if (m_prog.texcoord_is_point_coord(texcoord)) { // Point sprite coord generation. Stacks with the 2D override mask. - if (m_prog.texcoord_is_2d(texcoord)) + if (!m_prog.texcoord_is_2d(texcoord)) { - ret += getFloatTypeName(4) + "(gl_PointCoord, 0., in_w)"; - properties.has_w_access = true; + ret += getFloatTypeName(4) + "(gl_PointCoord, 1., 0.)"; + } + else if (!touches_w) + { + ret += getFloatTypeName(4) + "(gl_PointCoord, 0., 0.)"; } else { - ret += getFloatTypeName(4) + "(gl_PointCoord, 1., 0.)"; + ret += getFloatTypeName(4) + "(gl_PointCoord, 0., in_w)"; + properties.has_w_access = true; } } else if (src2.perspective_corr) @@ -673,14 +688,19 @@ template std::string FragmentProgramDecompiler::GetSRC(T src) } else { - if (m_prog.texcoord_is_2d(texcoord)) + const bool skip_zw_load = !touches_z && !touches_w; + if (!m_prog.texcoord_is_2d(texcoord) || skip_zw_load) { - ret += getFloatTypeName(4) + "(" + reg_var + ".xy, 0., in_w)"; - properties.has_w_access = true; + ret += reg_var; + } + else if (!touches_w) + { + ret += getFloatTypeName(4) + "(" + reg_var + ".xy, 0., 0.)"; } else { - ret += reg_var; + ret += getFloatTypeName(4) + "(" + reg_var + ".xy, 0., in_w)"; + properties.has_w_access = true; } } break; From 215880cd1a2fc88e6b0ac252012fe79f6ade5f63 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sat, 13 Dec 2025 14:59:54 +0300 Subject: [PATCH 16/59] rsx/vk/gl: Make texture parameter reads backend-dependent - Allows optimizations and improvements per-backend --- rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp | 3 ++- rpcs3/Emu/RSX/GL/GLShaderInterpreter.cpp | 1 + .../RSXFragmentTextureDepthConversion.glsl | 6 +++--- .../RSXProg/RSXFragmentTextureMSAAOps.glsl | 2 +- .../RSXProg/RSXFragmentTextureOps.glsl | 16 ++++++++-------- rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp | 3 ++- rpcs3/Emu/RSX/VK/VKShaderInterpreter.cpp | 1 + 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp index 0879e5e987..cc28c0c98f 100644 --- a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp @@ -201,7 +201,8 @@ void GLFragmentDecompilerThread::insertConstants(std::stringstream & OS) " uvec4 stipple_pattern[8];\n" "};\n\n" - "#define texture_base_index 0\n\n"; + "#define texture_base_index 0\n" + "#define TEX_PARAM(index) texture_parameters[index]\n\n"; } void GLFragmentDecompilerThread::insertGlobalFunctions(std::stringstream &OS) diff --git a/rpcs3/Emu/RSX/GL/GLShaderInterpreter.cpp b/rpcs3/Emu/RSX/GL/GLShaderInterpreter.cpp index 9606ca7b16..c7f9ec2622 100644 --- a/rpcs3/Emu/RSX/GL/GLShaderInterpreter.cpp +++ b/rpcs3/Emu/RSX/GL/GLShaderInterpreter.cpp @@ -300,6 +300,7 @@ namespace gl } builder << "\n" + "#define TEX_PARAM(index) texture_parameters[index + texture_base_index]\n" "#define IS_TEXTURE_RESIDENT(index) (texture_handles[index] < 0xFF)\n" "#define SAMPLER1D(index) sampler1D_array[texture_handles[index]]\n" "#define SAMPLER2D(index) sampler2D_array[texture_handles[index]]\n" diff --git a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureDepthConversion.glsl b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureDepthConversion.glsl index ef77ae179d..a9737e97fa 100644 --- a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureDepthConversion.glsl +++ b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureDepthConversion.glsl @@ -1,8 +1,8 @@ R"( #define ZS_READ(index, coord) vec2(texture(TEX_NAME(index), coord).r, float(texture(TEX_NAME_STENCIL(index), coord).x)) -#define TEX1D_Z24X8_RGBA8(index, coord1) _process_texel(convert_z24x8_to_rgba8(ZS_READ(index, COORD_SCALE1(index, coord1)), texture_parameters[index + texture_base_index].remap, TEX_FLAGS(index)), TEX_FLAGS(index)) -#define TEX2D_Z24X8_RGBA8(index, coord2) _process_texel(convert_z24x8_to_rgba8(ZS_READ(index, COORD_SCALE2(index, coord2)), texture_parameters[index + texture_base_index].remap, TEX_FLAGS(index)), TEX_FLAGS(index)) -#define TEX3D_Z24X8_RGBA8(index, coord3) _process_texel(convert_z24x8_to_rgba8(ZS_READ(index, COORD_SCALE3(index, coord3)), texture_parameters[index + texture_base_index].remap, TEX_FLAGS(index)), TEX_FLAGS(index)) +#define TEX1D_Z24X8_RGBA8(index, coord1) _process_texel(convert_z24x8_to_rgba8(ZS_READ(index, COORD_SCALE1(index, coord1)), TEX_PARAM(index).remap, TEX_FLAGS(index)), TEX_FLAGS(index)) +#define TEX2D_Z24X8_RGBA8(index, coord2) _process_texel(convert_z24x8_to_rgba8(ZS_READ(index, COORD_SCALE2(index, coord2)), TEX_PARAM(index).remap, TEX_FLAGS(index)), TEX_FLAGS(index)) +#define TEX3D_Z24X8_RGBA8(index, coord3) _process_texel(convert_z24x8_to_rgba8(ZS_READ(index, COORD_SCALE3(index, coord3)), TEX_PARAM(index).remap, TEX_FLAGS(index)), TEX_FLAGS(index)) // NOTE: Memory layout is fetched as byteswapped BGRA [GBAR] (GOW collection, DS2, DeS) // The A component (Z) is useless (should contain stencil8 or just 1) diff --git a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOps.glsl b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOps.glsl index 37f2427ba0..965eac5276 100644 --- a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOps.glsl +++ b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOps.glsl @@ -4,7 +4,7 @@ R"( #define TEX2D_MS(index, coord2) _process_texel(sampleTexture2DMS(TEX_NAME(index), coord2, index), TEX_FLAGS(index)) #define TEX2D_SHADOW_MS(index, coord3) vec4(comparison_passes(sampleTexture2DMS(TEX_NAME(index), coord3.xy, index).x, coord3.z, ZCOMPARE_FUNC(index))) #define TEX2D_SHADOWPROJ_MS(index, coord4) TEX2D_SHADOW_MS(index, (coord4.xyz / coord4.w)) -#define TEX2D_Z24X8_RGBA8_MS(index, coord2) _process_texel(convert_z24x8_to_rgba8(ZS_READ_MS(index, coord2), texture_parameters[index + texture_base_index].remap, TEX_FLAGS(index)), TEX_FLAGS(index)) +#define TEX2D_Z24X8_RGBA8_MS(index, coord2) _process_texel(convert_z24x8_to_rgba8(ZS_READ_MS(index, coord2), TEX_PARAM(index).remap, TEX_FLAGS(index)), TEX_FLAGS(index)) vec3 compute2x2DownsampleWeights(const in float coord, const in float uv_step, const in float actual_step) { diff --git a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureOps.glsl b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureOps.glsl index 7ef54a244d..e231333184 100644 --- a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureOps.glsl +++ b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureOps.glsl @@ -24,17 +24,17 @@ R"( uint _texture_flag_override = 0; #define _enable_texture_expand() _texture_flag_override = SIGN_EXPAND_MASK #define _disable_texture_expand() _texture_flag_override = 0 - #define TEX_FLAGS(index) (texture_parameters[index + texture_base_index].flags | _texture_flag_override) + #define TEX_FLAGS(index) (TEX_PARAM(index).flags | _texture_flag_override) #else - #define TEX_FLAGS(index) texture_parameters[index + texture_base_index].flags + #define TEX_FLAGS(index) TEX_PARAM(index).flags #endif #define TEX_NAME(index) tex##index #define TEX_NAME_STENCIL(index) tex##index##_stencil -#define COORD_SCALE1(index, coord1) _texcoord_xform(coord1, texture_parameters[index + texture_base_index]) -#define COORD_SCALE2(index, coord2) _texcoord_xform(coord2, texture_parameters[index + texture_base_index]) -#define COORD_SCALE3(index, coord3) _texcoord_xform(coord3, texture_parameters[index + texture_base_index]) +#define COORD_SCALE1(index, coord1) _texcoord_xform(coord1, TEX_PARAM(index)) +#define COORD_SCALE2(index, coord2) _texcoord_xform(coord2, TEX_PARAM(index)) +#define COORD_SCALE3(index, coord3) _texcoord_xform(coord3, TEX_PARAM(index)) #define COORD_PROJ1(index, coord2) COORD_SCALE1(index, coord2.x / coord2.y) #define COORD_PROJ2(index, coord3) COORD_SCALE2(index, coord3.xy / coord3.z) #define COORD_PROJ3(index, coord4) COORD_SCALE3(index, coord4.xyz / coord4.w) @@ -57,9 +57,9 @@ R"( #ifdef _ENABLE_SHADOW #ifdef _EMULATED_TEXSHADOW - #define SHADOW_COORD(index, coord3) _texcoord_xform_shadow(coord3, texture_parameters[index + texture_base_index]) - #define SHADOW_COORD4(index, coord4) _texcoord_xform_shadow(coord4, texture_parameters[index + texture_base_index]) - #define SHADOW_COORD_PROJ(index, coord4) _texcoord_xform_shadow(coord4.xyz / coord4.w, texture_parameters[index + texture_base_index]) + #define SHADOW_COORD(index, coord3) _texcoord_xform_shadow(coord3, TEX_PARAM(index)) + #define SHADOW_COORD4(index, coord4) _texcoord_xform_shadow(coord4, TEX_PARAM(index)) + #define SHADOW_COORD_PROJ(index, coord4) _texcoord_xform_shadow(coord4.xyz / coord4.w, TEX_PARAM(index)) #define TEX2D_SHADOW(index, coord3) texture(TEX_NAME(index), SHADOW_COORD(index, coord3)) #define TEX3D_SHADOW(index, coord4) texture(TEX_NAME(index), SHADOW_COORD4(index, coord4)) diff --git a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp index ecca27f0df..0cba2fa80b 100644 --- a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp @@ -346,7 +346,8 @@ void VKFragmentDecompilerThread::insertGlobalFunctions(std::stringstream &OS) } OS << - "#define texture_base_index _fs_texture_base_index\n\n"; + "#define texture_base_index _fs_texture_base_index\n" + "#define TEX_PARAM(index) texture_parameters[index + texture_base_index]\n\n"; glsl::insert_glsl_legacy_function(OS, m_shader_props); } diff --git a/rpcs3/Emu/RSX/VK/VKShaderInterpreter.cpp b/rpcs3/Emu/RSX/VK/VKShaderInterpreter.cpp index 083c3f8c7f..9315ac1593 100644 --- a/rpcs3/Emu/RSX/VK/VKShaderInterpreter.cpp +++ b/rpcs3/Emu/RSX/VK/VKShaderInterpreter.cpp @@ -264,6 +264,7 @@ namespace vk } builder << "\n" + "#define TEX_PARAM(index) texture_parameters[index + texture_base_index]\n" "#define IS_TEXTURE_RESIDENT(index) true\n" "#define SAMPLER1D(index) sampler1D_array[index]\n" "#define SAMPLER2D(index) sampler2D_array[index]\n" From 3cb3f6972e5152a9e2130aa8b08c539f8332aee6 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sat, 13 Dec 2025 15:18:43 +0300 Subject: [PATCH 17/59] vk: Hoist texture param loads --- rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp index 0cba2fa80b..c720ae8550 100644 --- a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp @@ -347,7 +347,7 @@ void VKFragmentDecompilerThread::insertGlobalFunctions(std::stringstream &OS) OS << "#define texture_base_index _fs_texture_base_index\n" - "#define TEX_PARAM(index) texture_parameters[index + texture_base_index]\n\n"; + "#define TEX_PARAM(index) texture_parameters_##index\n\n"; glsl::insert_glsl_legacy_function(OS, m_shader_props); } @@ -433,6 +433,16 @@ void VKFragmentDecompilerThread::insertMainStart(std::stringstream & OS) if (properties.in_register_mask & in_spec_color) OS << " vec4 spec_color = gl_FrontFacing ? spec_color1 : spec_color0;\n"; } + + for (u16 i = 0, mask = (properties.common_access_sampler_mask | properties.shadow_sampler_mask); mask != 0; ++i, mask >>= 1) + { + if (!(mask & 1)) + { + continue; + } + + OS << " const sampler_info texture_parameters_" << i << " = texture_parameters[texture_base_index + " << i << "];\n"; + } } void VKFragmentDecompilerThread::insertMainEnd(std::stringstream & OS) From 20dcfa5c8a56882669aef6333a5d0b3ee086465c Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 14 Dec 2025 07:26:30 +0300 Subject: [PATCH 18/59] rsx: Move heavy codegen capabilities to permutation flags from runtime checks --- rpcs3/Emu/RSX/Core/RSXDrawCommands.cpp | 68 ------------------- rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp | 12 +++- rpcs3/Emu/RSX/Program/GLSLCommon.cpp | 30 ++++++-- .../RSXProg/RSXFragmentPrologue.glsl | 4 +- .../RSXProg/RSXFragmentTextureOps.glsl | 2 + .../GLSLSnippets/RSXProg/RSXROPEpilogue.glsl | 42 +++++------- rpcs3/Emu/RSX/Program/GLSLTypes.h | 9 ++- rpcs3/Emu/RSX/RSXThread.cpp | 39 +++++++++++ rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp | 12 +++- rpcs3/Emu/RSX/gcm_enums.h | 14 +++- 10 files changed, 123 insertions(+), 109 deletions(-) diff --git a/rpcs3/Emu/RSX/Core/RSXDrawCommands.cpp b/rpcs3/Emu/RSX/Core/RSXDrawCommands.cpp index 449900cef1..ed2447ab24 100644 --- a/rpcs3/Emu/RSX/Core/RSXDrawCommands.cpp +++ b/rpcs3/Emu/RSX/Core/RSXDrawCommands.cpp @@ -687,73 +687,6 @@ namespace rsx rop_control.enable_alpha_test(); } - if (REGS(m_ctx)->polygon_stipple_enabled()) - { - rop_control.enable_polygon_stipple(); - } - - auto can_use_hw_a2c = [&]() -> bool - { - const auto& config = RSX(m_ctx)->get_backend_config(); - if (!config.supports_hw_a2c) - { - return false; - } - - if (config.supports_hw_a2c_1spp) - { - return true; - } - - return REGS(m_ctx)->surface_antialias() != rsx::surface_antialiasing::center_1_sample; - }; - - if (REGS(m_ctx)->msaa_alpha_to_coverage_enabled() && !can_use_hw_a2c()) - { - // TODO: Properly support alpha-to-coverage and alpha-to-one behavior in shaders - // Alpha values generate a coverage mask for order independent blending - // Requires hardware AA to work properly (or just fragment sample stage in fragment shaders) - // Simulated using combined alpha blend and alpha test - rop_control.enable_alpha_to_coverage(); - if (REGS(m_ctx)->msaa_sample_mask()) - { - rop_control.enable_MSAA_writes(); - } - - // Sample configuration bits - switch (REGS(m_ctx)->surface_antialias()) - { - case rsx::surface_antialiasing::center_1_sample: - break; - case rsx::surface_antialiasing::diagonal_centered_2_samples: - rop_control.set_msaa_control(1u); - break; - default: - rop_control.set_msaa_control(3u); - break; - } - } - - // Check if framebuffer is actually an XRGB format and not a WZYX format - switch (REGS(m_ctx)->surface_color()) - { - case rsx::surface_color_format::w16z16y16x16: - case rsx::surface_color_format::w32z32y32x32: - case rsx::surface_color_format::x32: - // These behave very differently from "normal" formats. - break; - default: - // Integer framebuffer formats. - rop_control.enable_framebuffer_INT(); - - // Check if we want sRGB conversion. - if (REGS(m_ctx)->framebuffer_srgb_enabled()) - { - rop_control.enable_framebuffer_sRGB(); - } - break; - } - // Generate wpos coefficients // wpos equation is now as follows (ignoring pixel center offset): // wpos.y = (frag_coord / resolution_scale) * ((window_origin!=top)?-1.: 1.) + ((window_origin!=top)? window_height : 0) @@ -766,7 +699,6 @@ namespace rsx payload.rop_control = rop_control.value; payload.alpha_ref = REGS(m_ctx)->alpha_ref(); - const auto window_origin = REGS(m_ctx)->shader_window_origin(); const u32 window_height = REGS(m_ctx)->shader_window_height(); const auto pixel_center = REGS(m_ctx)->pixel_center(); diff --git a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp index cc28c0c98f..047b362bae 100644 --- a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp @@ -219,16 +219,24 @@ void GLFragmentDecompilerThread::insertGlobalFunctions(std::stringstream &OS) m_shader_props.require_srgb_to_linear = properties.has_upg; m_shader_props.require_linear_to_srgb = properties.has_pkg; m_shader_props.require_fog_read = properties.in_register_mask & in_fogc; - m_shader_props.emulate_coverage_tests = !rsx::get_renderer_backend_config().supports_hw_a2c_1spp; m_shader_props.emulate_shadow_compare = device_props.emulate_depth_compare; + m_shader_props.low_precision_tests = ::gl::get_driver_caps().vendor_NVIDIA && !(m_prog.ctrl & RSX_SHADER_CONTROL_ATTRIBUTE_INTERPOLATION); m_shader_props.disable_early_discard = !::gl::get_driver_caps().vendor_NVIDIA; m_shader_props.supports_native_fp16 = device_props.has_native_half_support; - m_shader_props.ROP_output_rounding = g_cfg.video.shader_precision != gpu_preset_level::low; + + m_shader_props.ROP_output_rounding = (g_cfg.video.shader_precision != gpu_preset_level::low) && !!(m_prog.ctrl & RSX_SHADER_CONTROL_8BIT_FRAMEBUFFER); + m_shader_props.ROP_sRGB_packing = !!(m_prog.ctrl & RSX_SHADER_CONTROL_SRGB_FRAMEBUFFER); + m_shader_props.ROP_alpha_test = !!(m_prog.ctrl & RSX_SHADER_CONTROL_ALPHA_TEST); + m_shader_props.ROP_alpha_to_coverage_test = !!(m_prog.ctrl & RSX_SHADER_CONTROL_ALPHA_TO_COVERAGE); + m_shader_props.ROP_polygon_stipple_test = !!(m_prog.ctrl & RSX_SHADER_CONTROL_POLYGON_STIPPLE); + m_shader_props.ROP_discard = !!(m_prog.ctrl & RSX_SHADER_CONTROL_USES_KIL); + m_shader_props.require_tex1D_ops = properties.has_tex1D; m_shader_props.require_tex2D_ops = properties.has_tex2D; m_shader_props.require_tex3D_ops = properties.has_tex3D; m_shader_props.require_shadowProj_ops = properties.shadow_sampler_mask != 0 && properties.has_texShadowProj; + m_shader_props.require_alpha_kill = !!(m_prog.ctrl & RSX_SHADER_CONTROL_TEXTURE_ALPHA_KILL); glsl::insert_glsl_legacy_function(OS, m_shader_props); } diff --git a/rpcs3/Emu/RSX/Program/GLSLCommon.cpp b/rpcs3/Emu/RSX/Program/GLSLCommon.cpp index afebcce7ed..1421b179fc 100644 --- a/rpcs3/Emu/RSX/Program/GLSLCommon.cpp +++ b/rpcs3/Emu/RSX/Program/GLSLCommon.cpp @@ -216,12 +216,12 @@ namespace glsl enabled_options.push_back("_32_BIT_OUTPUT"); } - if (!props.fp32_outputs) + if (props.ROP_sRGB_packing) { enabled_options.push_back("_ENABLE_FRAMEBUFFER_SRGB"); } - if (props.disable_early_discard) + if (props.disable_early_discard && props.ROP_discard) { enabled_options.push_back("_DISABLE_EARLY_DISCARD"); } @@ -231,7 +231,15 @@ namespace glsl enabled_options.push_back("_ENABLE_ROP_OUTPUT_ROUNDING"); } - enabled_options.push_back("_ENABLE_POLYGON_STIPPLE"); + if (props.ROP_alpha_test) + { + enabled_options.push_back("_ENABLE_ALPHA_TEST"); + } + + if (props.ROP_polygon_stipple_test) + { + enabled_options.push_back("_ENABLE_POLYGON_STIPPLE"); + } } // Import common header @@ -276,12 +284,12 @@ namespace glsl return; } - if (props.emulate_coverage_tests) + if (props.ROP_alpha_to_coverage_test) { - enabled_options.push_back("_EMULATE_COVERAGE_TEST"); + enabled_options.push_back("_ENABLE_ALPHA_TO_COVERAGE_TEST"); } - if (!props.fp32_outputs || props.require_linear_to_srgb) + if (props.ROP_sRGB_packing || props.require_linear_to_srgb) { enabled_options.push_back("_ENABLE_LINEAR_TO_SRGB"); } @@ -296,6 +304,11 @@ namespace glsl enabled_options.push_back("_ENABLE_WPOS"); } + if (props.ROP_alpha_test || (props.require_msaa_ops && props.require_tex_shadow_ops)) + { + enabled_options.push_back("_ENABLE_COMPARISON_FUNC"); + } + if (props.require_fog_read) { program_common::define_glsl_constants(OS, @@ -385,6 +398,11 @@ namespace glsl enabled_options.push_back("_ENABLE_SHADOWPROJ"); } + if (props.require_alpha_kill) + { + enabled_options.push_back("_ENABLE_TEXTURE_ALPHA_KILL"); + } + program_common::define_glsl_switches(OS, enabled_options); enabled_options.clear(); diff --git a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentPrologue.glsl b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentPrologue.glsl index e40373f64c..2fe1277ea9 100644 --- a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentPrologue.glsl +++ b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentPrologue.glsl @@ -81,7 +81,7 @@ vec4 fetch_fog_value(const in uint mode) } #endif -#ifdef _EMULATE_COVERAGE_TEST +#ifdef _ENABLE_ALPHA_TO_COVERAGE_TEST // Purely stochastic bool coverage_test_passes(const in vec4 _sample) { @@ -109,6 +109,7 @@ vec4 srgb_to_linear(const in vec4 cs) } #endif +#ifdef _ENABLE_COMPARISON_FUNC // Required by all fragment shaders for alpha test bool comparison_passes(const in float a, const in float b, const in uint func) { @@ -125,5 +126,6 @@ bool comparison_passes(const in float a, const in float b, const in uint func) case 7: return true; //always } } +#endif )" diff --git a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureOps.glsl b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureOps.glsl index e231333184..f0457f0034 100644 --- a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureOps.glsl +++ b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureOps.glsl @@ -188,6 +188,7 @@ vec4 _process_texel(in vec4 rgba, const in uint control_bits) return rgba; } +#ifdef _ENABLE_TEXTURE_ALPHA_KILL if (_test_bit(control_bits, ALPHAKILL)) { // Alphakill @@ -197,6 +198,7 @@ vec4 _process_texel(in vec4 rgba, const in uint control_bits) return rgba; } } +#endif if (_test_bit(control_bits, RENORMALIZE)) { diff --git a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPEpilogue.glsl b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPEpilogue.glsl index aaaaa559ca..283371d0ed 100644 --- a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPEpilogue.glsl +++ b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPEpilogue.glsl @@ -8,43 +8,33 @@ R"( #endif #ifdef _ENABLE_FRAMEBUFFER_SRGB - if (_test_bit(rop_control, SRGB_FRAMEBUFFER_BIT)) - { - col0.rgb = _mrt_color_t(linear_to_srgb(col0)).rgb; - col1.rgb = _mrt_color_t(linear_to_srgb(col1)).rgb; - col2.rgb = _mrt_color_t(linear_to_srgb(col2)).rgb; - col3.rgb = _mrt_color_t(linear_to_srgb(col3)).rgb; - } + col0.rgb = _mrt_color_t(linear_to_srgb(col0)).rgb; + col1.rgb = _mrt_color_t(linear_to_srgb(col1)).rgb; + col2.rgb = _mrt_color_t(linear_to_srgb(col2)).rgb; + col3.rgb = _mrt_color_t(linear_to_srgb(col3)).rgb; #endif #ifdef _ENABLE_ROP_OUTPUT_ROUNDING - if (_test_bit(rop_control, INT_FRAMEBUFFER_BIT)) - { - col0 = round_to_8bit(col0); - col1 = round_to_8bit(col1); - col2 = round_to_8bit(col2); - col3 = round_to_8bit(col3); - } + col0 = round_to_8bit(col0); + col1 = round_to_8bit(col1); + col2 = round_to_8bit(col2); + col3 = round_to_8bit(col3); #endif // Post-output stages // Alpha Testing - if (_test_bit(rop_control, ALPHA_TEST_ENABLE_BIT)) +#ifdef _ENABLE_ALPHA_TEST + const uint alpha_func = _get_bits(rop_control, ALPHA_TEST_FUNC_OFFSET, ALPHA_TEST_FUNC_LENGTH); + if (!comparison_passes(col0.a, alpha_ref, alpha_func)) { - const uint alpha_func = _get_bits(rop_control, ALPHA_TEST_FUNC_OFFSET, ALPHA_TEST_FUNC_LENGTH); - if (!comparison_passes(col0.a, alpha_ref, alpha_func)) - { - discard; - } + discard; } +#endif -#ifdef _EMULATE_COVERAGE_TEST - if (_test_bit(rop_control, ALPHA_TO_COVERAGE_ENABLE_BIT)) +#ifdef _ENABLE_ALPHA_TO_COVERAGE_TEST + if (!_test_bit(rop_control, MSAA_WRITE_ENABLE_BIT) || !coverage_test_passes(col0)) { - if (!_test_bit(rop_control, MSAA_WRITE_ENABLE_BIT) || !coverage_test_passes(col0)) - { - discard; - } + discard; } #endif diff --git a/rpcs3/Emu/RSX/Program/GLSLTypes.h b/rpcs3/Emu/RSX/Program/GLSLTypes.h index 4716c845d2..5b1b61396e 100644 --- a/rpcs3/Emu/RSX/Program/GLSLTypes.h +++ b/rpcs3/Emu/RSX/Program/GLSLTypes.h @@ -36,12 +36,18 @@ namespace glsl bool require_srgb_to_linear : 1; bool require_linear_to_srgb : 1; bool require_fog_read : 1; - bool emulate_coverage_tests : 1; bool emulate_shadow_compare : 1; bool low_precision_tests : 1; bool disable_early_discard : 1; bool supports_native_fp16 : 1; + + // ROP control flags bool ROP_output_rounding : 1; + bool ROP_sRGB_packing : 1; + bool ROP_alpha_test : 1; + bool ROP_alpha_to_coverage_test : 1; + bool ROP_polygon_stipple_test : 1; + bool ROP_discard : 1; // Texturing spec bool require_texture_ops : 1; // Global switch to enable/disable all texture code @@ -53,5 +59,6 @@ namespace glsl bool require_tex2D_ops : 1; // Include 2D texture stuff bool require_tex3D_ops : 1; // Include 3D texture stuff (including cubemap) bool require_shadowProj_ops : 1; // Include shadow2DProj projection textures (1D is unsupported anyway) + bool require_alpha_kill : 1; // Include alpha kill checking code }; }; diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp index 73c86907f1..dc4f01f010 100644 --- a/rpcs3/Emu/RSX/RSXThread.cpp +++ b/rpcs3/Emu/RSX/RSXThread.cpp @@ -2063,6 +2063,26 @@ namespace rsx { current_fragment_program.ctrl |= RSX_SHADER_CONTROL_ATTRIBUTE_INTERPOLATION; } + + if (method_registers.alpha_test_enabled()) + { + current_fragment_program.ctrl |= RSX_SHADER_CONTROL_ALPHA_TEST; + } + + if (method_registers.polygon_stipple_enabled()) + { + current_fragment_program.ctrl |= RSX_SHADER_CONTROL_POLYGON_STIPPLE; + } + + if (method_registers.msaa_alpha_to_coverage_enabled()) + { + const bool is_multiple_samples = method_registers.surface_antialias() != rsx::surface_antialiasing::center_1_sample; + if (!backend_config.supports_hw_a2c || (!is_multiple_samples && !backend_config.supports_hw_a2c_1spp)) + { + // Emulation required + current_fragment_program.ctrl |= RSX_SHADER_CONTROL_ALPHA_TO_COVERAGE; + } + } } else if (method_registers.point_sprite_enabled() && method_registers.current_draw_clause.primitive == primitive_type::points) @@ -2071,6 +2091,24 @@ namespace rsx current_fragment_program.texcoord_control_mask |= u32(method_registers.point_sprite_control_mask()) << 16; } + // Check if framebuffer is actually an XRGB format and not a WZYX format + switch (method_registers.surface_color()) + { + case rsx::surface_color_format::w16z16y16x16: + case rsx::surface_color_format::w32z32y32x32: + case rsx::surface_color_format::x32: + // These behave very differently from "normal" formats. + break; + default: + // Integer framebuffer formats. These can support sRGB output as well as some special rules for output quantization. + current_fragment_program.ctrl |= RSX_SHADER_CONTROL_8BIT_FRAMEBUFFER; + if (method_registers.framebuffer_srgb_enabled()) + { + current_fragment_program.ctrl |= RSX_SHADER_CONTROL_SRGB_FRAMEBUFFER; + } + break; + } + for (u32 textures_ref = current_fp_metadata.referenced_textures_mask, i = 0; textures_ref; textures_ref >>= 1, ++i) { if (!(textures_ref & 1)) continue; @@ -2098,6 +2136,7 @@ namespace rsx { //alphakill can be ignored unless a valid comparison function is set texture_control |= (1 << texture_control_bits::ALPHAKILL); + current_fragment_program.ctrl |= RSX_SHADER_CONTROL_TEXTURE_ALPHA_KILL; } //const u32 texaddr = rsx::get_address(tex.offset(), tex.location()); diff --git a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp index c720ae8550..ab06fbdade 100644 --- a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp @@ -318,16 +318,24 @@ void VKFragmentDecompilerThread::insertGlobalFunctions(std::stringstream &OS) m_shader_props.require_srgb_to_linear = properties.has_upg; m_shader_props.require_linear_to_srgb = properties.has_pkg; m_shader_props.require_fog_read = properties.in_register_mask & in_fogc; - m_shader_props.emulate_coverage_tests = g_cfg.video.antialiasing_level == msaa_level::none; m_shader_props.emulate_shadow_compare = device_props.emulate_depth_compare; + m_shader_props.low_precision_tests = device_props.has_low_precision_rounding && !(m_prog.ctrl & RSX_SHADER_CONTROL_ATTRIBUTE_INTERPOLATION); m_shader_props.disable_early_discard = !vk::is_NVIDIA(vk::get_driver_vendor()); m_shader_props.supports_native_fp16 = device_props.has_native_half_support; - m_shader_props.ROP_output_rounding = g_cfg.video.shader_precision != gpu_preset_level::low; + + m_shader_props.ROP_output_rounding = (g_cfg.video.shader_precision != gpu_preset_level::low) && !!(m_prog.ctrl & RSX_SHADER_CONTROL_8BIT_FRAMEBUFFER); + m_shader_props.ROP_sRGB_packing = !!(m_prog.ctrl & RSX_SHADER_CONTROL_SRGB_FRAMEBUFFER); + m_shader_props.ROP_alpha_test = !!(m_prog.ctrl & RSX_SHADER_CONTROL_ALPHA_TEST); + m_shader_props.ROP_alpha_to_coverage_test = !!(m_prog.ctrl & RSX_SHADER_CONTROL_ALPHA_TO_COVERAGE); + m_shader_props.ROP_polygon_stipple_test = !!(m_prog.ctrl & RSX_SHADER_CONTROL_POLYGON_STIPPLE); + m_shader_props.ROP_discard = !!(m_prog.ctrl & RSX_SHADER_CONTROL_USES_KIL); + m_shader_props.require_tex1D_ops = properties.has_tex1D; m_shader_props.require_tex2D_ops = properties.has_tex2D; m_shader_props.require_tex3D_ops = properties.has_tex3D; m_shader_props.require_shadowProj_ops = properties.shadow_sampler_mask != 0 && properties.has_texShadowProj; + m_shader_props.require_alpha_kill = !!(m_prog.ctrl & RSX_SHADER_CONTROL_TEXTURE_ALPHA_KILL); // Declare global constants if (m_shader_props.require_fog_read) diff --git a/rpcs3/Emu/RSX/gcm_enums.h b/rpcs3/Emu/RSX/gcm_enums.h index 61b51c3857..7fd9437f66 100644 --- a/rpcs3/Emu/RSX/gcm_enums.h +++ b/rpcs3/Emu/RSX/gcm_enums.h @@ -454,9 +454,17 @@ namespace gcm RSX_SHADER_CONTROL_UNKNOWN1 = 0x8000, // seemingly set when srgb packer is used?? // Custom - RSX_SHADER_CONTROL_ATTRIBUTE_INTERPOLATION = 0x10000, // Rasterizing triangles and not lines or points - RSX_SHADER_CONTROL_INSTANCED_CONSTANTS = 0x20000, // Support instance ID offsets when loading constants - RSX_SHADER_CONTROL_INTERPRETER_MODEL = 0x40000, // Compile internals expecting interpreter + RSX_SHADER_CONTROL_ATTRIBUTE_INTERPOLATION = 0x0010000, // Rasterizing triangles and not lines or points + RSX_SHADER_CONTROL_INSTANCED_CONSTANTS = 0x0020000, // Support instance ID offsets when loading constants + RSX_SHADER_CONTROL_INTERPRETER_MODEL = 0x0040000, // Compile internals expecting interpreter + + RSX_SHADER_CONTROL_8BIT_FRAMEBUFFER = 0x0080000, // Quantize outputs to 8-bit FBO + RSX_SHADER_CONTROL_SRGB_FRAMEBUFFER = 0x0100000, // Outputs are SRGB. We could reuse UNKNOWN1 but we just keep the namespaces separate. + + RSX_SHADER_CONTROL_TEXTURE_ALPHA_KILL = 0x0200000, // Uses alpha kill on texture input + RSX_SHADER_CONTROL_ALPHA_TEST = 0x0400000, // Uses alpha test on the outputs + RSX_SHADER_CONTROL_POLYGON_STIPPLE = 0x0800000, // Uses polygon stipple for dithered rendering + RSX_SHADER_CONTROL_ALPHA_TO_COVERAGE = 0x1000000, // Alpha to coverage }; // GCM Reports From 1a733353dee7c758796013c554707e0327e1219b Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 14 Dec 2025 15:23:56 +0300 Subject: [PATCH 19/59] rsx: Fix MSAA sampling operations --- .../RSXProg/RSXFragmentTextureMSAAOps.glsl | 6 ++--- .../RSXFragmentTextureMSAAOpsInternal.glsl | 24 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOps.glsl b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOps.glsl index 965eac5276..00e81c39c5 100644 --- a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOps.glsl +++ b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOps.glsl @@ -1,8 +1,8 @@ R"( #define ZCOMPARE_FUNC(index) _get_bits(TEX_FLAGS(index), DEPTH_COMPARE, 3) -#define ZS_READ_MS(index, coord) vec2(sampleTexture2DMS(TEX_NAME(index), coord, index).r, float(sampleTexture2DMS(TEX_NAME_STENCIL(index), coord, index).x)) -#define TEX2D_MS(index, coord2) _process_texel(sampleTexture2DMS(TEX_NAME(index), coord2, index), TEX_FLAGS(index)) -#define TEX2D_SHADOW_MS(index, coord3) vec4(comparison_passes(sampleTexture2DMS(TEX_NAME(index), coord3.xy, index).x, coord3.z, ZCOMPARE_FUNC(index))) +#define ZS_READ_MS(index, coord) vec2(sampleTexture2DMS(TEX_NAME(index), coord, TEX_PARAM(index)).r, float(sampleTexture2DMS(TEX_NAME_STENCIL(index), coord, TEX_PARAM(index)).x)) +#define TEX2D_MS(index, coord2) _process_texel(sampleTexture2DMS(TEX_NAME(index), coord2, TEX_PARAM(index)), TEX_FLAGS(index)) +#define TEX2D_SHADOW_MS(index, coord3) vec4(comparison_passes(sampleTexture2DMS(TEX_NAME(index), coord3.xy, TEX_PARAM(index)).x, coord3.z, ZCOMPARE_FUNC(index))) #define TEX2D_SHADOWPROJ_MS(index, coord4) TEX2D_SHADOW_MS(index, (coord4.xyz / coord4.w)) #define TEX2D_Z24X8_RGBA8_MS(index, coord2) _process_texel(convert_z24x8_to_rgba8(ZS_READ_MS(index, coord2), TEX_PARAM(index).remap, TEX_FLAGS(index)), TEX_FLAGS(index)) diff --git a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOpsInternal.glsl b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOpsInternal.glsl index 0bf734ab35..5bf5ab9f9a 100644 --- a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOpsInternal.glsl +++ b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXFragmentTextureMSAAOpsInternal.glsl @@ -1,5 +1,5 @@ R"( -vec4 texelFetch2DMS(in _MSAA_SAMPLER_TYPE_ tex, const in vec2 sample_count, const in ivec2 icoords, const in int index, const in ivec2 offset) +vec4 texelFetch2DMS(in _MSAA_SAMPLER_TYPE_ tex, const in vec2 sample_count, const in ivec2 icoords, const in ivec2 offset) { const vec2 resolve_coords = vec2(icoords + offset); const vec2 aa_coords = floor(resolve_coords / sample_count); // AA coords = real_coords / sample_count @@ -8,15 +8,15 @@ vec4 texelFetch2DMS(in _MSAA_SAMPLER_TYPE_ tex, const in vec2 sample_count, cons return texelFetch(tex, ivec2(aa_coords), int(sample_index)); } -vec4 sampleTexture2DMS(in _MSAA_SAMPLER_TYPE_ tex, const in vec2 coords, const in int index) +vec4 sampleTexture2DMS(in _MSAA_SAMPLER_TYPE_ tex, const in vec2 coords, const in sampler_info tex_params) { - const uint flags = TEX_FLAGS(index); - const vec2 scaled_coords = COORD_SCALE2(index, coords); + const uint flags = tex_params.flags; + const vec2 scaled_coords = _texcoord_xform(coords, tex_params); const vec2 normalized_coords = texture2DMSCoord(scaled_coords, flags); const vec2 sample_count = vec2(2., textureSamples(tex) * 0.5); const vec2 image_size = textureSize(tex) * sample_count; const ivec2 icoords = ivec2(normalized_coords * image_size); - const vec4 sample0 = texelFetch2DMS(tex, sample_count, icoords, index, ivec2(0)); + const vec4 sample0 = texelFetch2DMS(tex, sample_count, icoords, ivec2(0)); if (_get_bits(flags, FILTERED_MAG_BIT, 2) == 0) { @@ -35,7 +35,7 @@ vec4 sampleTexture2DMS(in _MSAA_SAMPLER_TYPE_ tex, const in vec2 coords, const i vec4 a, b; float factor; - const vec4 sample2 = texelFetch2DMS(tex, sample_count, icoords, index, ivec2(0, 1)); // Top left + const vec4 sample2 = texelFetch2DMS(tex, sample_count, icoords, ivec2(0, 1)); // Top left if (no_filter.x) { @@ -46,21 +46,21 @@ vec4 sampleTexture2DMS(in _MSAA_SAMPLER_TYPE_ tex, const in vec2 coords, const i else { // Filter required, sample more data - const vec4 sample1 = texelFetch2DMS(tex, sample_count, icoords, index, ivec2(1, 0)); // Bottom right - const vec4 sample3 = texelFetch2DMS(tex, sample_count, icoords, index, ivec2(1, 1)); // Top right + const vec4 sample1 = texelFetch2DMS(tex, sample_count, icoords, ivec2(1, 0)); // Bottom right + const vec4 sample3 = texelFetch2DMS(tex, sample_count, icoords, ivec2(1, 1)); // Top right if (actual_step.x > uv_step.x) { // Downscale in X, centered const vec3 weights = compute2x2DownsampleWeights(normalized_coords.x, uv_step.x, actual_step.x); - const vec4 sample4 = texelFetch2DMS(tex, sample_count, icoords, index, ivec2(2, 0)); // Further bottom right - a = fma(sample0, weights.xxxx, sample1 * weights.y) + (sample4 * weights.z); // Weighted sum + const vec4 sample4 = texelFetch2DMS(tex, sample_count, icoords, ivec2(2, 0)); // Further bottom right + a = fma(sample0, weights.xxxx, sample1 * weights.y) + (sample4 * weights.z); // Weighted sum if (!no_filter.y) { - const vec4 sample5 = texelFetch2DMS(tex, sample_count, icoords, index, ivec2(2, 1)); // Further top right - b = fma(sample2, weights.xxxx, sample3 * weights.y) + (sample5 * weights.z); // Weighted sum + const vec4 sample5 = texelFetch2DMS(tex, sample_count, icoords, ivec2(2, 1)); // Further top right + b = fma(sample2, weights.xxxx, sample3 * weights.y) + (sample5 * weights.z); // Weighted sum } } else if (actual_step.x < uv_step.x) From 8030b01064113c9c9a41d7946f4e1d0d3ee1b6e5 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 14 Dec 2025 15:24:32 +0300 Subject: [PATCH 20/59] rsx: Propagate shader kill flag and refactor texture flags processing a bit --- rpcs3/Emu/RSX/RSXThread.cpp | 378 ++++++++++++++++++------------------ 1 file changed, 190 insertions(+), 188 deletions(-) diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp index dc4f01f010..dd272d5f84 100644 --- a/rpcs3/Emu/RSX/RSXThread.cpp +++ b/rpcs3/Emu/RSX/RSXThread.cpp @@ -2052,7 +2052,7 @@ namespace rsx m_graphics_state.clear(rsx::pipeline_state::fragment_program_dirty); - current_fragment_program.ctrl = m_ctx->register_state->shader_control() & (CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS | CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT); + current_fragment_program.ctrl = m_ctx->register_state->shader_control() & (CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS | CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_USES_KIL); current_fragment_program.texcoord_control_mask = m_ctx->register_state->texcoord_control_mask(); current_fragment_program.two_sided_lighting = m_ctx->register_state->two_side_light_en(); current_fragment_program.mrt_buffers_count = rsx::utility::get_mrt_buffers_count(m_ctx->register_state->surface_color_target()); @@ -2116,195 +2116,197 @@ namespace rsx auto &tex = rsx::method_registers.fragment_textures[i]; current_fp_texture_state.clear(i); - if (tex.enabled() && sampler_descriptors[i]->format_class != RSX_FORMAT_CLASS_UNDEFINED) + if (!tex.enabled() || sampler_descriptors[i]->format_class == RSX_FORMAT_CLASS_UNDEFINED) { - std::memcpy(current_fragment_program.texture_params[i].scale, sampler_descriptors[i]->texcoord_xform.scale, 6 * sizeof(f32)); - current_fragment_program.texture_params[i].remap = tex.remap(); - - m_graphics_state |= rsx::pipeline_state::fragment_texture_state_dirty; - - u32 texture_control = 0; - current_fp_texture_state.set_dimension(sampler_descriptors[i]->image_type, i); - - if (sampler_descriptors[i]->texcoord_xform.clamp) - { - std::memcpy(current_fragment_program.texture_params[i].clamp_min, sampler_descriptors[i]->texcoord_xform.clamp_min, 4 * sizeof(f32)); - texture_control |= (1 << rsx::texture_control_bits::CLAMP_TEXCOORDS_BIT); - } - - if (tex.alpha_kill_enabled()) - { - //alphakill can be ignored unless a valid comparison function is set - texture_control |= (1 << texture_control_bits::ALPHAKILL); - current_fragment_program.ctrl |= RSX_SHADER_CONTROL_TEXTURE_ALPHA_KILL; - } - - //const u32 texaddr = rsx::get_address(tex.offset(), tex.location()); - const u32 raw_format = tex.format(); - const u32 format = raw_format & ~(CELL_GCM_TEXTURE_LN | CELL_GCM_TEXTURE_UN); - - if (raw_format & CELL_GCM_TEXTURE_UN) - { - if (tex.min_filter() == rsx::texture_minify_filter::nearest || - tex.mag_filter() == rsx::texture_magnify_filter::nearest) - { - // Subpixel offset so that (X + bias) * scale will round correctly. - // This is done to work around fdiv precision issues in some GPUs (NVIDIA) - // We apply the simplification where (x + bias) * z = xz + zbias here. - constexpr auto subpixel_bias = 0.01f; - current_fragment_program.texture_params[i].bias[0] += (subpixel_bias * current_fragment_program.texture_params[i].scale[0]); - current_fragment_program.texture_params[i].bias[1] += (subpixel_bias * current_fragment_program.texture_params[i].scale[1]); - current_fragment_program.texture_params[i].bias[2] += (subpixel_bias * current_fragment_program.texture_params[i].scale[2]); - } - } - - if (backend_config.supports_hw_msaa && sampler_descriptors[i]->samples > 1) - { - current_fp_texture_state.multisampled_textures |= (1 << i); - texture_control |= (static_cast(tex.zfunc()) << texture_control_bits::DEPTH_COMPARE_OP); - texture_control |= (static_cast(tex.mag_filter() != rsx::texture_magnify_filter::nearest) << texture_control_bits::FILTERED_MAG); - texture_control |= (static_cast(tex.min_filter() != rsx::texture_minify_filter::nearest) << texture_control_bits::FILTERED_MIN); - texture_control |= (((tex.format() & CELL_GCM_TEXTURE_UN) >> 6) << texture_control_bits::UNNORMALIZED_COORDS); - - if (rsx::is_texcoord_wrapping_mode(tex.wrap_s())) - { - texture_control |= (1 << texture_control_bits::WRAP_S); - } - - if (rsx::is_texcoord_wrapping_mode(tex.wrap_t())) - { - texture_control |= (1 << texture_control_bits::WRAP_T); - } - - if (rsx::is_texcoord_wrapping_mode(tex.wrap_r())) - { - texture_control |= (1 << texture_control_bits::WRAP_R); - } - } - - if (sampler_descriptors[i]->format_class != RSX_FORMAT_CLASS_COLOR) - { - switch (sampler_descriptors[i]->format_class) - { - case RSX_FORMAT_CLASS_DEPTH16_FLOAT: - case RSX_FORMAT_CLASS_DEPTH24_FLOAT_X8_PACK32: - texture_control |= (1 << texture_control_bits::DEPTH_FLOAT); - break; - default: - break; - } - - switch (format) - { - case CELL_GCM_TEXTURE_A8R8G8B8: - case CELL_GCM_TEXTURE_D8R8G8B8: - { - // Emulate bitcast in shader - current_fp_texture_state.redirected_textures |= (1 << i); - const auto float_en = (sampler_descriptors[i]->format_class == RSX_FORMAT_CLASS_DEPTH24_FLOAT_X8_PACK32)? 1 : 0; - texture_control |= (float_en << texture_control_bits::DEPTH_FLOAT); - break; - } - case CELL_GCM_TEXTURE_X16: - { - // A simple way to quickly read DEPTH16 data without shadow comparison - break; - } - case CELL_GCM_TEXTURE_DEPTH16: - case CELL_GCM_TEXTURE_DEPTH24_D8: - case CELL_GCM_TEXTURE_DEPTH16_FLOAT: - case CELL_GCM_TEXTURE_DEPTH24_D8_FLOAT: - { - // Natively supported Z formats with shadow comparison feature - const auto compare_mode = tex.zfunc(); - if (!tex.alpha_kill_enabled() && - compare_mode < rsx::comparison_function::always && - compare_mode > rsx::comparison_function::never) - { - current_fp_texture_state.shadow_textures |= (1 << i); - } - break; - } - default: - rsx_log.error("Depth texture bound to pipeline with unexpected format 0x%X", format); - } - } - else if (!backend_config.supports_hw_renormalization /* && - tex.min_filter() == rsx::texture_minify_filter::nearest && - tex.mag_filter() == rsx::texture_magnify_filter::nearest*/) - { - // FIXME: This check should only apply to point-sampled textures. However, it severely regresses some games (id tech 5). - // This is because even when filtering is active, the error from the PS3 texture expansion still applies. - // A proper fix is to expand these formats into BGRA8 when high texture precision is required. That requires different GUI settings and inflation shaders, so it will be handled separately. - - switch (format) - { - case CELL_GCM_TEXTURE_A1R5G5B5: - case CELL_GCM_TEXTURE_A4R4G4B4: - case CELL_GCM_TEXTURE_D1R5G5B5: - case CELL_GCM_TEXTURE_R5G5B5A1: - case CELL_GCM_TEXTURE_R5G6B5: - case CELL_GCM_TEXTURE_R6G5B5: - texture_control |= (1 << texture_control_bits::RENORMALIZE); - break; - default: - break; - } - } - - if (rsx::is_int8_remapped_format(format)) - { - // Special operations applied to 8-bit formats such as gamma correction and sign conversion - // NOTE: The unsigned_remap=bias flag being set flags the texture as being compressed normal (2n-1 / BX2) (UE3) - // NOTE: The ARGB8_signed flag means to reinterpret the raw bytes as signed. This is different than unsigned_remap=bias which does range decompression. - // This is a separate method of setting the format to signed mode without doing so per-channel - // Precedence = SNORM > GAMMA > UNSIGNED_REMAP (See Resistance 3 for GAMMA/BX2 relationship, UE3 for BX2 effect) - - const u32 argb8_signed = tex.argb_signed(); // _SNROM - const u32 gamma = tex.gamma() & ~argb8_signed; // _SRGB - const u32 unsigned_remap = (tex.unsigned_remap() == CELL_GCM_TEXTURE_UNSIGNED_REMAP_NORMAL)? 0u : (~(gamma | argb8_signed) & 0xF); // _BX2 - u32 argb8_convert = gamma; - - // The options are mutually exclusive - ensure((argb8_signed & gamma) == 0); - ensure((argb8_signed & unsigned_remap) == 0); - ensure((gamma & unsigned_remap) == 0); - - // Helper function to apply a per-channel mask based on an input mask - const auto apply_sign_convert_mask = [&](u32 mask, u32 bit_offset) - { - // TODO: Use actual remap mask to account for 0 and 1 overrides in default mapping - // TODO: Replace this clusterfuck of texture control with matrix transformation - const auto remap_ctrl = (tex.remap() >> 8) & 0xAA; - if (remap_ctrl == 0xAA) - { - argb8_convert |= (mask & 0xFu) << bit_offset; - return; - } - - if ((remap_ctrl & 0x03) == 0x02) argb8_convert |= (mask & 0x1u) << bit_offset; - if ((remap_ctrl & 0x0C) == 0x08) argb8_convert |= (mask & 0x2u) << bit_offset; - if ((remap_ctrl & 0x30) == 0x20) argb8_convert |= (mask & 0x4u) << bit_offset; - if ((remap_ctrl & 0xC0) == 0x80) argb8_convert |= (mask & 0x8u) << bit_offset; - }; - - if (argb8_signed) - { - // Apply integer sign extension from uint8 to sint8 and renormalize - apply_sign_convert_mask(argb8_signed, texture_control_bits::SEXT_OFFSET); - } - - if (unsigned_remap) - { - // Apply sign expansion, compressed normal-map style (2n - 1) - apply_sign_convert_mask(unsigned_remap, texture_control_bits::EXPAND_OFFSET); - } - - texture_control |= argb8_convert; - } - - current_fragment_program.texture_params[i].control = texture_control; + continue; } + + std::memcpy(current_fragment_program.texture_params[i].scale, sampler_descriptors[i]->texcoord_xform.scale, 6 * sizeof(f32)); + current_fragment_program.texture_params[i].remap = tex.remap(); + + m_graphics_state |= rsx::pipeline_state::fragment_texture_state_dirty; + + u32 texture_control = 0; + current_fp_texture_state.set_dimension(sampler_descriptors[i]->image_type, i); + + if (sampler_descriptors[i]->texcoord_xform.clamp) + { + std::memcpy(current_fragment_program.texture_params[i].clamp_min, sampler_descriptors[i]->texcoord_xform.clamp_min, 4 * sizeof(f32)); + texture_control |= (1 << rsx::texture_control_bits::CLAMP_TEXCOORDS_BIT); + } + + if (tex.alpha_kill_enabled()) + { + //alphakill can be ignored unless a valid comparison function is set + texture_control |= (1 << texture_control_bits::ALPHAKILL); + current_fragment_program.ctrl |= RSX_SHADER_CONTROL_TEXTURE_ALPHA_KILL; + } + + //const u32 texaddr = rsx::get_address(tex.offset(), tex.location()); + const u32 raw_format = tex.format(); + const u32 format = raw_format & ~(CELL_GCM_TEXTURE_LN | CELL_GCM_TEXTURE_UN); + + if (raw_format & CELL_GCM_TEXTURE_UN) + { + if (tex.min_filter() == rsx::texture_minify_filter::nearest || + tex.mag_filter() == rsx::texture_magnify_filter::nearest) + { + // Subpixel offset so that (X + bias) * scale will round correctly. + // This is done to work around fdiv precision issues in some GPUs (NVIDIA) + // We apply the simplification where (x + bias) * z = xz + zbias here. + constexpr auto subpixel_bias = 0.01f; + current_fragment_program.texture_params[i].bias[0] += (subpixel_bias * current_fragment_program.texture_params[i].scale[0]); + current_fragment_program.texture_params[i].bias[1] += (subpixel_bias * current_fragment_program.texture_params[i].scale[1]); + current_fragment_program.texture_params[i].bias[2] += (subpixel_bias * current_fragment_program.texture_params[i].scale[2]); + } + } + + if (backend_config.supports_hw_msaa && sampler_descriptors[i]->samples > 1) + { + current_fp_texture_state.multisampled_textures |= (1 << i); + texture_control |= (static_cast(tex.zfunc()) << texture_control_bits::DEPTH_COMPARE_OP); + texture_control |= (static_cast(tex.mag_filter() != rsx::texture_magnify_filter::nearest) << texture_control_bits::FILTERED_MAG); + texture_control |= (static_cast(tex.min_filter() != rsx::texture_minify_filter::nearest) << texture_control_bits::FILTERED_MIN); + texture_control |= (((tex.format() & CELL_GCM_TEXTURE_UN) >> 6) << texture_control_bits::UNNORMALIZED_COORDS); + + if (rsx::is_texcoord_wrapping_mode(tex.wrap_s())) + { + texture_control |= (1 << texture_control_bits::WRAP_S); + } + + if (rsx::is_texcoord_wrapping_mode(tex.wrap_t())) + { + texture_control |= (1 << texture_control_bits::WRAP_T); + } + + if (rsx::is_texcoord_wrapping_mode(tex.wrap_r())) + { + texture_control |= (1 << texture_control_bits::WRAP_R); + } + } + + if (sampler_descriptors[i]->format_class != RSX_FORMAT_CLASS_COLOR) + { + switch (sampler_descriptors[i]->format_class) + { + case RSX_FORMAT_CLASS_DEPTH16_FLOAT: + case RSX_FORMAT_CLASS_DEPTH24_FLOAT_X8_PACK32: + texture_control |= (1 << texture_control_bits::DEPTH_FLOAT); + break; + default: + break; + } + + switch (format) + { + case CELL_GCM_TEXTURE_A8R8G8B8: + case CELL_GCM_TEXTURE_D8R8G8B8: + { + // Emulate bitcast in shader + current_fp_texture_state.redirected_textures |= (1 << i); + const auto float_en = (sampler_descriptors[i]->format_class == RSX_FORMAT_CLASS_DEPTH24_FLOAT_X8_PACK32)? 1 : 0; + texture_control |= (float_en << texture_control_bits::DEPTH_FLOAT); + break; + } + case CELL_GCM_TEXTURE_X16: + { + // A simple way to quickly read DEPTH16 data without shadow comparison + break; + } + case CELL_GCM_TEXTURE_DEPTH16: + case CELL_GCM_TEXTURE_DEPTH24_D8: + case CELL_GCM_TEXTURE_DEPTH16_FLOAT: + case CELL_GCM_TEXTURE_DEPTH24_D8_FLOAT: + { + // Natively supported Z formats with shadow comparison feature + const auto compare_mode = tex.zfunc(); + if (!tex.alpha_kill_enabled() && + compare_mode < rsx::comparison_function::always && + compare_mode > rsx::comparison_function::never) + { + current_fp_texture_state.shadow_textures |= (1 << i); + } + break; + } + default: + rsx_log.error("Depth texture bound to pipeline with unexpected format 0x%X", format); + } + } + else if (!backend_config.supports_hw_renormalization /* && + tex.min_filter() == rsx::texture_minify_filter::nearest && + tex.mag_filter() == rsx::texture_magnify_filter::nearest*/) + { + // FIXME: This check should only apply to point-sampled textures. However, it severely regresses some games (id tech 5). + // This is because even when filtering is active, the error from the PS3 texture expansion still applies. + // A proper fix is to expand these formats into BGRA8 when high texture precision is required. That requires different GUI settings and inflation shaders, so it will be handled separately. + + switch (format) + { + case CELL_GCM_TEXTURE_A1R5G5B5: + case CELL_GCM_TEXTURE_A4R4G4B4: + case CELL_GCM_TEXTURE_D1R5G5B5: + case CELL_GCM_TEXTURE_R5G5B5A1: + case CELL_GCM_TEXTURE_R5G6B5: + case CELL_GCM_TEXTURE_R6G5B5: + texture_control |= (1 << texture_control_bits::RENORMALIZE); + break; + default: + break; + } + } + + if (rsx::is_int8_remapped_format(format)) + { + // Special operations applied to 8-bit formats such as gamma correction and sign conversion + // NOTE: The unsigned_remap=bias flag being set flags the texture as being compressed normal (2n-1 / BX2) (UE3) + // NOTE: The ARGB8_signed flag means to reinterpret the raw bytes as signed. This is different than unsigned_remap=bias which does range decompression. + // This is a separate method of setting the format to signed mode without doing so per-channel + // Precedence = SNORM > GAMMA > UNSIGNED_REMAP (See Resistance 3 for GAMMA/BX2 relationship, UE3 for BX2 effect) + + const u32 argb8_signed = tex.argb_signed(); // _SNROM + const u32 gamma = tex.gamma() & ~argb8_signed; // _SRGB + const u32 unsigned_remap = (tex.unsigned_remap() == CELL_GCM_TEXTURE_UNSIGNED_REMAP_NORMAL)? 0u : (~(gamma | argb8_signed) & 0xF); // _BX2 + u32 argb8_convert = gamma; + + // The options are mutually exclusive + ensure((argb8_signed & gamma) == 0); + ensure((argb8_signed & unsigned_remap) == 0); + ensure((gamma & unsigned_remap) == 0); + + // Helper function to apply a per-channel mask based on an input mask + const auto apply_sign_convert_mask = [&](u32 mask, u32 bit_offset) + { + // TODO: Use actual remap mask to account for 0 and 1 overrides in default mapping + // TODO: Replace this clusterfuck of texture control with matrix transformation + const auto remap_ctrl = (tex.remap() >> 8) & 0xAA; + if (remap_ctrl == 0xAA) + { + argb8_convert |= (mask & 0xFu) << bit_offset; + return; + } + + if ((remap_ctrl & 0x03) == 0x02) argb8_convert |= (mask & 0x1u) << bit_offset; + if ((remap_ctrl & 0x0C) == 0x08) argb8_convert |= (mask & 0x2u) << bit_offset; + if ((remap_ctrl & 0x30) == 0x20) argb8_convert |= (mask & 0x4u) << bit_offset; + if ((remap_ctrl & 0xC0) == 0x80) argb8_convert |= (mask & 0x8u) << bit_offset; + }; + + if (argb8_signed) + { + // Apply integer sign extension from uint8 to sint8 and renormalize + apply_sign_convert_mask(argb8_signed, texture_control_bits::SEXT_OFFSET); + } + + if (unsigned_remap) + { + // Apply sign expansion, compressed normal-map style (2n - 1) + apply_sign_convert_mask(unsigned_remap, texture_control_bits::EXPAND_OFFSET); + } + + texture_control |= argb8_convert; + } + + current_fragment_program.texture_params[i].control = texture_control; } // Update texture configuration From 69cdded5ba54c577d139e5415926da64ef2865a1 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 14 Dec 2025 20:38:12 +0300 Subject: [PATCH 21/59] rsx: Fix sRGB control flag propagation to shader control - Also clean up the texturing code and fix some memcpy overflow bugs. --- rpcs3/Emu/RSX/RSXThread.cpp | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp index dd272d5f84..638814fda1 100644 --- a/rpcs3/Emu/RSX/RSXThread.cpp +++ b/rpcs3/Emu/RSX/RSXThread.cpp @@ -2057,26 +2057,26 @@ namespace rsx current_fragment_program.two_sided_lighting = m_ctx->register_state->two_side_light_en(); current_fragment_program.mrt_buffers_count = rsx::utility::get_mrt_buffers_count(m_ctx->register_state->surface_color_target()); - if (method_registers.current_draw_clause.classify_mode() == primitive_class::polygon) + if (m_ctx->register_state->current_draw_clause.classify_mode() == primitive_class::polygon) { if (!backend_config.supports_normalized_barycentrics) { current_fragment_program.ctrl |= RSX_SHADER_CONTROL_ATTRIBUTE_INTERPOLATION; } - if (method_registers.alpha_test_enabled()) + if (m_ctx->register_state->alpha_test_enabled()) { current_fragment_program.ctrl |= RSX_SHADER_CONTROL_ALPHA_TEST; } - if (method_registers.polygon_stipple_enabled()) + if (m_ctx->register_state->polygon_stipple_enabled()) { current_fragment_program.ctrl |= RSX_SHADER_CONTROL_POLYGON_STIPPLE; } - if (method_registers.msaa_alpha_to_coverage_enabled()) + if (m_ctx->register_state->msaa_alpha_to_coverage_enabled()) { - const bool is_multiple_samples = method_registers.surface_antialias() != rsx::surface_antialiasing::center_1_sample; + const bool is_multiple_samples = m_ctx->register_state->surface_antialias() != rsx::surface_antialiasing::center_1_sample; if (!backend_config.supports_hw_a2c || (!is_multiple_samples && !backend_config.supports_hw_a2c_1spp)) { // Emulation required @@ -2084,15 +2084,15 @@ namespace rsx } } } - else if (method_registers.point_sprite_enabled() && - method_registers.current_draw_clause.primitive == primitive_type::points) + else if (m_ctx->register_state->point_sprite_enabled() && + m_ctx->register_state->current_draw_clause.primitive == primitive_type::points) { // Set high word of the control mask to store point sprite control - current_fragment_program.texcoord_control_mask |= u32(method_registers.point_sprite_control_mask()) << 16; + current_fragment_program.texcoord_control_mask |= u32(m_ctx->register_state->point_sprite_control_mask()) << 16; } // Check if framebuffer is actually an XRGB format and not a WZYX format - switch (method_registers.surface_color()) + switch (m_ctx->register_state->surface_color()) { case rsx::surface_color_format::w16z16y16x16: case rsx::surface_color_format::w32z32y32x32: @@ -2102,7 +2102,8 @@ namespace rsx default: // Integer framebuffer formats. These can support sRGB output as well as some special rules for output quantization. current_fragment_program.ctrl |= RSX_SHADER_CONTROL_8BIT_FRAMEBUFFER; - if (method_registers.framebuffer_srgb_enabled()) + if (!(current_fragment_program.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS) && // Cannot output sRGB from 32-bit registers + m_ctx->register_state->framebuffer_srgb_enabled()) { current_fragment_program.ctrl |= RSX_SHADER_CONTROL_SRGB_FRAMEBUFFER; } @@ -2113,7 +2114,7 @@ namespace rsx { if (!(textures_ref & 1)) continue; - auto &tex = rsx::method_registers.fragment_textures[i]; + auto &tex = m_ctx->register_state->fragment_textures[i]; current_fp_texture_state.clear(i); if (!tex.enabled() || sampler_descriptors[i]->format_class == RSX_FORMAT_CLASS_UNDEFINED) @@ -2121,7 +2122,11 @@ namespace rsx continue; } - std::memcpy(current_fragment_program.texture_params[i].scale, sampler_descriptors[i]->texcoord_xform.scale, 6 * sizeof(f32)); + std::memcpy( + current_fragment_program.texture_params[i].scale, + sampler_descriptors[i]->texcoord_xform.scale, + sizeof(sampler_descriptors[i]->texcoord_xform.scale)); + current_fragment_program.texture_params[i].remap = tex.remap(); m_graphics_state |= rsx::pipeline_state::fragment_texture_state_dirty; @@ -2131,7 +2136,11 @@ namespace rsx if (sampler_descriptors[i]->texcoord_xform.clamp) { - std::memcpy(current_fragment_program.texture_params[i].clamp_min, sampler_descriptors[i]->texcoord_xform.clamp_min, 4 * sizeof(f32)); + std::memcpy( + current_fragment_program.texture_params[i].clamp_min, + sampler_descriptors[i]->texcoord_xform.clamp_min, + sizeof(sampler_descriptors[i]->texcoord_xform.clamp_min)); + texture_control |= (1 << rsx::texture_control_bits::CLAMP_TEXCOORDS_BIT); } @@ -2316,7 +2325,7 @@ namespace rsx if (current_fragment_program.ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) { //Check that the depth stage is not disabled - if (!rsx::method_registers.depth_test_enabled()) + if (!m_ctx->register_state->depth_test_enabled()) { rsx_log.trace("FS exports depth component but depth test is disabled (INVALID_OPERATION)"); } From da21b101442c295769cd3086a3f96a291be251ff Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 14 Dec 2025 21:25:37 +0300 Subject: [PATCH 22/59] rsx: Propagate state changes to trigger permutation lookup - Alpha test - MSAA (for CSAA enable flag) - Polygon stipple --- rpcs3/Emu/RSX/NV47/HW/nv4097.cpp | 8 ++++++-- rpcs3/Emu/RSX/rsx_methods.cpp | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp b/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp index d0298cf359..8db588da60 100644 --- a/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp +++ b/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp @@ -250,8 +250,12 @@ namespace rsx const auto current = REGS(ctx)->decode(arg); const auto previous = REGS(ctx)->decode(REGS(ctx)->latch); - if (*current.antialias() != *previous.antialias() || // Antialias control has changed, update ROP parameters - current.is_integer_color_format() != previous.is_integer_color_format()) // The type of color format also requires ROP control update + if (current.is_integer_color_format() != previous.is_integer_color_format()) // Different ROP emulation + { + RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_program_state_dirty; + } + + if (*current.antialias() != *previous.antialias()) // Antialias control has changed, update ROP parameters { RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_state_dirty; } diff --git a/rpcs3/Emu/RSX/rsx_methods.cpp b/rpcs3/Emu/RSX/rsx_methods.cpp index 903a10c582..3d12440882 100644 --- a/rpcs3/Emu/RSX/rsx_methods.cpp +++ b/rpcs3/Emu/RSX/rsx_methods.cpp @@ -716,9 +716,9 @@ namespace rsx state_signals[NV4097_SET_POINT_SIZE] = rsx::vertex_state_dirty; state_signals[NV4097_SET_ALPHA_FUNC] = rsx::fragment_state_dirty; state_signals[NV4097_SET_ALPHA_REF] = rsx::fragment_state_dirty; - state_signals[NV4097_SET_ALPHA_TEST_ENABLE] = rsx::fragment_state_dirty; - state_signals[NV4097_SET_ANTI_ALIASING_CONTROL] = rsx::fragment_state_dirty | rsx::pipeline_config_dirty; - state_signals[NV4097_SET_SHADER_PACKER] = rsx::fragment_state_dirty; + state_signals[NV4097_SET_ALPHA_TEST_ENABLE] = rsx::fragment_program_state_dirty; + state_signals[NV4097_SET_ANTI_ALIASING_CONTROL] = rsx::fragment_program_state_dirty; + state_signals[NV4097_SET_SHADER_PACKER] = rsx::fragment_program_state_dirty; state_signals[NV4097_SET_SHADER_WINDOW] = rsx::fragment_state_dirty; state_signals[NV4097_SET_FOG_MODE] = rsx::fragment_state_dirty; state_signals[NV4097_SET_SCISSOR_HORIZONTAL] = rsx::scissor_config_state_dirty; @@ -733,7 +733,7 @@ namespace rsx state_signals[NV4097_SET_VIEWPORT_OFFSET + 0] = rsx::vertex_state_dirty; state_signals[NV4097_SET_VIEWPORT_OFFSET + 1] = rsx::vertex_state_dirty; state_signals[NV4097_SET_VIEWPORT_OFFSET + 2] = rsx::vertex_state_dirty; - state_signals[NV4097_SET_POLYGON_STIPPLE] = rsx::fragment_state_dirty; + state_signals[NV4097_SET_POLYGON_STIPPLE] = rsx::fragment_program_state_dirty; state_signals[NV4097_SET_POLYGON_STIPPLE_PATTERN + 0] = rsx::polygon_stipple_pattern_dirty; state_signals[NV4097_SET_POLYGON_STIPPLE_PATTERN + 1] = rsx::polygon_stipple_pattern_dirty; state_signals[NV4097_SET_POLYGON_STIPPLE_PATTERN + 2] = rsx::polygon_stipple_pattern_dirty; From 6e0b5a874d35edd60d918b759931b4a7642a3f77 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 14 Dec 2025 21:40:14 +0300 Subject: [PATCH 23/59] rxs: Improve alpha-to-coverage emulation --- rpcs3/Emu/RSX/NV47/HW/nv4097.cpp | 28 +++++++++++++++++++ rpcs3/Emu/RSX/NV47/HW/nv4097.h | 2 ++ .../GLSLSnippets/RSXProg/RSXROPEpilogue.glsl | 2 +- rpcs3/Emu/RSX/rsx_methods.cpp | 2 +- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp b/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp index 8db588da60..929925bcb1 100644 --- a/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp +++ b/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp @@ -306,6 +306,34 @@ namespace rsx REGS(ctx)->decode(reg, REGS(ctx)->latch); } + void set_aa_control(context* ctx, u32 reg, u32 arg) + { + const auto latch = REGS(ctx)->latch; + if (arg == latch) + { + return; + } + + // Reconfigure pipeline. + RSX(ctx)->m_graphics_state |= rsx::pipeline_config_dirty; + + // If we support A2C in hardware, leave the rest upto the hardware. The pipeline config should take care of it. + const auto& backend_config = RSX(ctx)->get_backend_config(); + if (backend_config.supports_hw_a2c && + backend_config.supports_hw_a2c_1spp) + { + return; + } + + // No A2C hardware support or partial hardware support. Invalidate the current program if A2C state changed. + const auto a2c_old = REGS(ctx)->decode(latch).msaa_alpha_to_coverage(); + const auto a2c_new = REGS(ctx)->decode(arg).msaa_alpha_to_coverage(); + if (a2c_old != a2c_new) + { + RSX(ctx)->m_graphics_state |= rsx::fragment_program_state_dirty; + } + } + ///// Draw call setup (vertex, etc) void set_array_element16(context* ctx, u32, u32 arg) diff --git a/rpcs3/Emu/RSX/NV47/HW/nv4097.h b/rpcs3/Emu/RSX/NV47/HW/nv4097.h index db736396b9..a5a434e47d 100644 --- a/rpcs3/Emu/RSX/NV47/HW/nv4097.h +++ b/rpcs3/Emu/RSX/NV47/HW/nv4097.h @@ -87,6 +87,8 @@ namespace rsx void set_transform_constant_load(context* ctx, u32 reg, u32 arg); + void set_aa_control(context* ctx, u32 reg, u32 arg); + #define RSX(ctx) ctx->rsxthr #define REGS(ctx) (&rsx::method_registers) diff --git a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPEpilogue.glsl b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPEpilogue.glsl index 283371d0ed..240186cd29 100644 --- a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPEpilogue.glsl +++ b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPEpilogue.glsl @@ -32,7 +32,7 @@ R"( #endif #ifdef _ENABLE_ALPHA_TO_COVERAGE_TEST - if (!_test_bit(rop_control, MSAA_WRITE_ENABLE_BIT) || !coverage_test_passes(col0)) + if (!coverage_test_passes(col0)) { discard; } diff --git a/rpcs3/Emu/RSX/rsx_methods.cpp b/rpcs3/Emu/RSX/rsx_methods.cpp index 3d12440882..03bbd1e52a 100644 --- a/rpcs3/Emu/RSX/rsx_methods.cpp +++ b/rpcs3/Emu/RSX/rsx_methods.cpp @@ -717,7 +717,6 @@ namespace rsx state_signals[NV4097_SET_ALPHA_FUNC] = rsx::fragment_state_dirty; state_signals[NV4097_SET_ALPHA_REF] = rsx::fragment_state_dirty; state_signals[NV4097_SET_ALPHA_TEST_ENABLE] = rsx::fragment_program_state_dirty; - state_signals[NV4097_SET_ANTI_ALIASING_CONTROL] = rsx::fragment_program_state_dirty; state_signals[NV4097_SET_SHADER_PACKER] = rsx::fragment_program_state_dirty; state_signals[NV4097_SET_SHADER_WINDOW] = rsx::fragment_state_dirty; state_signals[NV4097_SET_FOG_MODE] = rsx::fragment_state_dirty; @@ -1714,6 +1713,7 @@ namespace rsx bind(NV4097_SET_BLEND_EQUATION, nv4097::set_blend_equation); bind(NV4097_SET_BLEND_FUNC_SFACTOR, nv4097::set_blend_factor); bind(NV4097_SET_BLEND_FUNC_DFACTOR, nv4097::set_blend_factor); + bind(NV4097_SET_ANTI_ALIASING_CONTROL, nv4097::set_aa_control); //NV308A (0xa400..0xbffc!) bind_array(NV308A_COLOR, 1, 256 * 7, nv308a::color::impl); From 0e9c877b83ed3930d1586e8467a8f4a01e0db79a Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 14 Dec 2025 21:40:46 +0300 Subject: [PATCH 24/59] rsx: Drop pointless ROP control bit check --- .../GLSLSnippets/RSXProg/RSXROPPrologue.glsl | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPPrologue.glsl b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPPrologue.glsl index 5b0798e43e..f24a1ca201 100644 --- a/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPPrologue.glsl +++ b/rpcs3/Emu/RSX/Program/GLSLSnippets/RSXProg/RSXROPPrologue.glsl @@ -1,24 +1,21 @@ R"( #ifdef _ENABLE_POLYGON_STIPPLE - if (_test_bit(rop_control, POLYGON_STIPPLE_ENABLE_BIT)) - { - // Convert x,y to linear address - const uvec2 stipple_coord = uvec2(gl_FragCoord.xy) % uvec2(32, 32); - const uint address = stipple_coord.y * 32u + stipple_coord.x; - const uint bit_offset = (address & 31u); - #ifdef VULKAN - // In vulkan we have a unified array with a dynamic offset - const uint word_index = _get_bits(address, 7, 3) + _fs_stipple_pattern_array_offset; - #else - const uint word_index = _get_bits(address, 7, 3); - #endif - const uint sub_index = _get_bits(address, 5, 2); + // Convert x,y to linear address + const uvec2 stipple_coord = uvec2(gl_FragCoord.xy) % uvec2(32, 32); + const uint address = stipple_coord.y * 32u + stipple_coord.x; + const uint bit_offset = (address & 31u); +#ifdef VULKAN + // In vulkan we have a unified array with a dynamic offset + const uint word_index = _get_bits(address, 7, 3) + _fs_stipple_pattern_array_offset; +#else + const uint word_index = _get_bits(address, 7, 3); +#endif + const uint sub_index = _get_bits(address, 5, 2); - if (!_test_bit(stipple_pattern[word_index][sub_index], int(bit_offset))) - { - _kill(); - } + if (!_test_bit(stipple_pattern[word_index][sub_index], int(bit_offset))) + { + _kill(); } #endif From 307dac479c9611f99ea7d7ec9849cceea86860bb Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sun, 14 Dec 2025 22:50:35 +0300 Subject: [PATCH 25/59] rsx: Fix texcoord clamp regression --- rpcs3/Emu/RSX/RSXThread.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp index 638814fda1..80ae166467 100644 --- a/rpcs3/Emu/RSX/RSXThread.cpp +++ b/rpcs3/Emu/RSX/RSXThread.cpp @@ -2125,7 +2125,7 @@ namespace rsx std::memcpy( current_fragment_program.texture_params[i].scale, sampler_descriptors[i]->texcoord_xform.scale, - sizeof(sampler_descriptors[i]->texcoord_xform.scale)); + sizeof(sampler_descriptors[i]->texcoord_xform.scale) * 2); // Copy scale and bias together current_fragment_program.texture_params[i].remap = tex.remap(); @@ -2139,7 +2139,7 @@ namespace rsx std::memcpy( current_fragment_program.texture_params[i].clamp_min, sampler_descriptors[i]->texcoord_xform.clamp_min, - sizeof(sampler_descriptors[i]->texcoord_xform.clamp_min)); + sizeof(sampler_descriptors[i]->texcoord_xform.clamp_min) * 2); // Copy clamp_min and clamp_max together texture_control |= (1 << rsx::texture_control_bits::CLAMP_TEXCOORDS_BIT); } From bd1a25698c30052164e3f06a56e7359a02fe26e6 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Mon, 15 Dec 2025 00:43:26 +0300 Subject: [PATCH 26/59] rsx: Fix alpha testing - We no longer reload fragment state when toggling alpha-test. Instead, a different shader variant is picked. --- rpcs3/Emu/RSX/Core/RSXDrawCommands.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/rpcs3/Emu/RSX/Core/RSXDrawCommands.cpp b/rpcs3/Emu/RSX/Core/RSXDrawCommands.cpp index ed2447ab24..53f6ce31e7 100644 --- a/rpcs3/Emu/RSX/Core/RSXDrawCommands.cpp +++ b/rpcs3/Emu/RSX/Core/RSXDrawCommands.cpp @@ -680,12 +680,9 @@ namespace rsx ROP_control_t rop_control{}; alignas(16) fragment_context_t payload{}; - if (REGS(m_ctx)->alpha_test_enabled()) - { - const u32 alpha_func = static_cast(REGS(m_ctx)->alpha_func()); - rop_control.set_alpha_test_func(alpha_func); - rop_control.enable_alpha_test(); - } + // Always encode the alpha function. Toggling alpha-test is not guaranteed to trigger context param reload anymore. + const u32 alpha_func = static_cast(REGS(m_ctx)->alpha_func()); + rop_control.set_alpha_test_func(alpha_func); // Generate wpos coefficients // wpos equation is now as follows (ignoring pixel center offset): From 5baf8aeb22d28749b23e79051e4197d60c845b5a Mon Sep 17 00:00:00 2001 From: kd-11 Date: Tue, 16 Dec 2025 01:59:47 +0300 Subject: [PATCH 27/59] vk/fp: Only emit alpha-testing variables when required --- rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp index ab06fbdade..93e074248e 100644 --- a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp @@ -460,10 +460,12 @@ void VKFragmentDecompilerThread::insertMainEnd(std::stringstream & OS) OS << "void main()\n"; OS << "{\n"; - // FIXME: Workaround - OS << - " const uint rop_control = fs_contexts[_fs_context_offset].rop_control;\n" - " const float alpha_ref = fs_contexts[_fs_context_offset].alpha_ref;\n\n"; + if (m_prog.ctrl & RSX_SHADER_CONTROL_ALPHA_TEST) + { + OS << + " const uint rop_control = fs_contexts[_fs_context_offset].rop_control;\n" + " const float alpha_ref = fs_contexts[_fs_context_offset].alpha_ref;\n\n"; + } ::glsl::insert_rop_init(OS); From 441e7e04857710a122a2eb8050222776a519ec37 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Tue, 16 Dec 2025 14:07:58 +0300 Subject: [PATCH 28/59] rsx: Disable early-Z optimizations if a loopback on the depth texture is active - The optimizations worked a bit too well and allowed early-Z to kick in. - Early Z inverts the logic making reads happen after writes and breaking loopback sampling. --- rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp | 19 +++++++++++++++---- rpcs3/Emu/RSX/RSXThread.cpp | 6 ++++++ rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp | 11 +++++++++++ rpcs3/Emu/RSX/gcm_enums.h | 5 +++++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp index 047b362bae..66f062c323 100644 --- a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp @@ -335,20 +335,31 @@ void GLFragmentDecompilerThread::insertMainEnd(std::stringstream & OS) OS << "\n" << " fs_main();\n\n"; + if ((m_ctrl & RSX_SHADER_CONTROL_DISABLE_EARLY_Z) && + !(m_ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD)) && + g_cfg.video.shader_precision != gpu_preset_level::low) + { + // This is effectively unreachable code, but good enough to trick the GPU to skip early Z + OS << + "// Insert NOP sequence to disable early-Z\n" + "if (isnan(gl_FragCoord.z))\n" + " discard;\n\n"; + } + glsl::insert_rop(OS, m_shader_props); if (m_ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) { if (m_parr.HasParam(PF_PARAM_NONE, "vec4", "r1")) { - //Depth writes are always from a fp32 register. See issues section on nvidia's NV_fragment_program spec - //https://www.khronos.org/registry/OpenGL/extensions/NV/NV_fragment_program.txt + // Depth writes are always from a fp32 register. See issues section on nvidia's NV_fragment_program spec + // https://www.khronos.org/registry/OpenGL/extensions/NV/NV_fragment_program.txt OS << " gl_FragDepth = r1.z;\n"; } else { - //Input not declared. Leave commented to assist in debugging the shader - OS << " //gl_FragDepth = r1.z;\n"; + // Input not declared. Leave commented to assist in debugging the shader + OS << " // gl_FragDepth = r1.z;\n"; } } diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp index 80ae166467..1b58bf787f 100644 --- a/rpcs3/Emu/RSX/RSXThread.cpp +++ b/rpcs3/Emu/RSX/RSXThread.cpp @@ -2240,6 +2240,12 @@ namespace rsx default: rsx_log.error("Depth texture bound to pipeline with unexpected format 0x%X", format); } + + if (sampler_descriptors[i]->is_cyclic_reference && + g_cfg.video.shader_precision != gpu_preset_level::low) + { + current_fragment_program.ctrl |= RSX_SHADER_CONTROL_DISABLE_EARLY_Z; + } } else if (!backend_config.supports_hw_renormalization /* && tex.min_filter() == rsx::texture_minify_filter::nearest && diff --git a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp index 93e074248e..0e8e318464 100644 --- a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp @@ -471,6 +471,17 @@ void VKFragmentDecompilerThread::insertMainEnd(std::stringstream & OS) OS << "\n" << " fs_main();\n\n"; + if ((m_ctrl & RSX_SHADER_CONTROL_DISABLE_EARLY_Z) && + !(m_ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD)) && + g_cfg.video.shader_precision != gpu_preset_level::low) + { + // This is effectively unreachable code, but good enough to trick the GPU to skip early Z + OS << + "// Insert NOP sequence to disable early-Z\n" + "if (isnan(gl_FragCoord.z))\n" + " discard;\n\n"; + } + glsl::insert_rop(OS, m_shader_props); if (m_ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) diff --git a/rpcs3/Emu/RSX/gcm_enums.h b/rpcs3/Emu/RSX/gcm_enums.h index 7fd9437f66..539796a531 100644 --- a/rpcs3/Emu/RSX/gcm_enums.h +++ b/rpcs3/Emu/RSX/gcm_enums.h @@ -465,6 +465,11 @@ namespace gcm RSX_SHADER_CONTROL_ALPHA_TEST = 0x0400000, // Uses alpha test on the outputs RSX_SHADER_CONTROL_POLYGON_STIPPLE = 0x0800000, // Uses polygon stipple for dithered rendering RSX_SHADER_CONTROL_ALPHA_TO_COVERAGE = 0x1000000, // Alpha to coverage + + RSX_SHADER_CONTROL_DISABLE_EARLY_Z = 0x2000000, // Do not allow early-Z optimizations on this shader + + // Meta + RSX_SHADER_CONTROL_META_USES_DISCARD = (RSX_SHADER_CONTROL_USES_KIL | RSX_SHADER_CONTROL_TEXTURE_ALPHA_KILL | RSX_SHADER_CONTROL_ALPHA_TEST | RSX_SHADER_CONTROL_POLYGON_STIPPLE | RSX_SHADER_CONTROL_ALPHA_TO_COVERAGE) }; // GCM Reports From 5a809c5f724514baa377a03089a50f917f73669b Mon Sep 17 00:00:00 2001 From: kd-11 Date: Wed, 17 Dec 2025 09:29:44 +0300 Subject: [PATCH 29/59] rsx/fp: Stop using m_ctrl and use the correct object from m_prog --- rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp | 20 +++++++-------- .../RSX/Program/FragmentProgramDecompiler.cpp | 5 ++-- .../RSX/Program/FragmentProgramDecompiler.h | 1 - rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp | 25 ++++++++++--------- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp index 66f062c323..6ef26db1e0 100644 --- a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp @@ -93,13 +93,13 @@ void GLFragmentDecompilerThread::insertOutputs(std::stringstream & OS) { const std::pair table[] = { - { "ocol0", m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r0" : "h0" }, - { "ocol1", m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r2" : "h4" }, - { "ocol2", m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r3" : "h6" }, - { "ocol3", m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r4" : "h8" }, + { "ocol0", m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r0" : "h0" }, + { "ocol1", m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r2" : "h4" }, + { "ocol2", m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r3" : "h6" }, + { "ocol3", m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r4" : "h8" }, }; - const bool float_type = (m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS) || !device_props.has_native_half_support; + const bool float_type = (m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS) || !device_props.has_native_half_support; const auto reg_type = float_type ? "vec4" : getHalfTypeName(4); for (uint i = 0; i < std::size(table); ++i) { @@ -244,7 +244,7 @@ void GLFragmentDecompilerThread::insertGlobalFunctions(std::stringstream &OS) void GLFragmentDecompilerThread::insertMainStart(std::stringstream & OS) { std::set output_registers; - if (m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS) + if (m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS) { output_registers = { "r0", "r2", "r3", "r4" }; } @@ -253,7 +253,7 @@ void GLFragmentDecompilerThread::insertMainStart(std::stringstream & OS) output_registers = { "h0", "h4", "h6", "h8" }; } - if (m_ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) + if (m_prog.ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) { output_registers.insert("r1"); } @@ -335,8 +335,8 @@ void GLFragmentDecompilerThread::insertMainEnd(std::stringstream & OS) OS << "\n" << " fs_main();\n\n"; - if ((m_ctrl & RSX_SHADER_CONTROL_DISABLE_EARLY_Z) && - !(m_ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD)) && + if ((m_prog.ctrl & RSX_SHADER_CONTROL_DISABLE_EARLY_Z) && + !(m_prog.ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD)) && g_cfg.video.shader_precision != gpu_preset_level::low) { // This is effectively unreachable code, but good enough to trick the GPU to skip early Z @@ -348,7 +348,7 @@ void GLFragmentDecompilerThread::insertMainEnd(std::stringstream & OS) glsl::insert_rop(OS, m_shader_props); - if (m_ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) + if (m_prog.ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) { if (m_parr.HasParam(PF_PARAM_NONE, "vec4", "r1")) { diff --git a/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.cpp b/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.cpp index 1ba30e45b3..8c5c163f06 100644 --- a/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.cpp +++ b/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.cpp @@ -84,7 +84,6 @@ std::vector get_fragment_program_output_set(u32 ctrl, u32 mrt_count FragmentProgramDecompiler::FragmentProgramDecompiler(const RSXFragmentProgram &prog, u32& size) : m_size(size) , m_prog(prog) - , m_ctrl(prog.ctrl) { m_size = 0; } @@ -805,7 +804,7 @@ std::string FragmentProgramDecompiler::BuildCode() // Shader validation // Shader must at least write to one output for the body to be considered valid - const bool fp16_out = !(m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS); + const bool fp16_out = !(m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS); const std::string float4_type = (fp16_out && device_props.has_native_half_support)? getHalfTypeName(4) : getFloatTypeName(4); const std::string init_value = float4_type + "(0.)"; std::array output_register_names; @@ -814,7 +813,7 @@ std::string FragmentProgramDecompiler::BuildCode() std::stringstream main_epilogue; // Check depth export - if (m_ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) + if (m_prog.ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) { // Hw tests show that the depth export register is default-initialized to 0 and not wpos.z!! m_parr.AddParam(PF_PARAM_NONE, getFloatTypeName(4), "r1", init_value); diff --git a/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.h b/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.h index 09a02804c3..8e22a21a93 100644 --- a/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.h +++ b/rpcs3/Emu/RSX/Program/FragmentProgramDecompiler.h @@ -99,7 +99,6 @@ class FragmentProgramDecompiler protected: const RSXFragmentProgram &m_prog; - u32 m_ctrl = 0; /** returns the type name of float vectors. */ diff --git a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp index 0e8e318464..8ecc427f73 100644 --- a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp @@ -144,15 +144,15 @@ void VKFragmentDecompilerThread::insertOutputs(std::stringstream & OS) { const std::pair table[] = { - { "ocol0", m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r0" : "h0" }, - { "ocol1", m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r2" : "h4" }, - { "ocol2", m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r3" : "h6" }, - { "ocol3", m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r4" : "h8" }, + { "ocol0", m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r0" : "h0" }, + { "ocol1", m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r2" : "h4" }, + { "ocol2", m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r3" : "h6" }, + { "ocol3", m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS ? "r4" : "h8" }, }; // NOTE: We do not skip outputs, the only possible combinations are a(0), b(0), ab(0,1), abc(0,1,2), abcd(0,1,2,3) u8 output_index = 0; - const bool float_type = (m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS) || !device_props.has_native_half_support; + const bool float_type = (m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS) || !device_props.has_native_half_support; const auto reg_type = float_type ? "vec4" : getHalfTypeName(4); for (uint i = 0; i < std::size(table); ++i) { @@ -363,7 +363,7 @@ void VKFragmentDecompilerThread::insertGlobalFunctions(std::stringstream &OS) void VKFragmentDecompilerThread::insertMainStart(std::stringstream & OS) { std::set output_registers; - if (m_ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS) + if (m_prog.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS) { output_registers = { "r0", "r2", "r3", "r4" }; } @@ -372,7 +372,7 @@ void VKFragmentDecompilerThread::insertMainStart(std::stringstream & OS) output_registers = { "h0", "h4", "h6", "h8" }; } - if (m_ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) + if (m_prog.ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) { output_registers.insert("r1"); } @@ -471,20 +471,21 @@ void VKFragmentDecompilerThread::insertMainEnd(std::stringstream & OS) OS << "\n" << " fs_main();\n\n"; - if ((m_ctrl & RSX_SHADER_CONTROL_DISABLE_EARLY_Z) && - !(m_ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD)) && + if ((m_prog.ctrl & RSX_SHADER_CONTROL_DISABLE_EARLY_Z) && + !(m_prog.ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD)) && g_cfg.video.shader_precision != gpu_preset_level::low) { // This is effectively unreachable code, but good enough to trick the GPU to skip early Z OS << "// Insert NOP sequence to disable early-Z\n" - "if (isnan(gl_FragCoord.z))\n" - " discard;\n\n"; + " const uint rop_control = fs_contexts[_fs_context_offset].rop_control;\n" + " if (_test_bit(rop_control, 0))\n" + " discard;\n\n"; } glsl::insert_rop(OS, m_shader_props); - if (m_ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) + if (m_prog.ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT) { if (m_parr.HasParam(PF_PARAM_NONE, "vec4", "r1")) { From fd3ecea21e271f1d5f2487ae0d2f825d4303d2b3 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Wed, 17 Dec 2025 11:11:46 +0300 Subject: [PATCH 30/59] rsx: Improve early-Z disablement mechanism --- rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp | 11 ++++------- rpcs3/Emu/RSX/RSXThread.cpp | 2 ++ rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp | 11 ++++------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp index 6ef26db1e0..f8cc046569 100644 --- a/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/GL/GLFragmentProgram.cpp @@ -335,15 +335,12 @@ void GLFragmentDecompilerThread::insertMainEnd(std::stringstream & OS) OS << "\n" << " fs_main();\n\n"; - if ((m_prog.ctrl & RSX_SHADER_CONTROL_DISABLE_EARLY_Z) && - !(m_prog.ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD)) && - g_cfg.video.shader_precision != gpu_preset_level::low) + if (m_prog.ctrl & RSX_SHADER_CONTROL_DISABLE_EARLY_Z) { - // This is effectively unreachable code, but good enough to trick the GPU to skip early Z + // This is effectively pointless code, but good enough to trick the GPU to skip early Z OS << - "// Insert NOP sequence to disable early-Z\n" - "if (isnan(gl_FragCoord.z))\n" - " discard;\n\n"; + " // Insert pseudo-barrier sequence to disable early-Z\n" + " gl_FragDepth = gl_FragCoord.z;\n\n"; } glsl::insert_rop(OS, m_shader_props); diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp index 1b58bf787f..a353c34d94 100644 --- a/rpcs3/Emu/RSX/RSXThread.cpp +++ b/rpcs3/Emu/RSX/RSXThread.cpp @@ -2242,6 +2242,8 @@ namespace rsx } if (sampler_descriptors[i]->is_cyclic_reference && + !(current_fragment_program.ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD)) && + !g_cfg.video.strict_rendering_mode && g_cfg.video.shader_precision != gpu_preset_level::low) { current_fragment_program.ctrl |= RSX_SHADER_CONTROL_DISABLE_EARLY_Z; diff --git a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp index 8ecc427f73..af2e60ec93 100644 --- a/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp +++ b/rpcs3/Emu/RSX/VK/VKFragmentProgram.cpp @@ -471,16 +471,13 @@ void VKFragmentDecompilerThread::insertMainEnd(std::stringstream & OS) OS << "\n" << " fs_main();\n\n"; - if ((m_prog.ctrl & RSX_SHADER_CONTROL_DISABLE_EARLY_Z) && - !(m_prog.ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD)) && - g_cfg.video.shader_precision != gpu_preset_level::low) + if (m_prog.ctrl & RSX_SHADER_CONTROL_DISABLE_EARLY_Z) { // This is effectively unreachable code, but good enough to trick the GPU to skip early Z + // For vulkan, depth export has stronger semantics than discard. OS << - "// Insert NOP sequence to disable early-Z\n" - " const uint rop_control = fs_contexts[_fs_context_offset].rop_control;\n" - " if (_test_bit(rop_control, 0))\n" - " discard;\n\n"; + " // Insert pseudo-barrier sequence to disable early-Z\n" + " gl_FragDepth = gl_FragCoord.z;\n\n"; } glsl::insert_rop(OS, m_shader_props); From ba673d1407851086f2bb960a8c4a4b494eabbcb9 Mon Sep 17 00:00:00 2001 From: kd-11 Date: Fri, 26 Dec 2025 15:23:54 +0300 Subject: [PATCH 31/59] rsx/vk: Add post-cyclic-z barriers allowing us to keep early Z optimizations --- rpcs3/Emu/RSX/Core/RSXDriverState.h | 3 +++ rpcs3/Emu/RSX/RSXThread.cpp | 40 +++++++++++++++++++++++++---- rpcs3/Emu/RSX/VK/VKDraw.cpp | 10 ++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/rpcs3/Emu/RSX/Core/RSXDriverState.h b/rpcs3/Emu/RSX/Core/RSXDriverState.h index 16dd08c78d..9e7bc2cd68 100644 --- a/rpcs3/Emu/RSX/Core/RSXDriverState.h +++ b/rpcs3/Emu/RSX/Core/RSXDriverState.h @@ -40,6 +40,9 @@ namespace rsx xform_instancing_state_dirty = (1 << 25), // Transform instancing state has changed + zeta_address_is_cyclic = (1 << 26), // The currently bound Z buffer is active for R/W in a cyclic manner + zeta_address_cyclic_barrier = (1 << 27), // A memory barrier is required to "end" the Z buffer cyclic state + // TODO - Should signal that we simply need to do a FP compare before the next draw call and invalidate the ucode if the content has changed. // Marking as dirty to invalidate hot cache also works, it's not like there's tons of barriers per frame anyway. fragment_program_needs_rehash = fragment_program_ucode_dirty, diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp index a353c34d94..c6038e2f50 100644 --- a/rpcs3/Emu/RSX/RSXThread.cpp +++ b/rpcs3/Emu/RSX/RSXThread.cpp @@ -1681,10 +1681,24 @@ namespace rsx return; } + auto set_zeta_write_enabled = [&](bool state) + { + if (state == m_framebuffer_layout.zeta_write_enabled) + { + return; + } + + if (m_graphics_state & rsx::zeta_address_is_cyclic) + { + m_graphics_state |= rsx::fragment_program_state_dirty; + } + m_framebuffer_layout.zeta_write_enabled = state; + }; + auto evaluate_depth_buffer_state = [&]() { - m_framebuffer_layout.zeta_write_enabled = - (rsx::method_registers.depth_test_enabled() && rsx::method_registers.depth_write_enabled()); + const bool zeta_write_en = (rsx::method_registers.depth_test_enabled() && rsx::method_registers.depth_write_enabled()); + set_zeta_write_enabled(zeta_write_en); }; auto evaluate_stencil_buffer_state = [&]() @@ -1707,7 +1721,7 @@ namespace rsx rsx::method_registers.back_stencil_op_zfail() != rsx::stencil_op::keep); } - m_framebuffer_layout.zeta_write_enabled = (mask && active_write_op); + set_zeta_write_enabled(mask && active_write_op); } }; @@ -2110,6 +2124,9 @@ namespace rsx break; } + const bool zeta_was_cyclic = m_graphics_state & rsx::zeta_address_is_cyclic; + m_graphics_state.clear(rsx::zeta_address_is_cyclic); + for (u32 textures_ref = current_fp_metadata.referenced_textures_mask, i = 0; textures_ref; textures_ref >>= 1, ++i) { if (!(textures_ref & 1)) continue; @@ -2242,11 +2259,17 @@ namespace rsx } if (sampler_descriptors[i]->is_cyclic_reference && - !(current_fragment_program.ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD)) && + m_framebuffer_layout.zeta_address != 0 && !g_cfg.video.strict_rendering_mode && g_cfg.video.shader_precision != gpu_preset_level::low) { - current_fragment_program.ctrl |= RSX_SHADER_CONTROL_DISABLE_EARLY_Z; + m_graphics_state |= rsx::zeta_address_is_cyclic; + + if (!(current_fragment_program.ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD)) && + m_framebuffer_layout.zeta_write_enabled) + { + current_fragment_program.ctrl |= RSX_SHADER_CONTROL_DISABLE_EARLY_Z; + } } } else if (!backend_config.supports_hw_renormalization /* && @@ -2340,6 +2363,13 @@ namespace rsx } m_program_cache_hint.invalidate_fragment_program(current_fragment_program); + + if (zeta_was_cyclic && zeta_was_cyclic != m_graphics_state.test(rsx::zeta_address_is_cyclic)) + { + // Forced "fall-out" barrier. This is a special case for Z buffers because they can be cyclic without writes. + // That condition can cause early-Z in a later call to introduce data hazard in previous cyclic draws. + m_graphics_state |= rsx::zeta_address_cyclic_barrier; + } } bool thread::invalidate_fragment_program(u32 dst_dma, u32 dst_offset, u32 size) diff --git a/rpcs3/Emu/RSX/VK/VKDraw.cpp b/rpcs3/Emu/RSX/VK/VKDraw.cpp index 6101aeb9de..a088a6ead6 100644 --- a/rpcs3/Emu/RSX/VK/VKDraw.cpp +++ b/rpcs3/Emu/RSX/VK/VKDraw.cpp @@ -1044,6 +1044,14 @@ void VKGSRender::end() if (auto ds = std::get<1>(m_rtts.m_bound_depth_stencil)) { ds->write_barrier(*m_current_command_buffer); + + if (m_graphics_state.test(rsx::zeta_address_cyclic_barrier) && + ds->current_layout != VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) + { + // We actually need to end the subpass as a minimum. Without this, early-Z optimiazations in following draws will clobber reads from previous draws and cause flickering. + // Since we're ending the subpass, might as well restore DCC/HiZ for extra performance + ds->change_layout(*m_current_command_buffer, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); + } } for (auto &rtt : m_rtts.m_bound_render_targets) @@ -1054,6 +1062,8 @@ void VKGSRender::end() } } + m_graphics_state.clear(rsx::zeta_address_cyclic_barrier); + m_frame_stats.setup_time += m_profiler.duration(); // Now bind the shader resources. It is important that this takes place after the barriers so that we don't end up with stale descriptors From e58698684512f0f3dea70b0e84da4d35657a066e Mon Sep 17 00:00:00 2001 From: kd-11 Date: Fri, 26 Dec 2025 16:04:18 +0300 Subject: [PATCH 32/59] rsx/gl: Clear cyclic Z barrier flag - OpenGL doesn't care about this but we clear it anyway to indicate it is no longer pending. --- rpcs3/Emu/RSX/GL/GLDraw.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rpcs3/Emu/RSX/GL/GLDraw.cpp b/rpcs3/Emu/RSX/GL/GLDraw.cpp index 51bf257aef..e8ea4bc714 100644 --- a/rpcs3/Emu/RSX/GL/GLDraw.cpp +++ b/rpcs3/Emu/RSX/GL/GLDraw.cpp @@ -715,7 +715,10 @@ void GLGSRender::end() m_frame_stats.textures_upload_time += m_profiler.duration(); gl::command_context cmd{ gl_state }; - if (auto ds = std::get<1>(m_rtts.m_bound_depth_stencil)) ds->write_barrier(cmd); + if (auto ds = std::get<1>(m_rtts.m_bound_depth_stencil)) + { + ds->write_barrier(cmd); + } for (auto &rtt : m_rtts.m_bound_render_targets) { @@ -725,6 +728,8 @@ void GLGSRender::end() } } + m_graphics_state.clear(rsx::zeta_address_cyclic_barrier); + update_draw_state(); if (g_cfg.video.debug_output) From 316aa44d3e7f27c2505717221dc234bc08794f4d Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sat, 27 Dec 2025 00:07:22 +0300 Subject: [PATCH 33/59] vk: Fix crash when shuffling between cyclic and non-cyclic Z-buffer in one logical pass --- rpcs3/Emu/RSX/VK/VKDraw.cpp | 66 ++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/rpcs3/Emu/RSX/VK/VKDraw.cpp b/rpcs3/Emu/RSX/VK/VKDraw.cpp index a088a6ead6..b73b4a66a8 100644 --- a/rpcs3/Emu/RSX/VK/VKDraw.cpp +++ b/rpcs3/Emu/RSX/VK/VKDraw.cpp @@ -65,38 +65,51 @@ namespace vk case VK_IMAGE_LAYOUT_GENERAL: case VK_IMAGE_LAYOUT_ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT: ensure(sampler_state->upload_context == rsx::texture_upload_context::framebuffer_storage); - if (!sampler_state->is_cyclic_reference) + if (sampler_state->is_cyclic_reference) [[ unlikely ]] { - // This was used in a cyclic ref before, but is missing a barrier - // No need for a full stall, use a custom barrier instead - VkPipelineStageFlags src_stage; - VkAccessFlags src_access; - if (raw->aspect() == VK_IMAGE_ASPECT_COLOR_BIT) - { - src_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - src_access = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - } - else - { - src_stage = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; - src_access = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; - } - - vk::insert_image_memory_barrier( - cmd, - raw->value, - raw->current_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - src_stage, dst_stage, - src_access, VK_ACCESS_SHADER_READ_BIT, - { raw->aspect(), 0, 1, 0, 1 }); - - raw->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + // Nothing to do + break; } + + // This was used in a cyclic ref before, but is missing a barrier + // No need for a full stall, use a custom barrier instead + VkPipelineStageFlags src_stage; + VkAccessFlags src_access; + if (raw->aspect() == VK_IMAGE_ASPECT_COLOR_BIT) + { + src_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + src_access = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + } + else + { + src_stage = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + src_access = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + } + + vk::insert_image_memory_barrier( + cmd, + raw->value, + raw->current_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + src_stage, dst_stage, + src_access, VK_ACCESS_SHADER_READ_BIT, + { raw->aspect(), 0, 1, 0, 1 }); + + raw->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; break; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: ensure(sampler_state->upload_context == rsx::texture_upload_context::framebuffer_storage); - raw->change_layout(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + if (!sampler_state->is_cyclic_reference) [[ likely ]] + { + // Standard pre-read barrier. + raw->change_layout(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + break; + } + + // Normally this shouldn't happen. But that is only guaranteed if the attachment state never changes outside of RTT rebind interrupts. + // If the shader changes between binds to "disable" the cyclic nature, we could end up here. + // Draw 1 (cyllic) -> texture_barrier -> Draw 2 (no textures) -> attachment_optimal -> Draw 3 (cylic again, no new data) -> incorrect layout. + vk::as_rtt(raw)->texture_barrier(cmd); break; } } @@ -1051,6 +1064,7 @@ void VKGSRender::end() // We actually need to end the subpass as a minimum. Without this, early-Z optimiazations in following draws will clobber reads from previous draws and cause flickering. // Since we're ending the subpass, might as well restore DCC/HiZ for extra performance ds->change_layout(*m_current_command_buffer, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); + ds->reset_surface_counters(); } } From f5be489d9f34d04f59ea9fc21f217aa989696a0d Mon Sep 17 00:00:00 2001 From: kd-11 Date: Sat, 27 Dec 2025 02:29:55 +0300 Subject: [PATCH 34/59] rsx: Improve framebuffer layout change detection --- rpcs3/Emu/RSX/GL/GLRenderTargets.cpp | 1 + rpcs3/Emu/RSX/RSXThread.cpp | 25 +++++++++++++++++-------- rpcs3/Emu/RSX/RSXThread.h | 4 ++++ rpcs3/Emu/RSX/VK/VKGSRender.cpp | 5 +++-- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp b/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp index c222262699..4e5e61dcba 100644 --- a/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp +++ b/rpcs3/Emu/RSX/GL/GLRenderTargets.cpp @@ -301,6 +301,7 @@ void GLGSRender::init_buffers(rsx::framebuffer_creation_context context, bool /* } m_graphics_state.set(rsx::rtt_config_valid); + on_framebuffer_layout_updated(); check_zcull_status(true); set_viewport(); diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp index c6038e2f50..cc7b9fff17 100644 --- a/rpcs3/Emu/RSX/RSXThread.cpp +++ b/rpcs3/Emu/RSX/RSXThread.cpp @@ -1740,14 +1740,7 @@ namespace rsx } } - if (::size32(mrt_buffers) != current_fragment_program.mrt_buffers_count && - !m_graphics_state.test(rsx::pipeline_state::fragment_program_dirty) && - !is_current_program_interpreted()) - { - // Notify that we should recompile the FS - m_graphics_state |= rsx::pipeline_state::fragment_program_state_dirty; - } - + on_framebuffer_layout_updated(); return any_found; }; @@ -1845,6 +1838,22 @@ namespace rsx } } + void thread::on_framebuffer_layout_updated() + { + if (m_graphics_state.test(rsx::fragment_program_state_dirty)) + { + return; + } + + const auto target = m_ctx->register_state->surface_color_target(); + if (rsx::utility::get_mrt_buffers_count(target) == current_fragment_program.mrt_buffers_count) + { + return; + } + + m_graphics_state |= rsx::fragment_program_state_dirty; + } + bool thread::get_scissor(areau& region, bool clip_viewport) { if (!m_graphics_state.test(rsx::pipeline_state::scissor_config_state_dirty)) diff --git a/rpcs3/Emu/RSX/RSXThread.h b/rpcs3/Emu/RSX/RSXThread.h index 9e09c17d78..4f965dee80 100644 --- a/rpcs3/Emu/RSX/RSXThread.h +++ b/rpcs3/Emu/RSX/RSXThread.h @@ -257,6 +257,10 @@ namespace rsx void get_framebuffer_layout(rsx::framebuffer_creation_context context, framebuffer_layout &layout); bool get_scissor(areau& region, bool clip_viewport); + // Notify framebuffer layout has been committed. + // FIXME: This should not be here + void on_framebuffer_layout_updated(); + RSXVertexProgram current_vertex_program = {}; RSXFragmentProgram current_fragment_program = {}; diff --git a/rpcs3/Emu/RSX/VK/VKGSRender.cpp b/rpcs3/Emu/RSX/VK/VKGSRender.cpp index 3bc240b768..4a0e6c19ce 100644 --- a/rpcs3/Emu/RSX/VK/VKGSRender.cpp +++ b/rpcs3/Emu/RSX/VK/VKGSRender.cpp @@ -2469,7 +2469,7 @@ void VKGSRender::prepare_rtts(rsx::framebuffer_creation_context context) m_surface_info[i].samples = samples; } - //Process depth surface as well + // Process depth surface as well { if (m_depth_surface_info.pitch && g_cfg.video.write_depth_buffer) { @@ -2486,7 +2486,7 @@ void VKGSRender::prepare_rtts(rsx::framebuffer_creation_context context) m_depth_surface_info.samples = samples; } - //Bind created rtts as current fbo... + // Bind created rtts as current fbo... const auto draw_buffers = rsx::utility::get_rtt_indexes(m_framebuffer_layout.target); m_draw_buffers.clear(); m_fbo_images.clear(); @@ -2637,6 +2637,7 @@ void VKGSRender::prepare_rtts(rsx::framebuffer_creation_context context) set_viewport(); set_scissor(clipped_scissor); + on_framebuffer_layout_updated(); check_zcull_status(true); } From 92f56215d0b6c0fcc6889475deb8274fba6f0629 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Thu, 4 Dec 2025 00:23:53 +0100 Subject: [PATCH 35/59] Qt: Allow user to precompile caches on demand when installing packages or adding disc games --- rpcs3/rpcs3qt/main_window.cpp | 53 ++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 168d08a344..d4e52b8025 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -1180,7 +1180,7 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo { m_game_list_frame->AddRefreshedSlot([this, paths = std::move(bootable_paths_installed)](std::set& claimed_paths) mutable { - // Try to claim operaions on ID + // Try to claim operations on ID for (auto it = paths.begin(); it != paths.end();) { std::string resolved_path = Emu.GetCallbacks().resolve_path(it->first); @@ -1196,13 +1196,7 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo } } - // Executes after PrecompileCachesFromInstalledPackages - m_notify_batch_game_action_cb = [this, paths]() mutable - { - ShowOptionalGamePreparations(tr("Success!"), tr("Successfully installed software from package(s)!"), std::move(paths)); - }; - - PrecompileCachesFromInstalledPackages(paths); + ShowOptionalGamePreparations(tr("Success!"), tr("Successfully installed software from package(s)!"), std::move(paths)); }); } @@ -2402,6 +2396,8 @@ void main_window::ShowOptionalGamePreparations(const QString& title, const QStri QVBoxLayout* vlayout = new QVBoxLayout(dlg); + QCheckBox* precompile_check = new QCheckBox(tr("Precompile caches")); + precompile_check->setChecked(true); QCheckBox* desk_check = new QCheckBox(tr("Add desktop shortcut(s)")); #ifdef _WIN32 QCheckBox* quick_check = new QCheckBox(tr("Add Start menu shortcut(s)")); @@ -2410,10 +2406,12 @@ void main_window::ShowOptionalGamePreparations(const QString& title, const QStri #else QCheckBox* quick_check = new QCheckBox(tr("Add launcher shortcut(s)")); #endif - QLabel* label = new QLabel(tr("%1\nWould you like to install shortcuts to the installed software? (%2 new software detected)\n\n").arg(message).arg(bootable_paths.size()), dlg); + QLabel* label = new QLabel(tr("%1\nWould you like to precompile caches and install shortcuts to the installed software? (%2 new software detected)\n\n").arg(message).arg(bootable_paths.size()), dlg); vlayout->addWidget(label); vlayout->addStretch(10); + vlayout->addWidget(precompile_check); + vlayout->addStretch(3); vlayout->addWidget(desk_check); vlayout->addStretch(3); vlayout->addWidget(quick_check); @@ -2426,6 +2424,7 @@ void main_window::ShowOptionalGamePreparations(const QString& title, const QStri connect(btn_box, &QDialogButtonBox::accepted, this, [=, this, paths = std::move(bootable_paths)]() { + const bool precompile_caches = precompile_check->isChecked(); const bool create_desktop_shortcuts = desk_check->isChecked(); const bool create_app_shortcut = quick_check->isChecked(); @@ -2447,35 +2446,38 @@ void main_window::ShowOptionalGamePreparations(const QString& title, const QStri locations.insert(gui::utils::shortcut_location::applications); } - if (locations.empty()) + if (!locations.empty()) { - return; - } + std::vector game_data_shortcuts; - std::vector game_data_shortcuts; - - for (const auto& [boot_path, title_id] : paths) - { - for (const game_info& gameinfo : m_game_list_frame->GetGameInfo()) + for (const auto& [boot_path, title_id] : paths) { - if (gameinfo && gameinfo->info.serial == title_id.toStdString()) + for (const game_info& gameinfo : m_game_list_frame->GetGameInfo()) { - if (Emu.IsPathInsideDir(boot_path, gameinfo->info.path)) + if (gameinfo && gameinfo->info.serial == title_id.toStdString()) { - if (!locations.empty()) + if (Emu.IsPathInsideDir(boot_path, gameinfo->info.path)) { - game_data_shortcuts.push_back(gameinfo); + if (!locations.empty()) + { + game_data_shortcuts.push_back(gameinfo); + } } - } - break; + break; + } } } + + if (!game_data_shortcuts.empty() && !locations.empty()) + { + m_game_list_frame->CreateShortcuts(game_data_shortcuts, locations); + } } - if (!game_data_shortcuts.empty() && !locations.empty()) + if (precompile_caches) { - m_game_list_frame->CreateShortcuts(game_data_shortcuts, locations); + PrecompileCachesFromInstalledPackages(paths); } }); @@ -2483,7 +2485,6 @@ void main_window::ShowOptionalGamePreparations(const QString& title, const QStri dlg->open(); } - void main_window::PrecompileCachesFromInstalledPackages(const std::map& bootable_paths) { std::vector game_data; From bba1cf4b6e7237227e65b9eb1740ced639882603 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sun, 7 Dec 2025 12:17:07 +0100 Subject: [PATCH 36/59] Qt: ask user for install options before starting the installation --- rpcs3/rpcs3qt/main_window.cpp | 220 +++++++++------------------ rpcs3/rpcs3qt/main_window.h | 1 + rpcs3/rpcs3qt/pkg_install_dialog.cpp | 90 +++++++---- rpcs3/rpcs3qt/pkg_install_dialog.h | 12 +- 4 files changed, 142 insertions(+), 181 deletions(-) diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index d4e52b8025..3ba64487e6 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -801,7 +801,7 @@ bool main_window::InstallPackages(QStringList file_paths, bool from_boot) if (file_paths.count() == 1) { - const QString file_path = file_paths.front(); + const QString& file_path = file_paths.front(); const QFileInfo file_info(file_path); if (file_info.isDir()) @@ -819,92 +819,6 @@ bool main_window::InstallPackages(QStringList file_paths, bool from_boot) return InstallPackages(dir_file_paths, from_boot); } - - if (file_info.suffix().compare("pkg", Qt::CaseInsensitive) == 0) - { - compat::package_info info = game_compatibility::GetPkgInfo(file_path, m_game_list_frame ? m_game_list_frame->GetGameCompatibility() : nullptr); - - if (!info.is_valid) - { - QMessageBox::warning(this, tr("Invalid package!"), tr("The selected package is invalid!\n\nPath:\n%0").arg(file_path)); - return false; - } - - if (info.type != compat::package_type::other) - { - if (info.type == compat::package_type::dlc) - { - info.local_cat = tr("\nDLC", "Block for package type (DLC)"); - } - else - { - info.local_cat = tr("\nUpdate", "Block for package type (Update)"); - } - } - else if (!info.local_cat.isEmpty()) - { - info.local_cat = tr("\n%0", "Block for package type").arg(info.local_cat); - } - - if (!info.title_id.isEmpty()) - { - info.title_id = tr("\n%0", "Block for Title ID").arg(info.title_id); - } - - if (!info.version.isEmpty()) - { - info.version = tr("\nVersion %0", "Block for Version").arg(info.version); - } - - if (!info.changelog.isEmpty()) - { - info.changelog = tr("Changelog:\n%0", "Block for Changelog").arg(info.changelog); - } - - u64 free_space = 0; - - // Retrieve disk space info on data path's drive - if (fs::device_stat stat{}; fs::statfs(rpcs3::utils::get_hdd0_dir(), stat)) - { - free_space = stat.avail_free; - } - - const QString installation_info = - tr("Installation path: %0\nAvailable disk space: %1%2\nRequired disk space: %3") - .arg(QString::fromStdString(rpcs3::utils::get_hdd0_game_dir())) - .arg(gui::utils::format_byte_size(free_space)) - .arg(info.data_size <= free_space ? QString() : tr(" - NOT ENOUGH SPACE")) - .arg(gui::utils::format_byte_size(info.data_size)); - - const QString info_string = QStringLiteral("%0\n\n%1%2%3%4").arg(file_info.fileName()).arg(info.title).arg(info.local_cat).arg(info.title_id).arg(info.version); - QString message = tr("Do you want to install this package?\n\n%0\n\n%1").arg(info_string).arg(installation_info); - - QMessageBox mb(QMessageBox::Icon::Question, tr("PKG Decrypter / Installer"), gui::utils::make_paragraph(message), QMessageBox::Yes | QMessageBox::No, this); - mb.setDefaultButton(QMessageBox::No); - mb.setTextFormat(Qt::RichText); // Support HTML tags - mb.button(QMessageBox::Yes)->setEnabled(info.data_size <= free_space); - - if (!info.changelog.isEmpty()) - { - mb.setInformativeText(tr("To see the changelog, please click \"Show Details\".")); - mb.setDetailedText(info.changelog); - - // Smartass hack to make the unresizeable message box wide enough for the changelog - const int log_width = QLabel(info.changelog).sizeHint().width(); - while (QLabel(message).sizeHint().width() < log_width) - { - message += " "; - } - - mb.setText(message); - } - - if (mb.exec() != QMessageBox::Yes) - { - gui_log.notice("PKG: Cancelled installation from drop.\n%s\n%s", info_string, info.changelog); - return true; - } - } } // Install rap files if available @@ -983,28 +897,22 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo } std::vector packages; + bool precompile_caches = false; + bool create_desktop_shortcuts = false; + bool create_app_shortcut = false; game_compatibility* compat = m_game_list_frame ? m_game_list_frame->GetGameCompatibility() : nullptr; - if (file_paths.size() > 1) + // Let the user choose the packages to install and select the order in which they shall be installed. + pkg_install_dialog dlg(file_paths, compat, this); + connect(&dlg, &QDialog::accepted, this, [&]() { - // Let the user choose the packages to install and select the order in which they shall be installed. - pkg_install_dialog dlg(file_paths, compat, this); - connect(&dlg, &QDialog::accepted, this, [&packages, &dlg]() - { - packages = dlg.GetPathsToInstall(); - }); - dlg.exec(); - } - else - { - packages.push_back(game_compatibility::GetPkgInfo(file_paths.front(), compat)); - } - - if (packages.empty()) - { - return true; - } + packages = dlg.get_paths_to_install(); + precompile_caches = dlg.precompile_caches(); + create_desktop_shortcuts = dlg.create_desktop_shortcuts(); + create_app_shortcut = dlg.create_app_shortcut(); + }); + dlg.exec(); if (!from_boot) { @@ -1178,7 +1086,7 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo if (!bootable_paths_installed.empty()) { - m_game_list_frame->AddRefreshedSlot([this, paths = std::move(bootable_paths_installed)](std::set& claimed_paths) mutable + m_game_list_frame->AddRefreshedSlot([this, create_desktop_shortcuts, precompile_caches, create_app_shortcut, paths = std::move(bootable_paths_installed)](std::set& claimed_paths) mutable { // Try to claim operations on ID for (auto it = paths.begin(); it != paths.end();) @@ -1196,7 +1104,12 @@ bool main_window::HandlePackageInstallation(QStringList file_paths, bool from_bo } } - ShowOptionalGamePreparations(tr("Success!"), tr("Successfully installed software from package(s)!"), std::move(paths)); + CreateShortCuts(paths, create_desktop_shortcuts, create_app_shortcut); + + if (precompile_caches) + { + PrecompileCachesFromInstalledPackages(paths); + } }); } @@ -2431,49 +2344,7 @@ void main_window::ShowOptionalGamePreparations(const QString& title, const QStri dlg->hide(); dlg->accept(); - std::set locations; - -#ifdef _WIN32 - locations.insert(gui::utils::shortcut_location::rpcs3_shortcuts); -#endif - if (create_desktop_shortcuts) - { - locations.insert(gui::utils::shortcut_location::desktop); - } - - if (create_app_shortcut) - { - locations.insert(gui::utils::shortcut_location::applications); - } - - if (!locations.empty()) - { - std::vector game_data_shortcuts; - - for (const auto& [boot_path, title_id] : paths) - { - for (const game_info& gameinfo : m_game_list_frame->GetGameInfo()) - { - if (gameinfo && gameinfo->info.serial == title_id.toStdString()) - { - if (Emu.IsPathInsideDir(boot_path, gameinfo->info.path)) - { - if (!locations.empty()) - { - game_data_shortcuts.push_back(gameinfo); - } - } - - break; - } - } - } - - if (!game_data_shortcuts.empty() && !locations.empty()) - { - m_game_list_frame->CreateShortcuts(game_data_shortcuts, locations); - } - } + CreateShortCuts(paths, create_desktop_shortcuts, create_app_shortcut); if (precompile_caches) { @@ -2485,6 +2356,55 @@ void main_window::ShowOptionalGamePreparations(const QString& title, const QStri dlg->open(); } +void main_window::CreateShortCuts(const std::map& paths, bool create_desktop_shortcuts, bool create_app_shortcut) +{ + if (paths.empty()) return; + + std::set locations; + +#ifdef _WIN32 + locations.insert(gui::utils::shortcut_location::rpcs3_shortcuts); +#endif + if (create_desktop_shortcuts) + { + locations.insert(gui::utils::shortcut_location::desktop); + } + + if (create_app_shortcut) + { + locations.insert(gui::utils::shortcut_location::applications); + } + + if (!locations.empty()) + { + std::vector game_data_shortcuts; + + for (const auto& [boot_path, title_id] : paths) + { + for (const game_info& gameinfo : m_game_list_frame->GetGameInfo()) + { + if (gameinfo && gameinfo->info.serial == title_id.toStdString()) + { + if (Emu.IsPathInsideDir(boot_path, gameinfo->info.path)) + { + if (!locations.empty()) + { + game_data_shortcuts.push_back(gameinfo); + } + } + + break; + } + } + } + + if (!game_data_shortcuts.empty() && !locations.empty()) + { + m_game_list_frame->CreateShortcuts(game_data_shortcuts, locations); + } + } +} + void main_window::PrecompileCachesFromInstalledPackages(const std::map& bootable_paths) { std::vector game_data; diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h index 960a70c722..40d874e11c 100644 --- a/rpcs3/rpcs3qt/main_window.h +++ b/rpcs3/rpcs3qt/main_window.h @@ -149,6 +149,7 @@ private: static bool InstallFileInExData(const std::string& extension, const QString& path, const std::string& filename); bool HandlePackageInstallation(QStringList file_paths, bool from_boot); + void CreateShortCuts(const std::map& paths, bool create_desktop_shortcuts, bool create_app_shortcut); void HandlePupInstallation(const QString& file_path, const QString& dir_path = ""); void ExtractPup(); diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.cpp b/rpcs3/rpcs3qt/pkg_install_dialog.cpp index 370f4d053c..9b0f926484 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.cpp +++ b/rpcs3/rpcs3qt/pkg_install_dialog.cpp @@ -7,12 +7,11 @@ #include "Emu/system_utils.hpp" #include "Utilities/File.h" -#include #include #include #include -#include #include +#include enum Roles { @@ -105,8 +104,6 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil item->setData(Roles::VersionRole, info.version); item->setData(Roles::DataSizeRole, static_cast(info.data_size)); item->setToolTip(tooltip); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(Qt::Checked); } m_dir_list->sortItems(); @@ -134,42 +131,79 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil } }); - connect(m_dir_list, &QListWidget::itemChanged, this, [this, installation_info, buttons](QListWidgetItem*) + QHBoxLayout* hbox = nullptr; + if (m_dir_list->count() > 1) { - UpdateInfo(installation_info, buttons); - }); + for (int i = 0; i < m_dir_list->count(); i++) + { + if (QListWidgetItem* item = m_dir_list->item(i)) + { + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Checked); + } + } + + connect(m_dir_list, &QListWidget::itemChanged, this, [this, installation_info, buttons](QListWidgetItem*) + { + update_info(installation_info, buttons); + }); - QToolButton* move_up = new QToolButton; - move_up->setArrowType(Qt::UpArrow); - move_up->setToolTip(tr("Move selected item up")); - connect(move_up, &QToolButton::clicked, this, [this]() { MoveItem(-1); }); + QToolButton* move_up = new QToolButton; + move_up->setArrowType(Qt::UpArrow); + move_up->setToolTip(tr("Move selected item up")); + connect(move_up, &QToolButton::clicked, this, [this]() { move_item(-1); }); - QToolButton* move_down = new QToolButton; - move_down->setArrowType(Qt::DownArrow); - move_down->setToolTip(tr("Move selected item down")); - connect(move_down, &QToolButton::clicked, this, [this]() { MoveItem(1); }); + QToolButton* move_down = new QToolButton; + move_down->setArrowType(Qt::DownArrow); + move_down->setToolTip(tr("Move selected item down")); + connect(move_down, &QToolButton::clicked, this, [this]() { move_item(1); }); - QHBoxLayout* hbox = new QHBoxLayout; - hbox->addStretch(); - hbox->addWidget(move_up); - hbox->addWidget(move_down); + hbox = new QHBoxLayout; + hbox->addStretch(); + hbox->addWidget(move_up); + hbox->addWidget(move_down); + } - QLabel* description = new QLabel(tr("You are about to install multiple packages.\nReorder and/or exclude them if needed, then click \"Install\" to proceed.")); + QLabel* description = new QLabel(m_dir_list->count() == 1 + ? tr("Do you want to install this package?") + : tr("You are about to install multiple packages.\nReorder and/or exclude them if needed, then click \"Install\" to proceed.") + ); + QLabel* label = new QLabel(tr("Would you like to precompile caches and install shortcuts to the installed software?")); + + QCheckBox* precompile_check = new QCheckBox(tr("Precompile caches")); + connect(precompile_check, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ m_precompile_caches = state != Qt::CheckState::Unchecked; }); + precompile_check->setChecked(true); + + QCheckBox* desk_check = new QCheckBox(tr("Add desktop shortcut(s)")); + connect(desk_check, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ m_create_desktop_shortcuts = state != Qt::CheckState::Unchecked; }); + +#ifdef _WIN32 + QCheckBox* quick_check = new QCheckBox(tr("Add Start menu shortcut(s)")); +#elif defined(__APPLE__) + QCheckBox* quick_check = new QCheckBox(tr("Add dock shortcut(s)")); +#else + QCheckBox* quick_check = new QCheckBox(tr("Add launcher shortcut(s)")); +#endif + connect(quick_check, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state){ m_create_app_shortcut = state != Qt::CheckState::Unchecked; }); QVBoxLayout* vbox = new QVBoxLayout; vbox->addWidget(description); - vbox->addLayout(hbox); + if (hbox) vbox->addLayout(hbox); vbox->addWidget(m_dir_list); + vbox->addWidget(label); + vbox->addWidget(precompile_check); + vbox->addWidget(desk_check); + vbox->addWidget(quick_check); vbox->addWidget(installation_info); vbox->addWidget(buttons); setLayout(vbox); - setWindowTitle(tr("Batch PKG Installation")); + setWindowTitle(tr("PKG Installation")); setObjectName("pkg_install_dialog"); - UpdateInfo(installation_info, buttons); // Just to show and check available and required size + update_info(installation_info, buttons); // Just to show and check available and required size } -void pkg_install_dialog::UpdateInfo(QLabel* installation_info, QDialogButtonBox* buttons) const +void pkg_install_dialog::update_info(QLabel* installation_info, QDialogButtonBox* buttons) const { u64 data_size = 0; u64 free_space = 0; @@ -182,7 +216,7 @@ void pkg_install_dialog::UpdateInfo(QLabel* installation_info, QDialogButtonBox* for (int i = 0; i < m_dir_list->count(); i++) { - if (m_dir_list->item(i)->checkState() == Qt::Checked) + if (m_dir_list->count() == 1 || m_dir_list->item(i)->checkState() == Qt::Checked) { data_size += m_dir_list->item(i)->data(Roles::DataSizeRole).toULongLong(); } @@ -197,7 +231,7 @@ void pkg_install_dialog::UpdateInfo(QLabel* installation_info, QDialogButtonBox* buttons->button(QDialogButtonBox::Ok)->setEnabled(data_size && (data_size <= free_space)); } -void pkg_install_dialog::MoveItem(int offset) const +void pkg_install_dialog::move_item(int offset) const { const int src_index = m_dir_list->currentRow(); const int dest_index = src_index + offset; @@ -211,14 +245,14 @@ void pkg_install_dialog::MoveItem(int offset) const } } -std::vector pkg_install_dialog::GetPathsToInstall() const +std::vector pkg_install_dialog::get_paths_to_install() const { std::vector result; for (int i = 0; i < m_dir_list->count(); i++) { const QListWidgetItem* item = m_dir_list->item(i); - if (item && item->checkState() == Qt::Checked) + if (item && (m_dir_list->count() == 1 || item->checkState() == Qt::Checked)) { compat::package_info info; info.path = item->data(Roles::FullPathRole).toString(); diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.h b/rpcs3/rpcs3qt/pkg_install_dialog.h index 2122259a1d..82593ab6b5 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.h +++ b/rpcs3/rpcs3qt/pkg_install_dialog.h @@ -18,11 +18,17 @@ class pkg_install_dialog : public QDialog public: explicit pkg_install_dialog(const QStringList& paths, game_compatibility* compat, QWidget* parent = nullptr); - std::vector GetPathsToInstall() const; + std::vector get_paths_to_install() const; + bool precompile_caches() const { return m_precompile_caches; } + bool create_desktop_shortcuts() const { return m_create_desktop_shortcuts; } + bool create_app_shortcut() const { return m_create_app_shortcut; } private: - void UpdateInfo(QLabel* installation_info, QDialogButtonBox* buttons) const; - void MoveItem(int offset) const; + void update_info(QLabel* installation_info, QDialogButtonBox* buttons) const; + void move_item(int offset) const; QListWidget* m_dir_list; + bool m_precompile_caches = false; + bool m_create_desktop_shortcuts = false; + bool m_create_app_shortcut = false; }; From 01fe12483f2c0c7b4b32ce427e9eb7d31c265565 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sun, 7 Dec 2025 13:05:15 +0100 Subject: [PATCH 37/59] Qt: remove unused NotifyBatchedGameActionFinished signal --- rpcs3/rpcs3qt/game_list_frame.cpp | 3 --- rpcs3/rpcs3qt/game_list_frame.h | 1 - rpcs3/rpcs3qt/main_window.cpp | 9 --------- rpcs3/rpcs3qt/main_window.h | 1 - rpcs3/rpcs3qt/pkg_install_dialog.h | 2 +- 5 files changed, 1 insertion(+), 15 deletions(-) diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index f68bc44896..677b61a44c 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -2501,13 +2501,11 @@ void game_list_frame::BatchCreateCPUCaches(const std::vector& game_da if (total == 0) { QMessageBox::information(this, tr("LLVM Cache Batch Creation"), tr("No titles found"), QMessageBox::Ok); - Q_EMIT NotifyBatchedGameActionFinished(); return; } if (!m_gui_settings->GetBootConfirmation(this)) { - Q_EMIT NotifyBatchedGameActionFinished(); return; } @@ -2531,7 +2529,6 @@ void game_list_frame::BatchCreateCPUCaches(const std::vector& game_da { if (serial.empty()) { - Q_EMIT NotifyBatchedGameActionFinished(); return false; } diff --git a/rpcs3/rpcs3qt/game_list_frame.h b/rpcs3/rpcs3qt/game_list_frame.h index c31e6aa66a..bcdd643ca2 100644 --- a/rpcs3/rpcs3qt/game_list_frame.h +++ b/rpcs3/rpcs3qt/game_list_frame.h @@ -96,7 +96,6 @@ Q_SIGNALS: void FocusToSearchBar(); void Refreshed(); void RequestSaveStateManager(const game_info& game); - void NotifyBatchedGameActionFinished(); public: template diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 3ba64487e6..36b2891f63 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -3392,15 +3392,6 @@ void main_window::CreateConnects() connect(ui->mw_searchbar, &QLineEdit::textChanged, m_game_list_frame, &game_list_frame::SetSearchText); connect(ui->mw_searchbar, &QLineEdit::returnPressed, m_game_list_frame, &game_list_frame::FocusAndSelectFirstEntryIfNoneIs); connect(m_game_list_frame, &game_list_frame::FocusToSearchBar, this, [this]() { ui->mw_searchbar->setFocus(); }); - - connect(m_game_list_frame, &game_list_frame::NotifyBatchedGameActionFinished, this, [this]() mutable - { - if (m_notify_batch_game_action_cb) - { - m_notify_batch_game_action_cb(); - m_notify_batch_game_action_cb = {}; - } - }); } void main_window::CreateDockWindows() diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h index 40d874e11c..8a82c125ce 100644 --- a/rpcs3/rpcs3qt/main_window.h +++ b/rpcs3/rpcs3qt/main_window.h @@ -49,7 +49,6 @@ class main_window : public QMainWindow bool m_save_slider_pos = false; bool m_requested_show_logs_on_exit = false; int m_other_slider_pos = 0; - std::function m_notify_batch_game_action_cb; QIcon m_app_icon; QIcon m_icon_play; diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.h b/rpcs3/rpcs3qt/pkg_install_dialog.h index 82593ab6b5..f1a623009d 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.h +++ b/rpcs3/rpcs3qt/pkg_install_dialog.h @@ -27,7 +27,7 @@ private: void update_info(QLabel* installation_info, QDialogButtonBox* buttons) const; void move_item(int offset) const; - QListWidget* m_dir_list; + QListWidget* m_dir_list = nullptr; bool m_precompile_caches = false; bool m_create_desktop_shortcuts = false; bool m_create_app_shortcut = false; From 2579ddf99603a9d60a76b7d7a545272e6cb008aa Mon Sep 17 00:00:00 2001 From: Ninetime Date: Mon, 29 Dec 2025 00:12:21 -0500 Subject: [PATCH 38/59] RPCN menu changes (#17936) - Adds a shortcut for RPCN to the toolbar - Adds a link for the Network tab to the Configuration menu --- rpcs3/Icons/rpcn.png | Bin 0 -> 15262 bytes rpcs3/resources.qrc | 1 + rpcs3/rpcs3qt/main_window.cpp | 9 +++++++-- rpcs3/rpcs3qt/main_window.ui | 22 ++++++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 rpcs3/Icons/rpcn.png diff --git a/rpcs3/Icons/rpcn.png b/rpcs3/Icons/rpcn.png new file mode 100644 index 0000000000000000000000000000000000000000..1470fa79999dd1e65761d4345832cbd616540c9b GIT binary patch literal 15262 zcmeIZ`8(8K_&@%fr@^Lolp?!qqex^=q%2d3EZGZL zlU>LlJD)SW-tX`A{R2L~eY&o$am}1_p7(w3{c+#VC^HjXdRi`82!iPK^|Z_(2nK(` z5Q++XtOoXOfe#ulJsTegvezO1N3bSno`WDXq_1`ELLg#c*gaaP&#!;^{VDkmIYJK= z#Gg7EoD_f7VbgfBrdRY)W;TzcrI85ASR(SWVAk<3rx_`NK8_ye#yODb`>7bG_`bfHzUkoBe@nH=E)#4vPD^i=cb4xg&yzxL?z}Hl2|p#7X}VaO zB%f&^Rkfe6&!i|c_ODU4`bLGi`+oI}s_l|%16RVAha7%7jXX|Ho|G}kY@PqzH{o|C z?xw?ZtGlC9pI^oCu<-Mh>vW`Ng?TF{gQPlYM`A|)h5I^sc)EnI+MQgg7@|_C{#coQ zUl40ImU1^h!=qlP%6KrFnFnsB{bMkpE?ISL(qCr4viC?j1IAdr6q->VQ(PZp zQ!E*1xfiZ3&YvF5O7&d;htYuTQd`%)X%zSTzKj@>-DsI! zS(E?rXUF0z`w4;0I^u*v*5K3U@f$N9-kR(S)6vXKu0m*)y`3IXuE4s2#Jxl&4B;9a zj2Yug)Jfef=U0Z*1}9uil3|l4|H7Enc4aQUqC+4VP_EQt8+fZ|)oco^5YyeK3DERr zyaC5Y0jvf_1@TgcVg^d-mq7{eBh)&YfC`~{8B6_aagQMPoY;$0;$!MtFY3$pKP%n2 zarUtegr9_OJKgwj~zei&)HkSj6a9cQWqwoi0Ddrt^o|u$g}nw zw>29A`G;mE#IW~KHtkK5p;Q-1&%Jbu(^OEj$F{k^YmH8@gnUOt4}Ih2}R} z;LsY`$v;dga1^a?xB-p<^W2X15;e=1eT1AXEudi%{%uO*`!56G3ng8><&d36kd>}EklDC3xLtCh3@9#*)Fh!2?w_aV3lwejuk&P;6)>stb&4#ZCUOF|?2 zH;)Q@Q((sDP~Kk1JBH8;{(#<~g8%GrkCjQlC|?o6(#3OiF+$cI&*ix!Gzjk@mHoM^ zg@Dgnl&fhJc51>HYP5$&er*WCe{VR=LJ&q(h<5%APqTm)gzv>2p&MGe6LZ}+@5lo^ zR}>nDIfnl+(qucYpAhxTg)vf!*cCB>G*bhc1Ug&#sCp2t!W^v5_Lz{ijIi35x9C0r zQwT>Xj6|G~A@Dt^c-{mk3rV+scArS3m<7k>N#;PX2bCjewi%CzhH~QV1*Bz(?e#TmhQL~6kp)?#9E#3I4LyCNE(b}D0)Y&c9wT*JxX#xHUFdSQnTL<; z+I284^W!kC`0hSf4rWwlOBhXrH1{3!Esnw{Yj&~SVNqX5D(`833$5I$ljXxnMhq4O zn2;>nusi6*e7^~=h zx6&LNpRsQs$c};Vm*}K+?y~aWE_rh5SB+HfEaz}z{*nb6h}d<+RgW^h^KwOp8uj8Z zml31ykhBE;FP5FZR%)`5N4qxE1e;wj1x6rGM`1B zo@OUK+ZshlRugmLj6bJzPA8Y*(yXWXckc{AlsbG?z+dAuOANs=|iubBA`NuUO z#0uPAR4cCjBD{#I1%=!6Wg?)w7PG#A3ubgZhw4w+FZEeaDUWsu%lEM-G>;;XX-+fCUQJa6&))-+8&bH2M9nQ3+N{1 zt#&UuJYvB3sr;C+C0?uYbr4=7>sKwCmwVOq!5L`v^ye_Qlst;gJl}41Bx9t+m~O~Y z4>SX58B-)CGh(Nm;3#b}JLPb=?=LEcBnFJliKIuuUhaht^Em(kGcRy_4)1M$u6Za! zVQn7uKYiWnpJ}?231ESj@+hzZN0DXL#NoGrygmn1xzUCKpJ}XQIj=)h3aChX_KJLs zoC)C0?-&F;eC)URl=ZX7Rd9#Ia@>vxaUmMAZf7iu!hN8nI=EPWO43`wrRNl##p1bd zWyUxWuEPm169zj2%Zvt!^5C*PDW?XP&a^$y!;ux*4VI~Y#(;6t=XR{|h3+T&*hWL} zFUlv~2vm#_y)iP~&a+&3UV!W&+SvLD6mos{IKH6*O%N%~lz6Z|2DzimgX7J8lV^Mh zNyv-JwE>aWlo(R4LygWdMyApMVS?yQyj)w1teI9$EH2EaMj)vK$HJ0U zrj%2BMF-7vpg3b5ydVxoUfKVR{wW)5PhqC#6$9w55G6K2gKu(jFt9HKe>9hZQAezC zrideoo6QOt?ogaQ8~z|K$aII8TYi{nx*)NyQuBrZBjz{@h~H2ALp84BWH^1PlAthI z)#U;GZMeUzKrle`dvsqvvgJWrSyK~sUr`v6cn$&>qzQ3F8#CptfdS+L8hrVQ90^Fo;8^v)t;e|tZpe)Lmr1i=80a2a7fs%_s zhF+#Fsp|Z{c;t@m0M2n#qE1! z4)M0whwC`GaJ|E|W-;NN2RnHp5;N+@+2&^|g(vl<69DB@;sjlap`R!FNA*1^hvH68 z(uX9c)4QknZEmf^t;7x{dX(F>7iG8%x_J&3{FLtzlH>2p#Vzin{$x^=`I7FD_bM@0 zPZ!m%dkWW2m z`MKz}JH!Q-E5kIXhS2yYHUV(HzOok2`G-nnH&68f=foKMok>mx`^&gn<49vqf_-3c zaNg@4{Z=pM0F9(CSHpQDQl=~vHlSluuj^^7x3@)-*L<$uG&USea*V*}5*pc^#iLGr zx|WDQmG->QS?MB|TZ7IY3d-(z(GsY*UZb)0#BpxlbB#}J`JYbJe!to3 zX8-by5jD{_7{!2Zfqw*)29Kn~P+OY3j$$kHwEOW}{%ON3dghd|```ZOkY4{!|j*mvVkjS@eJOR>Rz@ISHE~9%(a`N}*NuO|74Jic~J7POF}h z9*b!44Yazwy^;7p)s~#2O7KrLv5)DMg;JSshlo^7`f!IdCsa!7APhXDqoIVm&L@yJ z=!7=!h>PA#V;&i@`LDJ544fx?Ki^=E_i%aI_aw3R~>!{fLl%Z|`Q-razfhMC6_6a^Bx`;O3UOw>1mp#0ooWVXH9(p55C;-UFsmp1eMS zwB!XGT~et9vyf1{1gwqCCNGGms#f8sj6POIX+B&D*zWiKU*3mYRqJwpHEZLlWF2s+ zC;n%F+y5@e2~K!f*mo_wA?@ujy%ga5W_{L6=)zf2OSf7yNFOiJ41cOReW{;*qC8iN(wnUBA6B1)f9KgSTVaof7|bdhCS zJtrh`+gsrL$MDRPtc-{#1$o#E8 zYXj@5mGXq1|DJk&l9N#oqKenhLtSA1`y%a1#^uDmM^&FJYYOtKpW#f!t%4^y+Uu{} zo;}p)Lb|Vch_g4;8e5l@*83-$RQ1lG6HSQm;#k|LrxuXq$#cgaAvr}Hh2;0wSodD8 zi5bgwI~y1hw<^E*U%VAkp4s{E+ofe*T2L@&UYL*Qo2Upjp)dSp()t!kdeApeEa4%X z4!=S*j-!Oo*v-eC-ZyA+PG605j455adaIjmKShGpTHX9r|8e%EY)B<~OaJY^bz_F(}l%CzVgh?gRjMO?F`|ESKtpegM3LF|>L3HP^*DhAz1(tYG^*I(IR%yr{fI_!#T zDj+}N7d>!SG&vI0jF+<*A{`>UY0~IZ7>I?a4yKtb+#>FBtxfsL(&)bn6=G9;3v>V4 zMD-sU_0|B%u{`V&THH)IDZRPpji=RW)pKaOX*}FZ{7M9Ix8!&DWFGZ&epcZT1QS zTa9176URGDljKP?y9+Ikb&n~|WqtDa%`XVu6f@V+Lkwrj-D6V}|FT~^9NH<|GR4sOr&?{XH6@IQ{OT3#hg@zZXkb9 z&Op|OWl4q&e4DQm_V*HK=Letp(gy7Frd(}Ip7?j9-Yp^Fs;Bs zpy+Iid3(52&m5c+cU{o+FhW10IkH;?c`()4txb{MhNRm?=uZ78j&rJbZ{VmBrxf5D zn&mQ2dcn#~Qn}_%vgd29?WkQRDaM;5sPc8J{LJQ3&uWX$D<9d*nQ!S?R^Gzy*w@X3 z@2@ql=<_5`x87+E4Qlu=yD@|{XQAUPB`lKHXYNU z0xgOwb?@;uw|r}MxsN-f870=)#6X;IRgce5C&M%3T%?U<#*i8->9eVx)EL^o-Sv2d ztHu5BC$yI~dXj;dl-{!+bZk61pYM4fVwC?1@1oK-KMqEtM4 ziT6m>e|ZhsOc$eG@|V*i|Hk^=9+rV3mt1I}(R5W^*@5?;cw{a>Dj!(b3}eb67h?Mk@v z>GMNQF<2e+Rcc(!+Bcy)d4~`26=r-*L73h=Hn$|v+I#0fjfl~hXa|vvxl@yp?n7r*lBf9M9qH!ID#)$hW_kazLKnb_%v&Dr#9HM4~IJbr&dXr zud;CW>z#qkrWUESgUSG1s)g=F5ykUYHsXR)50gU(W}?*ah59#jjl?b<>Zh3wVSxb} zU9{54a5>0S0VLAtPmS~0d&*+4cF0Nslj|D_fIdVkwjN3xiy;&-Kq?Uw%|ggSs#|C) zx&YNqmT3ScAgi{VKh=O$YEh1I54vY0y7&%`N%PAUf-=%2M5WsJ1_d%*pF@Nqx9&io; zsC3E?TAkSs{6?~-CqII~2Org!_ee_P-Aq>6>a*l%^=SR=5Qc9qCf4L+#(4qk@7uAb z6Voq6vyjImIKWmctXo8!Tc3NZJ z`ei%uh{!0Ey!b&bqBap;NQKdFD+u~votWm0K|((YyC41jiAm)g1J%MUaIM~_e&@5`uj9PqsL zDk3T_!nxV#DkWqQpn0gwskjIB^=sIeKscz48 zu%lJF?8R<1v7{|5k>d_s;fJ?^F&njXA0?l|2eeASi*MViWm`^pcU&9U*R`>T`L|vA zIZGRh#9HROn4WK+OtZf|@bvU|vKqE$1la6dog$sNLcRYUS0kdbVKw=vzm7kvaf^?% zLs?xbrV<_&hH~_U1YB(X?axOn>L!n{<%buL4QWeEZdcen?1t0eO)f3dyqgvGCl@9G z4$BPc9I|<`;MY&vr25U%MXOU;NBI3Xv`E5J5Vmwe{jB^$e4)9YWxNM#xj_{CX;yfi ziU-H#T6mkR<(EgW)j8KAZXRyV+RKpz<5EVN5=MFTD4Y``^x_RO~Ep|bsWgV)R*S~)7y)flayfDX= zG7MOZyq#-(aQ*jS@6{zgBu+dD=feStcd5`hZ3uss?yop*a$vG$fAZ|d>6W92x;7NX z>#D?o?@M6@{`(%&GJh5v8EN_*oK5TmC^vVDq9AlX?wlC;E% zH&a?%YwN$ga5>;{htn%X?jCRrYh@tYoa4nxNKm zWn{d#GXya(du{)M$Dnt-{`#&_EnBQ9;;$N1`%Q)(pp!@gn$kSkFTGXw6e&BaqPuy? zazh|3X%*|JvAIkKNN5;y?4|Ja3sXZ%*AsX*eO5>F{#d#_`p_VbGr&p#bPXjWisl>j zsh8Le9@v~0+79*K-e%R@a|&kw>2K^A=qG?>$UWowqycDqi5!uR z8I`R%8olJOw=2`SxH>W_qPzYR!D$-qn;-a94M4X$uB)ecaJgD-WHWh=+PUBRzW&YV z=(1A?npS;WVGqHWd4Y{{p>|h<2Up-GMGm0~9GyH{esmuD1wqRRIHEV=nDmwa4{lPY zjZEGg%8OL{glpbc{IB_l%gx^Z$q7V|?SQhU)VB|m7elbXCr@aY7YolOq_-U0Bx9s3 zz$_!PkOKd$kjIBEe4V#F#)E7BnvXgRh#LSSu77^M6Re~A-*t$r%@Q{kUr1RnPvB50 z<8;8cIdzN8Kn)#~Oo#EdGZnWUqVoSc4j{3zuB@Rf*bV!gu>A42%?sc2o3@_qMe5%A zaE?!9+9Q^4W229hLW6gK^IWDbdv7;Ryd1P#w&uOMlse@5L4r5J;F=sijyOi?cmYvI zjUMv%GJiNy>{K(DSmN4WSbx_2pE2jn1j3+xb^z)53Ju!?+ybr2K zmK{DnA8BBx67kq5hE&nUQ+-cG5DIpC5o5EweHkXFx{zHf62k6Koy3I)RO`@91ng}T%+p@dw6Ae z=d17%mAz-v&abUbziku`D)f_IGsgrpp4ACgn(S>|h|=Nr!i-%F@;7nHrtl)>cE`u+ zy12lh|NP?F#-M?VPn($pIlPKAPM(bzyc)W9KJ6A08)%TS zzN=a3TU&c|=MmJZKW7C~*+LMv7I_w+DObaB)=B2!7sIDbUrzs+=2YFPH)^EJOBN5r zVOn4l|Gn^#_msZS%Tr0}l{+F1S|*}|SiR|RU5zke;JX`EjVxM$s07CTB$pf(FPG;O zUSFIC1Szbp%F->7YNUfId^V3AHD}bzzMTKW7dG#I1|ZKb@PiTecU0IDKd!WI?7YWI zzOkYzpgTS@oM?OJo-^(7tvk`j+8(}_cHA!)xN=nxN7TYD*5Mh5EeoIbdt0=PqzThO zDa?%hCg{dlM$^`q$n=bGd=n_|qKO^f9$kpRaQ;ja^_hH|@T6N|j(YN6Z{pveD;Lmj z7YvRKR7QIk-MvSo#aAz#0f^T{Lz3&(K(4X#)lkH){-}azWs$;)PVZ;-ew*G#?Y)WON{{b>WJS4DE(ywAz*DoIN@pMnq)Kh4 zs_)*ZuL>-lRoWx=sXb%xOZixv>Gme9u`JN23hgYxuRjAFz>o#c^}@ ziU7}5LbE?K@j&bBnYM&3H)I@(_4}2+ADsy6^qvG2$jNC)YKoIluZkkpB^{P4MKHmz zB+?-4Mlu7@`3qQ3BIUvEa|G3ikc&lXU;(+`&x22yDIxfyRPj3Hwye z5MtvQCB_*UQm=; zKLiiro>E{5d@(S0P>Hv(iPf(3y`6>HP93k`_&07dHC@vediOKK-=BBWA}l_Yu_(h~ z9R0=J7-i^UMYl~718%|I;hIWKZdcfLcQx?3cxyPfcqfbD2?Mc0CxW*JQJT>TSc!)E zM=B#d-W~inl}#a6)RonA=3kz8?F||poa4(x9sbT*;`(08wI4Y@5}mf>WdXLZa-loo zF%vYXWK>P@7kAV4xh7T-I83#5r~O|s(Q}n43>XKc!TGQ@|G+yjG6M{_9nQ`G`m-Sr z8mmRzu6>YpSA!ahADP;Xcvlv*-$OTL8!?bk%KMA>+pv;iYtH^jGbXB~Gq zE2xsPZFT06umGLVTLfrauNEo%MLwK7}{}8~~L;lNJ)nCSC{03jtS1_G{-Acq_TQ*lM*ap9hgVzo* z3Q5*E8}z9X>Z|_gd~2@{T`ZJsu5?k;Y+~lx`j1 zKaG6PG&2!~bBE@HEdpxFItI&EN|IIP-M_TI_UJ3ST$(93u8GdUh^j=~#921*g}k}o zpH!GH*F3Hqf4O|@{Kh+9iL(M7sf>z3=)oqw=6}m~+g?>T=wBn1d{tg= zJ{{DPtr3JkAeBMgR2$>nt3Ir!4E1oS{)cSD;(SN4;@h`Bn}=4s*qeRXg-H1^b^;`Y zAbXXjdl&j!TmAh;U#>Na%HH#!0|#-49Jo4R*58PT%D@V#?6=7&7sxRSL7`Gwmq6~U zq2&}20p`yNIW|+(w)zshXfbRlhq;a`8qKkxCd;w0>maTXu%~YXGUKNrw-0KSQZWKw zYvQ{nt?f?rM@3RI>TOTL1ls>lB~XDReTo11tK)PG6V8U|zm8q09>_ATB=M?>0&g^R z;{RJN#nGQM(7d#om-h9btA9wgqFAY*X~?aHh*BGy%Cp;W$KltUuW&nc_e0Lty{#5Zpe#?FuTfl{7_bxkTcNTTe%kwvp^#{5D7APZ6MPk>z1Z-e~9XQZ$bD6q!Jrjc zC3?U!jPf3g9jXUP5JQcQmdrgwu&}l`2HgK+^8bP)e;DiPdf7}3$)~Pou|Mn+sj|xB zop@n=R2p!*9{IXzi?$__|gzww) zqe=fMa3`2h$?Wvx@`c}b1k9wnJ(_6<$m+Xz%Kz#5#S?OT_*|eFNhDw8H?Pke@n;{- zeft`$`RX1JByC}lTrJCTv=!~82ZkDs3q+ALl0DgNlNt2oDOtRN>2;Hpz9+@@U^jEH z$SrZ8QQ?l4Yi`Y-qLLY`my@o;)CJ|Pqi$|S24INb-E6Re$J+0orlTO^YfQWN zMr$TdvXIj)&-;Uo54n|bA5HKCqa_W^6^jask1f_d*$%L?cekbex^iv)Ez7lA8yWWP zhBg5uHnWWWea>zMCg9LO@$JBxg6Y1{bzQ!mGtvtwaz*T&KD)tNjpOYR``kiSRd``A zC^iNM=ManR?pQQdRgbHl9Q^WY!MQ1L@!?zB>0F>UMR5X~`6Z>U7l+ah;5N?6q zSE~*){;{}rTJUD*z@+=W{?F=*hAk}y6kij6-Rx?y#2iOSv33e_g~?x=GZ_=kp9>ql z$dMzmKurJI_hc{nVBR%(ge52G39X;IZrwz>%BlChduMVC)V$C3o&SF6^{qu8l3|?s z@k(X`Cp2NH$OuJt6`f(hnQ;>Pw(H-iif#?@d%1I~%hapB6X&^YH{oyG$>q+vNP6r! z2~L!FojLqrqi7$!9{lmew?`Xp6`t)95xS1s-;_#L%maa+`SmzII~%ya`?yn{#ix;8a<;`q$PA&+i%V@m4;82W(qVOpllIPwJ49T4t6D>3v@ixA$um`pw51?B}+<9Tb1K z3&~9^6f+%{3=jn^C>8gjs5AFGYV#kvhdSS+*ePm?%Nyw?gKY{ICF$m3rC4AGni&*C zhVtdCa12EIB&_TrQZn^6lcJQ@{IBFd@<3ES2e_Keq_I^C$OsJAc)4>Vno)xxMxft> zm6}bK3@iP66l`8GM=E|P0PQmXE?5lTOA^>h-DIt&DRrTGNGCHk^x#h4IRM1Rl7qEk z{$*D5lkHeHwWQ51V@`KRztaRDvQZqn=#xsKao_1%q_;JK$!Bs|bbaR12j}lL2 zl&<{QOA77xw+I%6P3orl=R1_;{baoy3wmWgIBS4VAP6mYuV|5*y3=s^?`zYeE600O zkz61uxf)r2fOrsNsG`$+k}~XGrZSmL=?ut!%CGK7FuF>C!lUu-W4bX(O>P!*Vgk-z zV$F&nb)07B-i6)v!0#6F@FbEGAvVqGoo~TUm-pu*P3cNZrgu{w^n4lzwUAkU!Rmj^ zg!1khkNIr4-s9XNVA@7tr0LtvPx~|r#7#Gb5-I<^tKN%_@sH#@SMIQVEmug~5M6*WB}V+_F^=z_x|*^-zuq{*{wYG^{%+Vt&u3Zc`Pf5jp0AoN2v z;G--d?UbSn=qxWM;%v5rsP1m)5B2PHCJA^71mzUNW<~UqKR2enXk6IJXDVK;ijC4f z@$S#~bDNqcIAL&pQu!@8_Vxwd)iIl-*>)%OQ=iD_qypIxQse>=_8g4qicz!`nqS-1 zqb@9~LHapSXnAGm`VFKn-2K(!m6+djM`?tZLyI<+0I2EKSLL978UMOcVAyxFk4siN%z4DF4(_jtGKl+P6IKl4X&R2m+K26I(Kj87#?M^+!vVDN<;%gRz>jRR9<)Ut+;zDyp1))}4$~BM;;8HJK zpljIf8ZK{qGPK%D?D4*Q0mI0gF68NX4bkucT5QnN~ksMM{AX2C_LGVH~!s1RH7`6i=>nSYEc?{4}BuK zUNl_y;SZi~J?9DtopCUA`|_xhw=lb*)tLGFY?jSfAg?2WgM-~h3#c3PFRHKF=8udo z-Z~Q>#{l0tCJ6D!S!nwJiG8EDkhr9KJCUb*BQhbwd^=S|MEk7bskV|4qdP1B6{bpF zQQ)r0yxOldY=jNfdM_{YnGA*P=cWZ!?_bFeye@JV2O& zjp;kR&t!D({{D4o@zzFtxqQf6<{Qf7+H*EATG6@@ejYTXym9xj0NLjM}$9Hi%jWwDNq&G2SjKZ$9_d7SB$A-Zmfm-J_eebfW{)R6UiJlpCy=*XgY6u|IjH;#BWMfsT_9p*+H=`hj5)|HvM@#bh z6He3Y+4b@X$%wTvJ1sYZ}q_@)_@}Jm} z!-ONb?U4~7YcV*g^H>E6SJ;Z*JN-0?TsM5AtYfU9lE4sL@Z+q+q=XMW-UH;?8+AmR z5Gc!I5OV$6QRK@rw#5`P=QOUvV2V`6%ic|OIH>4CBt{e8>O}d+esA->OBuOkCMSkS z_N)=3zdEh{s&GB840JR{V$Kd^UF~8fmpwJO(5FAuQBHKHK(T_|VkMr)AJ3KhZbmL= zIt>wLxB6Nu7+#!D)`fDaNlffbP1g6H2H1j$b9%~jyAbV%V`^`*1z$I@fOe}MYi3M& zLo4MeAY%>K`{!ze2&+uCfR`RP7S}$M!0kjI8X3hwW?mB>B(*eamXKgL3f1NhDg3eU z?umH|z;eK)6no10UUlI0DKypyJH?7g8eY+i8FiP^`E(AHQH3;K6Q0t74Hh=o^GB0B zsS1snX#tmLMBUCcO!K0<1&1G0Q5!0cb$Vm+0muN4i9SA67&GLkIO0xu>k&?bbH+^DiN&;%{oR3@ls&v$Al6?e-_0bD-z zA}*u;dmE#bAikUj+U$QFk#pX-klZH95hVuguX^h=2vfEMK>yUYJ{_e@yz6oTsG&VJ zqSr$GU(;QTEalvho6BWHM-N&BS_JYcnI8t?5n9u2K{FR5!PPIrM6*n0om^_du>D#V zCJt4WjTz2R%A$2ebp)J@?7kbE*=8t~peK6?YO!sGGlOo2a%ju7lS;RH? z<^u5;&5V@G1rWvY$e+RLM_75k2y_->(zsPKaQ;4;pc&ekfk|7sH>_=Jj8b594SA8r z(uqD+2^IfV`6-G5Mqn3F&UrYBs!U*HdxYaXo2G_Bd zz;r&Eac);u!{QY)XxN6u4f_(2=b39qi_dLtRF zg}nNX?kcP7Yb(~A6~lK%pwekMd#AK6Gs(#^ZmD&sLI?pf)LkxwSWBP!>AI8eI>*4g zjx#ODv+_-cr;zWr?u<3vY0J&TfeC$#)so*Gjoh;9ba#0XV!o@-pk0ay_tSJti{M=^ zIs^~?%Jk?x1wSFt&WqFpT$d^JUmCo|9iV9_!7@IOgeIqc zp%X?H(l!J;y_q{_4`Ny5=7Ue7$f=iiIlzh8n6~w|_p{$9!^1SbVYJxyE?c?j-FYssBytx2<{4XCQLS190xsMxrr=n zH)^05_nis);H(AGD+fAO-QD!1CELucVCh%`YK*SLYNd35Xh;SV7jTC6h)+~IUxe3v zD`EV_i)W`aSE7_FX$G(GMVvdkw-UB;mBa-)sv=`UeJ8WR&Yfl0X=PU8*=^|7eK$)3 z0REI8Bi=ImqNGn@BQYN7nIA8ME}G;R>GA%QE=R<9fl!cx{LwMEf?72t$nxN3+m6SJ zicUP9%$w93Kdb(JHFGuc)luEq&7C@(Z;8z7AN6E$}L6MjOtgg z&5VEbFu&&oK3f&{B=H6y?WEWF2l=Vr0vI*3;fA1 zII6j;#~m}j{&^a|KQTJee_?U`cW0jq>BVl}d;9+YnONis literal 0 HcmV?d00001 diff --git a/rpcs3/resources.qrc b/rpcs3/resources.qrc index ebace85372..006c71fc8c 100644 --- a/rpcs3/resources.qrc +++ b/rpcs3/resources.qrc @@ -17,5 +17,6 @@ Icons/combo_config_bordered.png rpcs3.svg Icons/DualShock_3.svg + Icons/rpcn.png diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 36b2891f63..9278e7e6b3 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -1777,6 +1777,7 @@ void main_window::RepaintToolBarIcons() ui->toolbar_grid ->setIcon(icon(":/Icons/grid.png")); ui->toolbar_list ->setIcon(icon(":/Icons/list.png")); ui->toolbar_refresh ->setIcon(icon(":/Icons/refresh.png")); + ui->toolbar_rpcn ->setIcon(icon(":/Icons/rpcn.png")); ui->toolbar_stop ->setIcon(icon(":/Icons/stop.png")); ui->sysStopAct->setIcon(icon(":/Icons/stop.png")); @@ -2792,6 +2793,7 @@ void main_window::CreateConnects() connect(ui->confAudioAct, &QAction::triggered, this, [open_settings]() { open_settings(2); }); connect(ui->confIOAct, &QAction::triggered, this, [open_settings]() { open_settings(3); }); connect(ui->confSystemAct, &QAction::triggered, this, [open_settings]() { open_settings(4); }); + connect(ui->confNetwrkAct, &QAction::triggered, this, [open_settings]() { open_settings(5); }); connect(ui->confAdvAct, &QAction::triggered, this, [open_settings]() { open_settings(6); }); connect(ui->confEmuAct, &QAction::triggered, this, [open_settings]() { open_settings(7); }); connect(ui->confGuiAct, &QAction::triggered, this, [open_settings]() { open_settings(8); }); @@ -2907,11 +2909,13 @@ void main_window::CreateConnects() dlg.exec(); }); - connect(ui->confRPCNAct, &QAction::triggered, this, [this]() + const auto open_rpcn_settings = [this] { rpcn_settings_dialog dlg(this); dlg.exec(); - }); + }; + + connect(ui->confRPCNAct, &QAction::triggered, this, open_rpcn_settings); connect(ui->confClansAct, &QAction::triggered, this, [this]() { @@ -3373,6 +3377,7 @@ void main_window::CreateConnects() connect(ui->toolbar_config, &QAction::triggered, this, [=]() { open_settings(0); }); connect(ui->toolbar_list, &QAction::triggered, this, [this]() { ui->setlistModeListAct->trigger(); }); connect(ui->toolbar_grid, &QAction::triggered, this, [this]() { ui->setlistModeGridAct->trigger(); }); + connect(ui->toolbar_rpcn, &QAction::triggered, this, open_rpcn_settings); connect(ui->sizeSlider, &QSlider::valueChanged, this, &main_window::ResizeIcons); connect(ui->sizeSlider, &QSlider::sliderReleased, this, [this] diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index b62e26492e..96ae6b397d 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -277,6 +277,7 @@ + @@ -486,6 +487,7 @@ + @@ -614,6 +616,14 @@ Configure system + + + Network + + + Configure Network settings + + Advanced @@ -1026,6 +1036,18 @@ Configure the emulator + + + + :/Icons/rpcn.png:/Icons/rpcn.png + + + RPCN + + + Configure RPCN settings + + From 65273dde7621592205fa8251209c7e03f9294a0c Mon Sep 17 00:00:00 2001 From: RipleyTom Date: Sat, 27 Dec 2025 16:12:37 +0100 Subject: [PATCH 39/59] Remove call to glxewInit --- rpcs3/Emu/RSX/GL/OpenGL.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/rpcs3/Emu/RSX/GL/OpenGL.cpp b/rpcs3/Emu/RSX/GL/OpenGL.cpp index 78241f557a..7381eb990d 100644 --- a/rpcs3/Emu/RSX/GL/OpenGL.cpp +++ b/rpcs3/Emu/RSX/GL/OpenGL.cpp @@ -38,9 +38,6 @@ void gl::init() #ifdef __unix__ glewExperimental = true; glewInit(); -#ifdef HAVE_X11 - glxewInit(); -#endif #endif } From 9810d24ebb07e936315abea10170dc85c4f78664 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 6 Dec 2025 21:30:42 +0100 Subject: [PATCH 40/59] cellScreenshot: update filenames --- rpcs3/Emu/Cell/Modules/cellScreenshot.cpp | 20 ++++++++------------ rpcs3/Emu/Cell/Modules/cellScreenshot.h | 2 +- rpcs3/rpcs3qt/gs_frame.cpp | 4 +++- rpcs3/rpcs3qt/qt_utils.cpp | 2 +- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp b/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp index b899155dde..5d8e2f2a18 100644 --- a/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp +++ b/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp @@ -33,18 +33,12 @@ std::string screenshot_info::get_overlay_path() const std::string screenshot_info::get_photo_title() const { - std::string photo = photo_title; - if (photo.empty()) - photo = Emu.GetTitle(); - return photo; + return photo_title.empty() ? Emu.GetTitle() : photo_title; } std::string screenshot_info::get_game_title() const { - std::string game = game_title; - if (game.empty()) - game = Emu.GetTitle(); - return game; + return game_title.empty() ? Emu.GetTitle() : game_title; } std::string screenshot_info::get_game_comment() const @@ -52,15 +46,17 @@ std::string screenshot_info::get_game_comment() const return game_comment; } -std::string screenshot_info::get_screenshot_path(const std::string& date_path) const +std::string screenshot_info::get_screenshot_path(s32 year, s32 month, s32 day, s32 hour, s32 minute, s32 second) const { u32 counter = 0; - std::string path = vfs::get("/dev_hdd0/photo/" + date_path + "/" + get_photo_title()); - std::string suffix = ".png"; + const std::string path = vfs::get(fmt::format("/dev_hdd0/photo/%04d/%02d/%02d/%s %02d-%02d-%04d %02d-%02d-%02d", + year, month, day, get_photo_title(), day, month, year, hour, minute, second)); + constexpr std::string_view extension = ".png"; + std::string suffix = std::string(extension); while (!Emu.IsStopped() && fs::is_file(path + suffix)) { - suffix = fmt::format("_%d.png", ++counter); + suffix = fmt::format(" %d%s", ++counter, extension); } return path + suffix; diff --git a/rpcs3/Emu/Cell/Modules/cellScreenshot.h b/rpcs3/Emu/Cell/Modules/cellScreenshot.h index 20a8d41cc5..de84257c7b 100644 --- a/rpcs3/Emu/Cell/Modules/cellScreenshot.h +++ b/rpcs3/Emu/Cell/Modules/cellScreenshot.h @@ -44,7 +44,7 @@ struct screenshot_info std::string get_photo_title() const; std::string get_game_title() const; std::string get_game_comment() const; - std::string get_screenshot_path(const std::string& date_path) const; + std::string get_screenshot_path(s32 year, s32 month, s32 day, s32 hour, s32 minute, s32 second) const; }; struct screenshot_manager : public screenshot_info diff --git a/rpcs3/rpcs3qt/gs_frame.cpp b/rpcs3/rpcs3qt/gs_frame.cpp index df6aac317a..a2f122ecbb 100644 --- a/rpcs3/rpcs3qt/gs_frame.cpp +++ b/rpcs3/rpcs3qt/gs_frame.cpp @@ -1040,7 +1040,9 @@ void gs_frame::take_screenshot(std::vector&& data, u32 sshot_width, u32 ssho } } - const std::string cell_sshot_filename = manager.get_screenshot_path(date_time.toString("yyyy/MM/dd").toStdString()); + const QDate date = date_time.date(); + const QTime time = date_time.time(); + const std::string cell_sshot_filename = manager.get_screenshot_path(date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second()); const std::string cell_sshot_dir = fs::get_parent_dir(cell_sshot_filename); screenshot_log.notice("Saving cell screenshot to %s", cell_sshot_filename); diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index fa5e3c250e..fc53dc542a 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -641,7 +641,7 @@ namespace gui usz byte_unit = 0; usz divisor = 1; #if defined(__APPLE__) - constexpr usz multiplier = 1000; + constexpr usz multiplier = 1000; static const QString s_units[]{"B", "kB", "MB", "GB", "TB", "PB"}; #else constexpr usz multiplier = 1024; From d85b9d0cd10468a60507f738a8fda008ef6022d0 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sun, 7 Dec 2025 10:36:55 +0100 Subject: [PATCH 41/59] cellScreenshot: escape photo_title in path --- rpcs3/Emu/Cell/Modules/cellScreenshot.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp b/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp index 5d8e2f2a18..baf9ee380d 100644 --- a/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp +++ b/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp @@ -50,7 +50,7 @@ std::string screenshot_info::get_screenshot_path(s32 year, s32 month, s32 day, s { u32 counter = 0; const std::string path = vfs::get(fmt::format("/dev_hdd0/photo/%04d/%02d/%02d/%s %02d-%02d-%04d %02d-%02d-%02d", - year, month, day, get_photo_title(), day, month, year, hour, minute, second)); + year, month, day, vfs::escape(get_photo_title(), true), day, month, year, hour, minute, second)); constexpr std::string_view extension = ".png"; std::string suffix = std::string(extension); From 953f9f7e01793a1c40969c5300be67d4fd2f7462 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 27 Dec 2025 17:41:29 +0100 Subject: [PATCH 42/59] cellPhotoExport: Use roughly same path as cellScreenShot --- rpcs3/Emu/Cell/Modules/cellPhotoExport.cpp | 27 +++++-------------- rpcs3/Emu/Cell/Modules/cellScreenshot.cpp | 16 ------------ rpcs3/Emu/Cell/Modules/cellScreenshot.h | 1 - rpcs3/Emu/System.h | 1 + rpcs3/main_application.cpp | 30 ++++++++++++++++++++++ rpcs3/rpcs3qt/gs_frame.cpp | 4 +-- 6 files changed, 39 insertions(+), 40 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/cellPhotoExport.cpp b/rpcs3/Emu/Cell/Modules/cellPhotoExport.cpp index 8a264bc721..473bd435e7 100644 --- a/rpcs3/Emu/Cell/Modules/cellPhotoExport.cpp +++ b/rpcs3/Emu/Cell/Modules/cellPhotoExport.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "Emu/Cell/PPUModule.h" +#include "Emu/System.h" #include "Emu/IdManager.h" #include "Emu/VFS.h" #include "cellSysutil.h" @@ -107,30 +108,16 @@ bool check_photo_path(const std::string& file_path) return true; } -std::string get_available_photo_path(const std::string& filename) +std::string get_available_photo_path(std::string_view filename) { - const std::string photo_dir = "/dev_hdd0/photo/"; - std::string dst_path = vfs::get(photo_dir + filename); - - // Do not overwrite existing files. Add a suffix instead. - for (u32 i = 0; fs::exists(dst_path); i++) + std::string_view extension = ".png"; + if (const auto extension_start = filename.find_last_of('.'); + extension_start != umax) { - const std::string suffix = fmt::format("_%d", i); - std::string new_filename = filename; - - if (const usz pos = new_filename.find_last_of('.'); pos != std::string::npos) - { - new_filename.insert(pos, suffix); - } - else - { - new_filename.append(suffix); - } - - dst_path = vfs::get(photo_dir + new_filename); + extension = filename.substr(extension_start); } - return dst_path; + return Emu.GetCallbacks().get_photo_path(fmt::format("%s%s", Emu.GetTitle(), extension)); } diff --git a/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp b/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp index baf9ee380d..060683bdea 100644 --- a/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp +++ b/rpcs3/Emu/Cell/Modules/cellScreenshot.cpp @@ -46,22 +46,6 @@ std::string screenshot_info::get_game_comment() const return game_comment; } -std::string screenshot_info::get_screenshot_path(s32 year, s32 month, s32 day, s32 hour, s32 minute, s32 second) const -{ - u32 counter = 0; - const std::string path = vfs::get(fmt::format("/dev_hdd0/photo/%04d/%02d/%02d/%s %02d-%02d-%04d %02d-%02d-%02d", - year, month, day, vfs::escape(get_photo_title(), true), day, month, year, hour, minute, second)); - constexpr std::string_view extension = ".png"; - std::string suffix = std::string(extension); - - while (!Emu.IsStopped() && fs::is_file(path + suffix)) - { - suffix = fmt::format(" %d%s", ++counter, extension); - } - - return path + suffix; -} - error_code cellScreenShotSetParameter(vm::cptr param) { diff --git a/rpcs3/Emu/Cell/Modules/cellScreenshot.h b/rpcs3/Emu/Cell/Modules/cellScreenshot.h index de84257c7b..e581400dcd 100644 --- a/rpcs3/Emu/Cell/Modules/cellScreenshot.h +++ b/rpcs3/Emu/Cell/Modules/cellScreenshot.h @@ -44,7 +44,6 @@ struct screenshot_info std::string get_photo_title() const; std::string get_game_title() const; std::string get_game_comment() const; - std::string get_screenshot_path(s32 year, s32 month, s32 day, s32 hour, s32 minute, s32 second) const; }; struct screenshot_manager : public screenshot_info diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index 088fc70ef5..5288d72a1e 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -101,6 +101,7 @@ struct EmuCallbacks std::function get_localized_string; std::function get_localized_u32string; std::function get_localized_setting; + std::function get_photo_path; std::function)> play_sound; std::function get_image_info; // (filename, sub_type, width, height, CellSearchOrientation) std::function get_scaled_image; // (filename, target_width, target_height, width, height, dst, force_fit) diff --git a/rpcs3/main_application.cpp b/rpcs3/main_application.cpp index a1bc443cd0..ff3b70f952 100644 --- a/rpcs3/main_application.cpp +++ b/rpcs3/main_application.cpp @@ -18,6 +18,7 @@ #include "Emu/Io/Null/NullMouseHandler.h" #include "Emu/Io/KeyboardHandler.h" #include "Emu/Io/MouseHandler.h" +#include "Emu/VFS.h" #include "Input/basic_keyboard_handler.h" #include "Input/basic_mouse_handler.h" #include "Input/raw_mouse_handler.h" @@ -36,6 +37,7 @@ #include "Emu/Audio/FAudio/faudio_enumerator.h" #endif +#include #include // This shouldn't be outside rpcs3qt... #include // This shouldn't be outside rpcs3qt... #include // This shouldn't be outside rpcs3qt... @@ -377,5 +379,33 @@ EmuCallbacks main_application::CreateCallbacks() callbacks.enable_gamemode = [](bool enabled){ enable_gamemode(enabled); }; + callbacks.get_photo_path = [](std::string_view title) + { + const QDateTime date_time = QDateTime::currentDateTime(); + const QDate date = date_time.date(); + const QTime time = date_time.time(); + + std::string_view extension = ".png"; + if (const auto extension_start = title.find_last_of('.'); + extension_start != umax) + { + extension = title.substr(extension_start); + title = title.substr(0, extension_start); + } + + std::string suffix = std::string(extension); + const std::string path = vfs::get(fmt::format("/dev_hdd0/photo/%04d/%02d/%02d/%s %02d-%02d-%04d %02d-%02d-%02d", + date.year(), date.month(), date.day(), vfs::escape(title, true), + date.day(), date.month(), date.year(), time.hour(), time.minute(), time.second())); + + u32 counter = 0; + while (!Emu.IsStopped() && fs::is_file(path + suffix)) + { + suffix = fmt::format(" %d%s", ++counter, extension); + } + + return path + suffix; + }; + return callbacks; } diff --git a/rpcs3/rpcs3qt/gs_frame.cpp b/rpcs3/rpcs3qt/gs_frame.cpp index a2f122ecbb..483f5affdc 100644 --- a/rpcs3/rpcs3qt/gs_frame.cpp +++ b/rpcs3/rpcs3qt/gs_frame.cpp @@ -1040,9 +1040,7 @@ void gs_frame::take_screenshot(std::vector&& data, u32 sshot_width, u32 ssho } } - const QDate date = date_time.date(); - const QTime time = date_time.time(); - const std::string cell_sshot_filename = manager.get_screenshot_path(date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second()); + const std::string cell_sshot_filename = Emu.GetCallbacks().get_photo_path(manager.get_photo_title() + ".png"); const std::string cell_sshot_dir = fs::get_parent_dir(cell_sshot_filename); screenshot_log.notice("Saving cell screenshot to %s", cell_sshot_filename); From 953e31cbe89bedb0fc55099435eff3eda7eda216 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 30 Dec 2025 09:10:20 +0100 Subject: [PATCH 43/59] Adjust media list dialog for larger folder depth Use shared_ptr to fix crashes when navigating deeper folders --- rpcs3/Emu/Cell/Modules/cellMusic.cpp | 2 +- rpcs3/Emu/Cell/Modules/cellMusicDecode.cpp | 2 +- rpcs3/Emu/Cell/Modules/cellPhotoImport.cpp | 2 +- .../Overlays/overlay_media_list_dialog.cpp | 74 ++++++++++++------- .../RSX/Overlays/overlay_media_list_dialog.h | 10 +-- 5 files changed, 56 insertions(+), 34 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/cellMusic.cpp b/rpcs3/Emu/Cell/Modules/cellMusic.cpp index 83937f7d16..c23bf274b0 100644 --- a/rpcs3/Emu/Cell/Modules/cellMusic.cpp +++ b/rpcs3/Emu/Cell/Modules/cellMusic.cpp @@ -215,7 +215,7 @@ error_code cell_music_select_contents() const std::string vfs_dir_path = vfs::get("/dev_hdd0/music"); const std::string title = get_localized_string(localized_string_id::RSX_OVERLAYS_MEDIA_DIALOG_TITLE); - error_code error = rsx::overlays::show_media_list_dialog(rsx::overlays::media_list_dialog::media_type::audio, vfs_dir_path, title, + error_code error = rsx::overlays::show_media_list_dialog(rsx::overlays::media_list_dialog::media_type::audio, music_selection_context::max_depth, vfs_dir_path, title, [&music](s32 status, utils::media_info info) { sysutil_register_cb([&music, info = std::move(info), status](ppu_thread& ppu) -> s32 diff --git a/rpcs3/Emu/Cell/Modules/cellMusicDecode.cpp b/rpcs3/Emu/Cell/Modules/cellMusicDecode.cpp index c938f723b1..b7f21b90ad 100644 --- a/rpcs3/Emu/Cell/Modules/cellMusicDecode.cpp +++ b/rpcs3/Emu/Cell/Modules/cellMusicDecode.cpp @@ -134,7 +134,7 @@ error_code cell_music_decode_select_contents() const std::string vfs_dir_path = vfs::get("/dev_hdd0/music"); const std::string title = get_localized_string(localized_string_id::RSX_OVERLAYS_MEDIA_DIALOG_TITLE); - error_code error = rsx::overlays::show_media_list_dialog(rsx::overlays::media_list_dialog::media_type::audio, vfs_dir_path, title, + error_code error = rsx::overlays::show_media_list_dialog(rsx::overlays::media_list_dialog::media_type::audio, music_selection_context::max_depth, vfs_dir_path, title, [&dec](s32 status, utils::media_info info) { sysutil_register_cb([&dec, info = std::move(info), status](ppu_thread& ppu) -> s32 diff --git a/rpcs3/Emu/Cell/Modules/cellPhotoImport.cpp b/rpcs3/Emu/Cell/Modules/cellPhotoImport.cpp index d56db1f060..fbb287d311 100644 --- a/rpcs3/Emu/Cell/Modules/cellPhotoImport.cpp +++ b/rpcs3/Emu/Cell/Modules/cellPhotoImport.cpp @@ -142,7 +142,7 @@ error_code select_photo(std::string dst_dir) const std::string vfs_dir_path = vfs::get("/dev_hdd0/photo"); const std::string title = get_localized_string(localized_string_id::RSX_OVERLAYS_MEDIA_DIALOG_TITLE_PHOTO_IMPORT); - error_code error = rsx::overlays::show_media_list_dialog(rsx::overlays::media_list_dialog::media_type::photo, vfs_dir_path, title, + error_code error = rsx::overlays::show_media_list_dialog(rsx::overlays::media_list_dialog::media_type::photo, umax, vfs_dir_path, title, [&pi_manager, dst_dir](s32 status, utils::media_info info) { sysutil_register_cb([&pi_manager, dst_dir, info, status](ppu_thread& ppu) -> s32 diff --git a/rpcs3/Emu/RSX/Overlays/overlay_media_list_dialog.cpp b/rpcs3/Emu/RSX/Overlays/overlay_media_list_dialog.cpp index cc7affc983..bedbf652c7 100644 --- a/rpcs3/Emu/RSX/Overlays/overlay_media_list_dialog.cpp +++ b/rpcs3/Emu/RSX/Overlays/overlay_media_list_dialog.cpp @@ -8,6 +8,24 @@ #include "Utilities/StrUtil.h" #include "Utilities/Thread.h" +template <> +void fmt_class_string::format(std::string& out, u64 arg) +{ + format_enum(out, arg, [](rsx::overlays::media_list_dialog::media_type arg) + { + switch (arg) + { + case rsx::overlays::media_list_dialog::media_type::invalid: return "invalid"; + case rsx::overlays::media_list_dialog::media_type::directory: return "directory"; + case rsx::overlays::media_list_dialog::media_type::audio: return "audio"; + case rsx::overlays::media_list_dialog::media_type::video: return "video"; + case rsx::overlays::media_list_dialog::media_type::photo: return "photo"; + } + + return unknown; + }); +} + namespace rsx { namespace overlays @@ -203,7 +221,7 @@ namespace rsx return result; } - s32 media_list_dialog::show(media_entry* root, media_entry& result, const std::string& title, u32 focused, bool enable_overlay) + s32 media_list_dialog::show(std::shared_ptr root, media_entry& result, const std::string& title, u32 focused, bool enable_overlay) { auto ref = g_fxo->get().get(uid); @@ -237,7 +255,7 @@ namespace rsx { focused = 0; ensure(static_cast(return_code) < m_media->children.size()); - m_media = &m_media->children[return_code]; + m_media = m_media->children[return_code]; rsx_log.notice("Media dialog: selected entry: %d ('%s')", return_code, m_media->path); continue; } @@ -287,9 +305,10 @@ namespace rsx m_list = std::make_unique(virtual_width - 2 * 20, 540); m_list->set_pos(20, 85); - for (const media_entry& child : m_media->children) + for (const auto& child : m_media->children) { - std::unique_ptr entry = std::make_unique(child); + ensure(!!child); + std::unique_ptr entry = std::make_unique(*child); m_list->add_entry(entry); } @@ -321,9 +340,11 @@ namespace rsx static constexpr auto thread_name = "MediaList Thread"sv; }; - void parse_media_recursive(u32 depth, const std::string& media_path, const std::string& name, media_list_dialog::media_type type, media_list_dialog::media_entry& current_entry) + void parse_media_recursive(u32 depth, u32 max_depth, const std::string& media_path, const std::string& name, media_list_dialog::media_type type, std::shared_ptr current_entry) { - if (depth++ > music_selection_context::max_depth) + ensure(!!current_entry); + + if (depth++ > max_depth && max_depth != umax) { return; } @@ -339,26 +360,27 @@ namespace rsx const std::string unescaped_name = vfs::unescape(dir_entry.name); - media_list_dialog::media_entry new_entry{}; - parse_media_recursive(depth, media_path + "/" + dir_entry.name, unescaped_name, type, new_entry); - if (new_entry.type != media_list_dialog::media_type::invalid) + auto new_entry = std::make_shared(); + parse_media_recursive(depth, max_depth, media_path + "/" + dir_entry.name, unescaped_name, type, new_entry); + if (new_entry->type != media_list_dialog::media_type::invalid) { - new_entry.parent = ¤t_entry; - new_entry.index = ::narrow(current_entry.children.size()); - current_entry.children.emplace_back(std::move(new_entry)); + rsx_log.notice("parse_media_recursive: found '%s' (type=%s)", dir_entry.name, new_entry->type); + new_entry->parent = current_entry; + new_entry->index = ::narrow(current_entry->children.size()); + current_entry->children.emplace_back(std::move(new_entry)); } } // Only keep directories that contain valid entries - if (current_entry.children.empty()) + if (current_entry->children.empty()) { rsx_log.notice("parse_media_recursive: No matches in directory '%s'", media_path); } else { - rsx_log.notice("parse_media_recursive: Found %d matches in directory '%s'", current_entry.children.size(), media_path); - current_entry.type = media_list_dialog::media_type::directory; - current_entry.info.path = media_path; + rsx_log.notice("parse_media_recursive: Found %d matches in directory '%s'", current_entry->children.size(), media_path); + current_entry->type = media_list_dialog::media_type::directory; + current_entry->info.path = media_path; } } else @@ -370,20 +392,20 @@ namespace rsx auto [success, info] = utils::get_media_info(media_path, av_media_type); if (success) { - current_entry.type = type; - current_entry.info = std::move(info); + current_entry->type = type; + current_entry->info = std::move(info); rsx_log.notice("parse_media_recursive: Found media '%s'", media_path); } } - if (current_entry.type != media_list_dialog::media_type::invalid) + if (current_entry->type != media_list_dialog::media_type::invalid) { - current_entry.path = media_path; - current_entry.name = name; + current_entry->path = media_path; + current_entry->name = name; } } - error_code show_media_list_dialog(media_list_dialog::media_type type, const std::string& path, const std::string& title, std::function on_finished) + error_code show_media_list_dialog(media_list_dialog::media_type type, u32 max_depth, const std::string& path, const std::string& title, std::function on_finished) { rsx_log.todo("show_media_list_dialog(type=%d, path='%s', title='%s', on_finished=%d)", static_cast(type), path, title, !!on_finished); @@ -394,12 +416,12 @@ namespace rsx g_fxo->get>()([=]() { - media_list_dialog::media_entry root_media_entry{}; - root_media_entry.type = media_list_dialog::media_type::directory; + auto root_media_entry = std::make_shared(); + root_media_entry->type = media_list_dialog::media_type::directory; if (fs::is_dir(path)) { - parse_media_recursive(0, path, title, type, root_media_entry); + parse_media_recursive(0, max_depth, path, title, type, root_media_entry); } else { @@ -412,7 +434,7 @@ namespace rsx if (auto manager = g_fxo->try_get()) { - result = manager->create()->show(&root_media_entry, media, title, focused, true); + result = manager->create()->show(root_media_entry, media, title, focused, true); } else { diff --git a/rpcs3/Emu/RSX/Overlays/overlay_media_list_dialog.h b/rpcs3/Emu/RSX/Overlays/overlay_media_list_dialog.h index 6a5d39b3fa..e2656d728f 100644 --- a/rpcs3/Emu/RSX/Overlays/overlay_media_list_dialog.h +++ b/rpcs3/Emu/RSX/Overlays/overlay_media_list_dialog.h @@ -29,8 +29,8 @@ namespace rsx utils::media_info info; u32 index = 0; - media_entry* parent = nullptr; - std::vector children; + std::shared_ptr parent; + std::vector> children; }; media_list_dialog(); @@ -39,7 +39,7 @@ namespace rsx compiled_resource get_compiled() override; - s32 show(media_entry* root, media_entry& result, const std::string& title, u32 focused, bool enable_overlay); + s32 show(std::shared_ptr root, media_entry& result, const std::string& title, u32 focused, bool enable_overlay); private: void reload(const std::string& title, u32 focused); @@ -53,7 +53,7 @@ namespace rsx std::unique_ptr icon_data; }; - media_entry* m_media = nullptr; + std::shared_ptr m_media; std::unique_ptr m_dim_background; std::unique_ptr m_list; @@ -61,6 +61,6 @@ namespace rsx std::unique_ptr - Exit And Save Log + Exit and Save Log Exit RPCS3, move the log file to a custom location @@ -818,12 +827,12 @@ true - Show Game Compatibility in Grid Mode + Show Game Compatibility - Game List Refresh + Refresh Game List @@ -841,7 +850,7 @@ - View The Welcome Dialog + View Welcome Dialog @@ -897,7 +906,7 @@ Large - + true @@ -905,15 +914,15 @@ true - List View + List Mode - + true - Grid View + Grid Mode diff --git a/rpcs3/rpcs3qt/ps_move_tracker_dialog.ui b/rpcs3/rpcs3qt/ps_move_tracker_dialog.ui index 2272c989dc..3afa78b531 100644 --- a/rpcs3/rpcs3qt/ps_move_tracker_dialog.ui +++ b/rpcs3/rpcs3qt/ps_move_tracker_dialog.ui @@ -17,7 +17,7 @@ - Dialog + PS Move Tracker Settings From f535c82a3f577c243344de5f215237c21356c15e Mon Sep 17 00:00:00 2001 From: Antonino Di Guardo <64427768+digant73@users.noreply.github.com> Date: Wed, 31 Dec 2025 16:44:11 +0100 Subject: [PATCH 46/59] Improve Utilities and Log menus (#17944) --- rpcs3/rpcs3qt/log_frame.cpp | 9 +++++---- rpcs3/rpcs3qt/log_viewer.cpp | 13 ++++--------- rpcs3/rpcs3qt/main_window.ui | 15 +++++++-------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/rpcs3/rpcs3qt/log_frame.cpp b/rpcs3/rpcs3qt/log_frame.cpp index a155cf215d..1d2739608d 100644 --- a/rpcs3/rpcs3qt/log_frame.cpp +++ b/rpcs3/rpcs3qt/log_frame.cpp @@ -293,7 +293,7 @@ void log_frame::CreateAndConnectActions() }); }); - m_perform_goto_on_debugger = new QAction(tr("Go-To On The Debugger"), this); + m_perform_goto_on_debugger = new QAction(tr("Go-To on Debugger"), this); connect(m_perform_goto_on_debugger, &QAction::triggered, [this]() { QPlainTextEdit* pte = (m_tabWidget->currentIndex() == 1 ? m_tty : m_log); @@ -315,7 +315,7 @@ void log_frame::CreateAndConnectActions() memory_viewer_panel::ShowAtPC(static_cast(pc)); }); - m_perform_goto_thread_on_debugger = new QAction(tr("Show Thread On The Debugger"), this); + m_perform_goto_thread_on_debugger = new QAction(tr("Show Thread on Debugger"), this); connect(m_perform_goto_thread_on_debugger, &QAction::triggered, [this]() { QPlainTextEdit* pte = (m_tabWidget->currentIndex() == 1 ? m_tty : m_log); @@ -433,12 +433,13 @@ void log_frame::CreateAndConnectActions() m_perform_goto_thread_on_debugger->setToolTip(tr("Show the thread that corresponds to the thread ID from the log text on the debugger.")); m_perform_show_in_mem_viewer->setToolTip(tr("Jump to the selected hexadecimal address from the log text on the memory viewer.")); - menu->addSeparator(); - menu->addActions(m_log_level_acts->actions()); menu->addSeparator(); menu->addAction(m_stack_act_log); menu->addAction(m_stack_act_err); menu->addAction(m_show_prefix_act); + menu->addSeparator(); + menu->addActions(m_log_level_acts->actions()); + menu->exec(m_log->viewport()->mapToGlobal(pos)); }); diff --git a/rpcs3/rpcs3qt/log_viewer.cpp b/rpcs3/rpcs3qt/log_viewer.cpp index ef45025b58..99961809c9 100644 --- a/rpcs3/rpcs3qt/log_viewer.cpp +++ b/rpcs3/rpcs3qt/log_viewer.cpp @@ -114,24 +114,19 @@ void log_viewer::show_context_menu(const QPoint& pos) init_action(trace_act, logs::level::trace); menu.addAction(copy); + menu.addAction(clear); + menu.addSeparator(); menu.addAction(open); - menu.addSeparator(); + menu.addAction(config); + menu.addAction(filter); menu.addAction(save); menu.addSeparator(); - menu.addAction(config); - menu.addSeparator(); - menu.addAction(filter); - menu.addSeparator(); menu.addAction(timestamps); - menu.addSeparator(); menu.addAction(threads); - menu.addSeparator(); menu.addAction(last_actions_only); menu.addSeparator(); menu.addActions(log_level_acts->actions()); - menu.addSeparator(); - menu.addAction(clear); connect(copy, &QAction::triggered, this, [this]() { diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index bcd5f50791..39a3c014bb 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -335,24 +335,23 @@ - - - - - - + + - - + + + + + From 2f94891c236bdc7d24854654e3ff3aa8f02c06ae Mon Sep 17 00:00:00 2001 From: Niram7777 Date: Fri, 19 Dec 2025 23:36:16 +0100 Subject: [PATCH 47/59] Build Freebsd update LLVM dev (22) Fix the CI by using the correct compiler version https://wiki.freebsd.org/HardcodedCCVersions --- .github/workflows/rpcs3.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rpcs3.yml b/.github/workflows/rpcs3.yml index 1edd6aa9b8..bd0e206308 100644 --- a/.github/workflows/rpcs3.yml +++ b/.github/workflows/rpcs3.yml @@ -413,7 +413,11 @@ jobs: env: CCACHE_DIR: ${{ github.workspace }}/ccache QT_VER_MAIN: '6' - LLVM_COMPILER_VER: '19' + LLVM_COMPILER_VER: '-devel' + CC: 'clang-devel' + CXX: 'clang++-devel' + LLVM_CONFIG: 'llvm-config-devel' + steps: - name: Checkout repository uses: actions/checkout@main @@ -432,7 +436,7 @@ jobs: id: root uses: vmactions/freebsd-vm@v1 with: - envs: 'QT_VER_MAIN LLVM_COMPILER_VER CCACHE_DIR' + envs: 'QT_VER_MAIN LLVM_COMPILER_VER CCACHE_DIR CC CXX LLVM_CONFIG' usesh: true run: .ci/install-freebsd.sh && .ci/build-freebsd.sh From 51fd4b2f7683d4204ca11580100b50ad10a73417 Mon Sep 17 00:00:00 2001 From: Florin9doi Date: Fri, 26 Dec 2025 22:46:21 +0200 Subject: [PATCH 48/59] USB: Added support for G27 compatibility modes - Driving Force EX - Driving Force Pro - Driving Force GT - G25 --- rpcs3/Emu/Cell/lv2/sys_usbd.cpp | 21 + rpcs3/Emu/Cell/lv2/sys_usbd.h | 1 + rpcs3/Emu/Io/LogitechG27.cpp | 773 +++++++++++++----- rpcs3/Emu/Io/LogitechG27.h | 23 +- rpcs3/Emu/Io/LogitechG27Config.h | 6 +- rpcs3/Emu/Io/TopShotElite.cpp | 4 +- rpcs3/Emu/Io/TopShotFearmaster.cpp | 4 +- .../emulated_logitech_g27_settings_dialog.cpp | 41 +- .../emulated_logitech_g27_settings_dialog.h | 2 + 9 files changed, 674 insertions(+), 201 deletions(-) diff --git a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp index cf6339081e..463ec7a68d 100644 --- a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp @@ -1054,6 +1054,27 @@ void connect_usb_controller(u8 index, input::product_type type) } } +void reconnect_usb(u32 assigned_number) +{ + auto usbh = g_fxo->try_get>(); + if (!usbh) + { + return; + } + + std::lock_guard lock(usbh->mutex); + for (auto& [nr, pair] : usbh->handled_devices) + { + auto& [internal_dev, dev] = pair; + if (nr == assigned_number) + { + usbh->disconnect_usb_device(dev, false); + usbh->connect_usb_device(dev, false); + break; + } + } +} + void handle_hotplug_event(bool connected) { if (auto usbh = g_fxo->try_get>()) diff --git a/rpcs3/Emu/Cell/lv2/sys_usbd.h b/rpcs3/Emu/Cell/lv2/sys_usbd.h index a2fd911e35..76f5f0b061 100644 --- a/rpcs3/Emu/Cell/lv2/sys_usbd.h +++ b/rpcs3/Emu/Cell/lv2/sys_usbd.h @@ -89,4 +89,5 @@ 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 reconnect_usb(u32 assigned_number); void handle_hotplug_event(bool connected); diff --git a/rpcs3/Emu/Io/LogitechG27.cpp b/rpcs3/Emu/Io/LogitechG27.cpp index 5503c606fc..c8aa453592 100644 --- a/rpcs3/Emu/Io/LogitechG27.cpp +++ b/rpcs3/Emu/Io/LogitechG27.cpp @@ -17,7 +17,254 @@ #include "Input/pad_thread.h" #include "Input/sdl_instance.h" -LOG_CHANNEL(logitech_g27_log, "LOGIG27"); +LOG_CHANNEL(logitech_g27_log, "logitech_g27"); + +#pragma pack(push, 1) +struct DFEX_data +{ + u8 square : 1; + u8 cross : 1; + u8 circle : 1; + u8 triangle : 1; + u8 l1 : 1; // Left_paddle + u8 r1 : 1; // Right_paddle + u8 l2 : 1; + u8 r2 : 1; + + u8 select : 1; // Share + u8 start : 1; // Options + u8 l3 : 1; + u8 r3 : 1; + u8 ps : 1; + u8 : 3; + + u8 dpad; // 00=N, 01=NE, 02=E, 03=SW, 04=S, 05=SW, 06=W, 07=NW, 08=IDLE + u8 steering; // 00=Left, ff=Right + u8 brake_throttle; // 00=ThrottlePressed, 7f=BothReleased/BothPressed, ff=BrakePressed + u8 const1; + u8 const2; + u32 : 32; + u32 : 32; + u16 : 16; + u8 brake; // 00=Released, ff=Pressed + u8 throttle; // 00=Released, ff=Pressed + u32 : 32; + u32 : 32; +}; + +struct DFP_data +{ + u16 steering : 14; // 0000=Left, 1fff=Mid, 3fff=Right + u16 cross: 1; + u16 square : 1; + + u8 circle : 1; + u8 triangle : 1; + u8 r1 : 1; // Right_paddle + u8 l1 : 1; // Left_paddle + u8 r2 : 1; + u8 l2 : 1; + u8 select : 1; // Share + u8 start : 1; // Options + + u8 r3 : 1; + u8 l3 : 1; + u8 r3_2 : 1; + u8 l3_2 : 1; + u8 dpad : 4; // 0=N, 1=NE, 2=E, 3=SW, 4=S, 5=SW, 6=W, 7=NW, 8=IDLE + + u8 brake_throttle; // 00=ThrottlePressed, 7f=BothReleased/BothPressed, ff=BrakePressed + u8 throttle; // 00=Pressed, ff=Released + u8 brake; // 00=Pressed, ff=Released + + u8 pedals_attached : 1; + u8 powered : 1; + u8 : 1; + u8 self_check_done : 1; + u8 set1 : 1; // always set + u8 : 3; +}; + +struct DFGT_data +{ + u8 dpad : 4; // 0=N, 1=NE, 2=E, 3=SW, 4=S, 5=SW, 6=W, 7=NW, 8=IDLE + u8 cross: 1; + u8 square : 1; + u8 circle : 1; + u8 triangle : 1; + + u8 r1 : 1; // Right_paddle + u8 l1 : 1; // Left_paddle + u8 r2 : 1; + u8 l2 : 1; + u8 select : 1; // Share + u8 start : 1; // Options + u8 r3 : 1; + u8 l3 : 1; + + u8 : 2; + u8 dial_center : 1; + u8 plus : 1; + u8 dial_cw : 1; + u8 dial_ccw : 1; + u8 minus : 1; + u8 : 1; + + u8 ps : 1; + u8 pedals_attached : 1; + u8 powered : 1; + u8 self_check_done : 1; + u8 set1 : 1; + u8 : 1; + u8 set2 : 1; + u8 : 1; + + u16 steering : 14; // 0000=Left, 1fff=Mid, 3fff=Right + u16 : 2; + u8 throttle; // 00=Pressed, ff=Released + u8 brake; // 00=Pressed, ff=Released +}; + +struct G25_data +{ + u8 dpad : 4; // 0=N, 1=NE, 2=E, 3=SW, 4=S, 5=SW, 6=W, 7=NW, 8=IDLE + u8 cross: 1; + u8 square : 1; + u8 circle : 1; + u8 triangle : 1; + + u8 r1 : 1; // Right_paddle + u8 l1 : 1; // Left_paddle + u8 r2 : 1; // + dial_center + u8 l2 : 1; + u8 select : 1; // Share + u8 start : 1; // Options + u8 r3 : 1; // + dial_cw + plus + u8 l3 : 1; // + dial_ccw + minus + + u8 gear1 : 1; + u8 gear2 : 1; + u8 gear3 : 1; + u8 gear4 : 1; + u8 gear5 : 1; + u8 gear6 : 1; + u8 gearR : 1; + u8 : 1; + + u16 pedals_detached : 1; + u16 powered : 1; + u16 steering : 14; // 0000=Left, 1fff=Mid, 3fff=Right + + u8 throttle; // 00=Pressed, ff=Released + u8 brake; // 00=Pressed, ff=Released + u8 clutch; // 00=Pressed, ff=Released + + u8 shifter_x; // 30=left(1,2), 7a=middle(3,4), b2=right(5,6) + u8 shifter_y; // 32=bottom(2,4,6), b7=top(1,3,5) + + u8 shifter_attached : 1; + u8 set1 : 1; + u8 : 1; + u8 shifter_pressed : 1; + u8 : 4; +}; + +struct G27_data +{ + u8 dpad : 4; // 0=N, 1=NE, 2=E, 3=SW, 4=S, 5=SW, 6=W, 7=NW, 8=IDLE + u8 cross: 1; + u8 square : 1; + u8 circle : 1; + u8 triangle : 1; + + u8 r1 : 1; // Right_paddle + u8 l1 : 1; // Left_paddle + u8 r2 : 1; + u8 l2 : 1; + u8 select : 1; // Share + u8 start : 1; // Options + u8 r3 : 1; // + dial_center + u8 l3 : 1; + + u8 gear1 : 1; + u8 gear2 : 1; + u8 gear3 : 1; + u8 gear4 : 1; + u8 gear5 : 1; + u8 gear6 : 1; + u8 dial_cw : 1; + u8 dial_ccw : 1; + + u16 plus : 1; + u16 minus : 1; + u16 steering : 14; // 0000=Left, 1fff=Mid, 3fff=Right + + u8 throttle; // 00=Pressed, ff=Released + u8 brake; // 00=Pressed, ff=Released + u8 clutch; // 00=Pressed, ff=Released + + u8 shifter_x; // 30=left(1,2), 7a=middle(3,4), b2=right(5,6) + u8 shifter_y; // 32=bottom(2,4,6), b7=top(1,3,5) + + u8 gearR : 1; + u8 pedals_detached : 1; + u8 powered : 1; + u8 shifter_attached : 1; + u8 set1 : 1; + u8 : 1; + u8 shifter_pressed : 1; + u8 range : 1; +}; +#pragma pack(pop) + +static const std::map>> s_logitech_personality = { +{ + logitech_personality::driving_force_ex, + { + UsbDeviceDescriptor{0x0200, 0x00, 0x00, 0x00, 0x10, 0x046D, 0xC294, 0x1350, 0x01, 0x02, 0x00, 0x01}, + {0x09, 0x02, 0x29, 0x00, 0x01, 0x01, 0x04, 0x80, 0x31, 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, + 0x00, 0x00, 0x09, 0x21, 0x00, 0x01, 0x21, 0x01, 0x22, 0x9D, 0x00, 0x07, 0x05, 0x81, 0x03, 0x40, + 0x00, 0x0A, 0x07, 0x05, 0x01, 0x03, 0x10, 0x00, 0x0A} + } +}, +{ + logitech_personality::driving_force_pro, + { + UsbDeviceDescriptor{0x0200, 0x00, 0x00, 0x00, 0x10, 0x046D, 0xC298, 0x1350, 0x01, 0x02, 0x00, 0x01}, + {0x09, 0x02, 0x29, 0x00, 0x01, 0x01, 0x04, 0x80, 0x31, 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, + 0x00, 0x00, 0x09, 0x21, 0x00, 0x01, 0x21, 0x01, 0x22, 0x61, 0x00, 0x07, 0x05, 0x81, 0x03, 0x08, + 0x00, 0x0A, 0x07, 0x05, 0x01, 0x03, 0x08, 0x00, 0x0A} + } +}, +{ + logitech_personality::g25, + { + UsbDeviceDescriptor{0x0200, 0x00, 0x00, 0x00, 0x10, 0x046D, 0xC299, 0x1350, 0x01, 0x02, 0x00, 0x01}, + {0x09, 0x02, 0x29, 0x00, 0x01, 0x01, 0x04, 0x80, 0x31, 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, + 0x00, 0x00, 0x09, 0x21, 0x11, 0x01, 0x21, 0x01, 0x22, 0x6F, 0x00, 0x07, 0x05, 0x81, 0x03, 0x10, + 0x00, 0x02, 0x07, 0x05, 0x01, 0x03, 0x10, 0x00, 0x02} + } +}, +{ + logitech_personality::driving_force_gt, + { + UsbDeviceDescriptor{0x0200, 0x00, 0x00, 0x00, 0x10, 0x046D, 0xC29A, 0x1350, 0x00, 0x02, 0x00, 0x01}, + {0x09, 0x02, 0x29, 0x00, 0x01, 0x01, 0x00, 0x80, 0x31, 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, + 0x00, 0xFE, 0x09, 0x21, 0x11, 0x01, 0x21, 0x01, 0x22, 0x73, 0x00, 0x07, 0x05, 0x81, 0x03, 0x10, + 0x00, 0x02, 0x07, 0x05, 0x01, 0x03, 0x10, 0x00, 0x02} + } +}, +{ + logitech_personality::g27, + { + UsbDeviceDescriptor{0x0200, 0x00, 0x00, 0x00, 0x10, 0x046D, 0xC29B, 0x1350, 0x01, 0x02, 0x00, 0x01}, + {0x09, 0x02, 0x29, 0x00, 0x01, 0x01, 0x04, 0x80, 0x31, 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, + 0x00, 0x00, 0x09, 0x21, 0x11, 0x01, 0x21, 0x01, 0x22, 0x85, 0x00, 0x07, 0x05, 0x81, 0x03, 0x10, + 0x00, 0x02, 0x07, 0x05, 0x01, 0x03, 0x10, 0x00, 0x02} + } +} +}; // ref: https://github.com/libsdl-org/SDL/issues/7941, need to use SDL_HAPTIC_STEERING_AXIS for some windows drivers static const SDL_HapticDirection STEERING_DIRECTION = @@ -26,20 +273,31 @@ static const SDL_HapticDirection STEERING_DIRECTION = .dir = {0, 0, 0} }; -usb_device_logitech_g27::usb_device_logitech_g27(u32 controller_index, const std::array& location) - : usb_device_emulated(location), m_controller_index(controller_index) +void usb_device_logitech_g27::set_personality(logitech_personality personality, bool reconnect) { - device = UsbDescriptorNode(USB_DESCRIPTOR_DEVICE, UsbDeviceDescriptor{0x0200, 0, 0, 0, 16, 0x046d, 0xc29b, 0x1350, 1, 2, 0, 1}); + m_personality = personality; + device = UsbDescriptorNode(USB_DESCRIPTOR_DEVICE, ::at32(s_logitech_personality, personality).first); // parse the raw response like with passthrough device - static constexpr u8 raw_config[] = {0x9, 0x2, 0x29, 0x0, 0x1, 0x1, 0x4, 0x80, 0x31, 0x9, 0x4, 0x0, 0x0, 0x2, 0x3, 0x0, 0x0, 0x0, 0x9, 0x21, 0x11, 0x1, 0x21, 0x1, 0x22, 0x85, 0x0, 0x7, 0x5, 0x81, 0x3, 0x10, 0x0, 0x2, 0x7, 0x5, 0x1, 0x3, 0x10, 0x0, 0x2}; + const u8* raw_config = ::at32(s_logitech_personality, personality).second.data(); auto& conf = device.add_node(UsbDescriptorNode(raw_config[0], raw_config[1], &raw_config[2])); - for (unsigned int index = raw_config[0]; index < sizeof(raw_config);) + for (unsigned int index = raw_config[0]; index < raw_config[2];) { conf.add_node(UsbDescriptorNode(raw_config[index], raw_config[index + 1], &raw_config[index + 2])); index += raw_config[index]; } + if (reconnect) + { + reconnect_usb(assigned_number); + } +} + +usb_device_logitech_g27::usb_device_logitech_g27(u32 controller_index, const std::array& location) + : usb_device_emulated(location), m_controller_index(controller_index) +{ + set_personality(logitech_personality::driving_force_ex); + m_default_spring_effect.type = SDL_HAPTIC_SPRING; m_default_spring_effect.condition.direction = STEERING_DIRECTION; m_default_spring_effect.condition.length = SDL_HAPTIC_INFINITY; @@ -63,7 +321,14 @@ usb_device_logitech_g27::usb_device_logitech_g27(u32 controller_index, const std while (thread_ctrl::state() != thread_state::aborting) { sdl_refresh(); - thread_ctrl::wait_for(5'000'000); + thread_ctrl::wait_for(1'000'000); + + std::unique_lock lock(g_cfg_logitech_g27.m_mutex); + if (logitech_personality::invalid != m_next_personality && m_personality != m_next_personality) + { + set_personality(m_next_personality, true); + m_next_personality = logitech_personality::invalid; + } } }); } @@ -115,7 +380,7 @@ u16 usb_device_logitech_g27::get_num_emu_devices() void usb_device_logitech_g27::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) { - logitech_g27_log.todo("control transfer bmRequestType %02x, bRequest %02x, wValue %04x, wIndex %04x, wLength %04x, %s", bmRequestType, bRequest, wValue, wIndex, wLength, fmt::buf_to_hexstring(buf, buf_size)); + logitech_g27_log.notice("control transfer bmRequestType %02x, bRequest %02x, wValue %04x, wIndex %04x, wLength %04x, %s", bmRequestType, bRequest, wValue, wIndex, wLength, fmt::buf_to_hexstring(buf, buf_size)); usb_device_emulated::control_transfer(bmRequestType, bRequest, wValue, wIndex, wLength, buf_size, buf, transfer); } @@ -194,9 +459,11 @@ static inline logitech_g27_sdl_mapping get_runtime_mapping() convert_mapping(cfg.dial_clockwise, mapping.dial_clockwise); convert_mapping(cfg.dial_anticlockwise, mapping.dial_anticlockwise); + convert_mapping(cfg.dial_center, mapping.dial_center); convert_mapping(cfg.select, mapping.select); - convert_mapping(cfg.pause, mapping.pause); + convert_mapping(cfg.start, mapping.start); + convert_mapping(cfg.ps, mapping.ps); convert_mapping(cfg.shifter_1, mapping.shifter_1); convert_mapping(cfg.shifter_2, mapping.shifter_2); @@ -205,7 +472,6 @@ static inline logitech_g27_sdl_mapping get_runtime_mapping() convert_mapping(cfg.shifter_5, mapping.shifter_5); convert_mapping(cfg.shifter_6, mapping.shifter_6); convert_mapping(cfg.shifter_r, mapping.shifter_r); - convert_mapping(cfg.shifter_press, mapping.shifter_press); return mapping; } @@ -467,6 +733,50 @@ static u8 hat_components_to_logitech_g27_hat(bool up, bool down, bool left, bool return sdl_hat_to_logitech_g27_hat(sdl_hat); } +static std::pair shifter_to_coord_xy(bool shifter_1, bool shifter_2, bool shifter_3, bool shifter_4, + bool shifter_5, bool shifter_6, bool shifter_r) +{ + // rough analog values recorded in https://github.com/RPCS3/rpcs3/pull/17199#issuecomment-2883934412 + constexpr u8 coord_center = 0x80; + constexpr u8 coord_top = 0xb7; + constexpr u8 coord_bottom = 0x32; + constexpr u8 coord_left = 0x30; + constexpr u8 coord_right = 0xb3; + constexpr u8 coord_right_reverse = 0xaa; + if (shifter_1) + { + return {coord_left, coord_top}; + } + else if (shifter_2) + { + return {coord_left, coord_bottom}; + } + else if (shifter_3) + { + return {coord_center, coord_top}; + } + else if (shifter_4) + { + return {coord_center, coord_bottom}; + } + else if (shifter_5) + { + return {coord_right, coord_top}; + } + else if (shifter_6) + { + return {coord_right, coord_bottom}; + } + else if (shifter_r) + { + return {coord_right_reverse, coord_bottom}; + } + else + { + return {coord_center, coord_center}; + } +} + static bool fetch_sdl_as_button(SDL_Joystick* joystick, const sdl_mapping& mapping) { switch (mapping.type) @@ -646,6 +956,220 @@ static inline void set_bit(u8* buf, int bit_num, bool set) buf[byte_num] = buf[byte_num] & (~mask); } +void usb_device_logitech_g27::transfer_dfex(u32 buf_size, u8* buf, UsbTransfer* transfer) +{ + DFEX_data data{}; + ensure(buf_size >= sizeof(data)); + transfer->expected_count = sizeof(data); + + const std::lock_guard lock(m_sdl_handles_mutex); + data.square = sdl_to_logitech_g27_button(m_joysticks, m_mapping.square); + data.cross = sdl_to_logitech_g27_button(m_joysticks, m_mapping.cross); + data.circle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.circle); + data.triangle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.triangle); + data.l1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_down); + data.r1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_up); + data.l2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l2); + data.r2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r2); + data.select = sdl_to_logitech_g27_button(m_joysticks, m_mapping.select); + data.start = sdl_to_logitech_g27_button(m_joysticks, m_mapping.start); + data.l3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l3); + data.r3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r3); + data.dpad = hat_components_to_logitech_g27_hat( + sdl_to_logitech_g27_button(m_joysticks, m_mapping.up), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.down), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.left), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.right) + ); + data.steering = sdl_to_logitech_g27_steering(m_joysticks, m_mapping.steering) >> 6; + data.brake_throttle = 0x7f; + data.const1 = 0x7f; + data.const2 = 0x7f; + data.brake = 0xff - sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.brake); + data.throttle = 0xff - sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.throttle); + std::memcpy(buf, &data, sizeof(data)); +} + +void usb_device_logitech_g27::transfer_dfp(u32 buf_size, u8* buf, UsbTransfer* transfer) +{ + DFP_data data{}; + ensure(buf_size >= sizeof(data)); + transfer->expected_count = sizeof(data); + + const std::lock_guard lock(m_sdl_handles_mutex); + data.steering = sdl_to_logitech_g27_steering(m_joysticks, m_mapping.steering); + data.cross = sdl_to_logitech_g27_button(m_joysticks, m_mapping.cross); + data.square = sdl_to_logitech_g27_button(m_joysticks, m_mapping.square); + data.circle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.circle); + data.triangle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.triangle); + data.r1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_up); + data.l1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_down); + data.r2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r2); + data.l2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l2); + data.select = sdl_to_logitech_g27_button(m_joysticks, m_mapping.select); + data.start = sdl_to_logitech_g27_button(m_joysticks, m_mapping.start); + data.r3 = data.r3_2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r3); + data.l3 = data.l3_2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l3); + data.dpad = hat_components_to_logitech_g27_hat( + sdl_to_logitech_g27_button(m_joysticks, m_mapping.up), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.down), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.left), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.right) + ); + data.brake_throttle = 0x7f; + data.throttle = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.throttle); + data.brake = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.brake); + data.pedals_attached = 1; + data.powered = 1; + data.self_check_done = 1; + data.set1 = 1; + std::memcpy(buf, &data, sizeof(data)); +} + +void usb_device_logitech_g27::transfer_dfgt(u32 buf_size, u8* buf, UsbTransfer* transfer) +{ + DFGT_data data{}; + ensure(buf_size >= sizeof(data)); + transfer->expected_count = sizeof(data); + + const std::lock_guard lock(m_sdl_handles_mutex); + data.dpad = hat_components_to_logitech_g27_hat( + sdl_to_logitech_g27_button(m_joysticks, m_mapping.up), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.down), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.left), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.right) + ); + data.cross = sdl_to_logitech_g27_button(m_joysticks, m_mapping.cross); + data.square = sdl_to_logitech_g27_button(m_joysticks, m_mapping.square); + data.circle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.circle); + data.triangle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.triangle); + data.r1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_up); + data.l1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_down); + data.r2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r2); + data.l2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l2); + data.select = sdl_to_logitech_g27_button(m_joysticks, m_mapping.select); + data.start = sdl_to_logitech_g27_button(m_joysticks, m_mapping.start); + data.r3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r3); + data.l3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l3); + data.dial_center = sdl_to_logitech_g27_button(m_joysticks, m_mapping.dial_center); + data.plus = sdl_to_logitech_g27_button(m_joysticks, m_mapping.plus); + data.dial_cw = sdl_to_logitech_g27_button(m_joysticks, m_mapping.dial_clockwise); + data.dial_ccw = sdl_to_logitech_g27_button(m_joysticks, m_mapping.dial_anticlockwise); + data.minus = sdl_to_logitech_g27_button(m_joysticks, m_mapping.minus); + data.ps = sdl_to_logitech_g27_button(m_joysticks, m_mapping.ps); + data.pedals_attached = 1; + data.powered = 1; + data.self_check_done = 1; + data.set1 = 1; + data.set2 = 1; + data.steering = sdl_to_logitech_g27_steering(m_joysticks, m_mapping.steering); + data.throttle = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.throttle); + data.brake = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.brake); + std::memcpy(buf, &data, sizeof(data)); +} + +void usb_device_logitech_g27::transfer_g25(u32 buf_size, u8* buf, UsbTransfer* transfer) +{ + G25_data data{}; + ensure(buf_size >= sizeof(data)); + transfer->expected_count = sizeof(data); + + const std::lock_guard lock(m_sdl_handles_mutex); + data.dpad = hat_components_to_logitech_g27_hat( + sdl_to_logitech_g27_button(m_joysticks, m_mapping.up), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.down), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.left), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.right) + ); + data.cross = sdl_to_logitech_g27_button(m_joysticks, m_mapping.cross); + data.square = sdl_to_logitech_g27_button(m_joysticks, m_mapping.square); + data.circle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.circle); + data.triangle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.triangle); + data.r1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_up); + data.l1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_down); + data.r2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r2); + data.l2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l2); + data.select = sdl_to_logitech_g27_button(m_joysticks, m_mapping.select); + data.start = sdl_to_logitech_g27_button(m_joysticks, m_mapping.start); + data.r3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r3); + data.l3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l3); + data.gear1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_1); + data.gear2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_2); + data.gear3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_3); + data.gear4 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_4); + data.gear5 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_5); + data.gear6 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_6); + data.gearR = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_r); + data.pedals_detached = 0; + data.powered = 1; + data.steering = sdl_to_logitech_g27_steering(m_joysticks, m_mapping.steering); + data.throttle = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.throttle); + data.brake = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.brake); + data.clutch = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.clutch); + auto [shifter_x, shifter_y] = shifter_to_coord_xy(data.gear1, data.gear2, + data.gear3, data.gear4, data.gear5, data.gear6, data.gearR); + data.shifter_x = shifter_x; + data.shifter_y = shifter_y; + data.shifter_attached = 1; + data.set1 = 1; + data.shifter_pressed = data.gearR; + std::memcpy(buf, &data, sizeof(data)); +} + +void usb_device_logitech_g27::transfer_g27(u32 buf_size, u8* buf, UsbTransfer* transfer) +{ + G27_data data{}; + ensure(buf_size >= sizeof(data)); + transfer->expected_count = sizeof(data); + + const std::lock_guard lock(m_sdl_handles_mutex); + data.dpad = hat_components_to_logitech_g27_hat( + sdl_to_logitech_g27_button(m_joysticks, m_mapping.up), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.down), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.left), + sdl_to_logitech_g27_button(m_joysticks, m_mapping.right) + ); + data.cross = sdl_to_logitech_g27_button(m_joysticks, m_mapping.cross); + data.square = sdl_to_logitech_g27_button(m_joysticks, m_mapping.square); + data.circle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.circle); + data.triangle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.triangle); + data.r1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_up); + data.l1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_down); + data.r2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r2); + data.l2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l2); + data.select = sdl_to_logitech_g27_button(m_joysticks, m_mapping.select); + data.start = sdl_to_logitech_g27_button(m_joysticks, m_mapping.start); + data.r3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r3); + data.l3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l3); + data.gear1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_1); + data.gear2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_2); + data.gear3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_3); + data.gear4 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_4); + data.gear5 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_5); + data.gear6 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_6); + const bool shifter_r = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_r); + data.dial_cw = sdl_to_logitech_g27_button(m_joysticks, m_mapping.dial_clockwise); + data.dial_ccw = sdl_to_logitech_g27_button(m_joysticks, m_mapping.dial_anticlockwise); + data.plus = sdl_to_logitech_g27_button(m_joysticks, m_mapping.plus); + data.minus = sdl_to_logitech_g27_button(m_joysticks, m_mapping.minus); + data.steering = sdl_to_logitech_g27_steering(m_joysticks, m_mapping.steering); + data.throttle = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.throttle); + data.brake = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.brake); + data.clutch = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.clutch); + auto [shifter_x, shifter_y] = shifter_to_coord_xy(data.gear1, data.gear2, + data.gear3, data.gear4, data.gear5, data.gear6, shifter_r); + data.shifter_x = shifter_x; + data.shifter_y = shifter_y; + data.gearR = shifter_r; + data.pedals_detached = 0; + data.powered = 1; + data.shifter_attached = 1; + data.set1 = 1; + data.shifter_pressed = shifter_r; + data.range = (m_wheel_range > 360); + std::memcpy(buf, &data, sizeof(data)); +} + void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) { transfer->fake = true; @@ -655,168 +1179,31 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp if (endpoint & (1 << 7)) { - if (buf_size < 11) - { - logitech_g27_log.error("Not populating input buffer with a buffer of the size of %u", buf_size); - return; - } - - ensure(buf_size >= 11); memset(buf, 0, buf_size); - - transfer->expected_count = 11; - sdl_instance::get_instance().pump_events(); - // Fetch input states from SDL - m_sdl_handles_mutex.lock(); - const u16 steering = sdl_to_logitech_g27_steering(m_joysticks, m_mapping.steering); - const u8 throttle = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.throttle); - const u8 brake = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.brake); - const u8 clutch = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.clutch); - const bool shift_up = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_up); - const bool shift_down = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_down); - - const bool up = sdl_to_logitech_g27_button(m_joysticks, m_mapping.up); - const bool down = sdl_to_logitech_g27_button(m_joysticks, m_mapping.down); - const bool left = sdl_to_logitech_g27_button(m_joysticks, m_mapping.left); - const bool right = sdl_to_logitech_g27_button(m_joysticks, m_mapping.right); - - const bool triangle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.triangle); - const bool cross = sdl_to_logitech_g27_button(m_joysticks, m_mapping.cross); - const bool square = sdl_to_logitech_g27_button(m_joysticks, m_mapping.square); - const bool circle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.circle); - - const bool l2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l2); - const bool l3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l3); - const bool r2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r2); - const bool r3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r3); - - const bool plus = sdl_to_logitech_g27_button(m_joysticks, m_mapping.plus); - const bool minus = sdl_to_logitech_g27_button(m_joysticks, m_mapping.minus); - - const bool dial_clockwise = sdl_to_logitech_g27_button(m_joysticks, m_mapping.dial_clockwise); - const bool dial_anticlockwise = sdl_to_logitech_g27_button(m_joysticks, m_mapping.dial_anticlockwise); - - const bool select = sdl_to_logitech_g27_button(m_joysticks, m_mapping.select); - const bool pause = sdl_to_logitech_g27_button(m_joysticks, m_mapping.pause); - - const bool shifter_1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_1); - const bool shifter_2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_2); - const bool shifter_3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_3); - const bool shifter_4 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_4); - const bool shifter_5 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_5); - const bool shifter_6 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_6); - const bool shifter_r = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_r); - const bool shifter_press = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_press); - m_sdl_handles_mutex.unlock(); - - // populate buffer - buf[0] = hat_components_to_logitech_g27_hat(up, down, left, right); - set_bit(buf, 8, shift_up); - set_bit(buf, 9, shift_down); - - set_bit(buf, 7, triangle); - set_bit(buf, 4, cross); - set_bit(buf, 5, square); - set_bit(buf, 6, circle); - - set_bit(buf, 11, l2); - set_bit(buf, 15, l3); - set_bit(buf, 10, r2); - set_bit(buf, 14, r3); - - set_bit(buf, 22, dial_clockwise); - set_bit(buf, 23, dial_anticlockwise); - - set_bit(buf, 24, plus); - set_bit(buf, 25, minus); - - set_bit(buf, 12, select); - set_bit(buf, 13, pause); - - set_bit(buf, 16, shifter_1); - set_bit(buf, 17, shifter_2); - set_bit(buf, 18, shifter_3); - set_bit(buf, 19, shifter_4); - set_bit(buf, 20, shifter_5); - set_bit(buf, 21, shifter_6); - set_bit(buf, 80, shifter_r); - - // calibrated, unsure - set_bit(buf, 82, true); - // shifter connected - set_bit(buf, 83, true); - /* - * shifter pressed/down bit - * mechanical references: - * - G29 shifter mechanical explanation https://youtu.be/d7qCn3o8K98?t=1124 - * - same mechanism on the G27 https://youtu.be/rdjejtIfkVA?t=760 - * - same mechanism on the G25 https://youtu.be/eCyt_4luwF0?t=130 - * on healthy G29/G27/G25 shifters, shifter is mechnically kept pressed in reverse, the bit should be set - * the shifter_press mapping alone captures instead a shifter press without going into reverse, ie. neutral press, just in case there are games using it for input - */ - set_bit(buf, 86, shifter_press | shifter_r); - - buf[3] = (steering << 2) | buf[3]; - buf[4] = steering >> 6; - buf[5] = throttle; - buf[6] = brake; - buf[7] = clutch; - - // rough analog values recorded in https://github.com/RPCS3/rpcs3/pull/17199#issuecomment-2883934412 - // buf[8] shifter x - // buf[9] shifter y - constexpr u8 shifter_coord_center = 0x80; - constexpr u8 shifter_coord_top = 0xb7; - constexpr u8 shifter_coord_bottom = 0x32; - constexpr u8 shifter_coord_left = 0x30; - constexpr u8 shifter_coord_right = 0xb3; - constexpr u8 shifter_coord_right_reverse = 0xaa; - if (shifter_1) + switch (m_personality) { - buf[8] = shifter_coord_left; - buf[9] = shifter_coord_top; - } - else if (shifter_2) - { - buf[8] = shifter_coord_left; - buf[9] = shifter_coord_bottom; - } - else if (shifter_3) - { - buf[8] = shifter_coord_center; - buf[9] = shifter_coord_top; - } - else if (shifter_4) - { - buf[8] = shifter_coord_center; - buf[9] = shifter_coord_bottom; - } - else if (shifter_5) - { - buf[8] = shifter_coord_right; - buf[9] = shifter_coord_top; - } - else if (shifter_6) - { - buf[8] = shifter_coord_right; - buf[9] = shifter_coord_bottom; - } - else if (shifter_r) - { - buf[8] = shifter_coord_right_reverse; - buf[9] = shifter_coord_bottom; - } - else - { - buf[8] = shifter_coord_center; - buf[9] = shifter_coord_center; + case logitech_personality::driving_force_ex: + transfer_dfex(buf_size, buf, transfer); + break; + case logitech_personality::driving_force_pro: + transfer_dfp(buf_size, buf, transfer); + break; + case logitech_personality::g25: + transfer_g25(buf_size, buf, transfer); + break; + case logitech_personality::driving_force_gt: + transfer_dfgt(buf_size, buf, transfer); + break; + case logitech_personality::g27: + transfer_g27(buf_size, buf, transfer); + break; + case logitech_personality::invalid: + fmt::throw_exception("unreachable"); } - buf[10] = buf[10] | (m_wheel_range > 360 ? 0x90 : 0x10); - - // logitech_g27_log.error("%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], buf[10]); + // logitech_g27_log.error("dev=%d, ep in : %s", static_cast(m_personality), fmt::buf_to_hexstring(buf, buf_size)); return; } @@ -831,8 +1218,7 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp transfer->expected_count = buf_size; - // logitech_g27_log.error("%02x %02x %02x %02x %02x %02x %02x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]); - // printf("%02x %02x %02x %02x %02x %02x %02x\n", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]); + // logitech_g27_log.error("ep out : %s", fmt::buf_to_hexstring(buf, buf_size)); // TODO maybe force clipping from cfg @@ -843,9 +1229,44 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp switch (buf[1]) { case 0x01: + case 0x09: + case 0x10: { - // Change to DFP - logitech_g27_log.error("Drive Force Pro mode switch command ignored"); + // Change device mode + u8 cmd = buf[1]; + u8 arg = buf[2]; + if (buf[8] == 0xf8) // we have 2 commands back to back + { + cmd = buf[9]; + arg = buf[10]; + } + + m_next_personality = logitech_personality::invalid; + if (cmd == 0x09 && arg == 0x04) + m_next_personality = logitech_personality::g27; + else if (cmd == 0x09 && arg == 0x03) + m_next_personality = logitech_personality::driving_force_gt; + else if ((cmd == 0x09 && arg == 0x02) || cmd == 0x10) + m_next_personality = logitech_personality::g25; + else if ((cmd == 0x09 && arg == 0x01) || cmd == 0x01) + m_next_personality = logitech_personality::driving_force_pro; + else if (cmd == 0x09 && arg == 0x00) + m_next_personality = logitech_personality::driving_force_ex; + + if (logitech_personality limit = static_cast(g_cfg_logitech_g27.compatibility_limit.get()); + limit < m_next_personality) + { + m_next_personality = limit; + } + + logitech_g27_log.success("Change device mode : buf=[%s], cmd=0x%x, arg=0x%x, lim=%d -> pers=%d(%s)", + fmt::buf_to_hexstring(buf, buf_size), cmd, arg, g_cfg_logitech_g27.compatibility_limit.get(), + static_cast(m_next_personality), + m_next_personality == logitech_personality::g27 ? "G27" + : m_next_personality == logitech_personality::driving_force_gt ? "Driving Force GT" + : m_next_personality == logitech_personality::g25 ? "G25" + : m_next_personality == logitech_personality::driving_force_pro ? "Driving Force Pro" + : m_next_personality == logitech_personality::driving_force_ex ? "Driving Force EX" : "Invalid"); break; } case 0x02: @@ -862,24 +1283,12 @@ void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endp m_wheel_range = 900; break; } - case 0x09: - { - // Change device mode - logitech_g27_log.error("Change device mode to %d %s detaching command ignored", buf[2], buf[3] ? "with" : "without"); - break; - } case 0x0a: { // Revert indentity logitech_g27_log.error("Revert device identity after reset %s command ignored", buf[2] ? "enable" : "disable"); break; } - case 0x10: - { - // Switch to G25 with detach - logitech_g27_log.error("Switch to G25 with detach command ignored"); - break; - } case 0x11: { // Switch to G25 without detach diff --git a/rpcs3/Emu/Io/LogitechG27.h b/rpcs3/Emu/Io/LogitechG27.h index 52735a253d..4ec0d35c0b 100644 --- a/rpcs3/Emu/Io/LogitechG27.h +++ b/rpcs3/Emu/Io/LogitechG27.h @@ -16,6 +16,16 @@ #include #include +enum class logitech_personality +{ + driving_force_ex, + driving_force_pro, + g25, + driving_force_gt, + g27, + invalid, +}; + enum class logitech_g27_ffb_state { inactive, @@ -83,9 +93,11 @@ struct logitech_g27_sdl_mapping sdl_mapping dial_clockwise {}; sdl_mapping dial_anticlockwise {}; + sdl_mapping dial_center {}; sdl_mapping select {}; - sdl_mapping pause {}; + sdl_mapping start {}; + sdl_mapping ps {}; sdl_mapping shifter_1 {}; sdl_mapping shifter_2 {}; @@ -94,7 +106,6 @@ struct logitech_g27_sdl_mapping sdl_mapping shifter_5 {}; sdl_mapping shifter_6 {}; sdl_mapping shifter_r {}; - sdl_mapping shifter_press {}; }; class usb_device_logitech_g27 : public usb_device_emulated @@ -112,9 +123,17 @@ public: private: void sdl_refresh(); + void set_personality(logitech_personality personality, bool reconnect = false); + void transfer_dfex(u32 buf_size, u8* buf, UsbTransfer* transfer); + void transfer_dfp(u32 buf_size, u8* buf, UsbTransfer* transfer); + void transfer_dfgt(u32 buf_size, u8* buf, UsbTransfer* transfer); + void transfer_g25(u32 buf_size, u8* buf, UsbTransfer* transfer); + void transfer_g27(u32 buf_size, u8* buf, UsbTransfer* transfer); u32 m_controller_index = 0; + logitech_personality m_personality = logitech_personality::invalid; + logitech_personality m_next_personality = logitech_personality::invalid; logitech_g27_sdl_mapping m_mapping {}; bool m_reverse_effects = false; diff --git a/rpcs3/Emu/Io/LogitechG27Config.h b/rpcs3/Emu/Io/LogitechG27Config.h index 82a2e89dea..1a46a0fcc2 100644 --- a/rpcs3/Emu/Io/LogitechG27Config.h +++ b/rpcs3/Emu/Io/LogitechG27Config.h @@ -96,9 +96,11 @@ public: emulated_logitech_g27_mapping dial_clockwise{this, "dial_clockwise", 0, sdl_mapping_type::button, 21, hat_component::none, false}; emulated_logitech_g27_mapping dial_anticlockwise{this, "dial_anticlockwise", 0, sdl_mapping_type::button, 22, hat_component::none, false}; + emulated_logitech_g27_mapping dial_center{this, "dial_center", 0, sdl_mapping_type::button, 23, hat_component::none, false}; emulated_logitech_g27_mapping select{this, "select", 0, sdl_mapping_type::button, 8, hat_component::none, false}; - emulated_logitech_g27_mapping pause{this, "pause", 0, sdl_mapping_type::button, 9, hat_component::none, false}; + emulated_logitech_g27_mapping start{this, "pause", 0, sdl_mapping_type::button, 9, hat_component::none, false}; + emulated_logitech_g27_mapping ps{this, "ps", 0, sdl_mapping_type::button, 24, hat_component::none, false}; emulated_logitech_g27_mapping shifter_1{this, "shifter_1", 0, sdl_mapping_type::button, 3, hat_component::none, false}; emulated_logitech_g27_mapping shifter_2{this, "shifter_2", 0, sdl_mapping_type::button, 0, hat_component::none, false}; @@ -107,9 +109,9 @@ public: emulated_logitech_g27_mapping shifter_5{this, "shifter_5", 0, sdl_mapping_type::hat, 0, hat_component::up, false}; emulated_logitech_g27_mapping shifter_6{this, "shifter_6", 0, sdl_mapping_type::hat, 0, hat_component::down, false}; emulated_logitech_g27_mapping shifter_r{this, "shifter_r", 0, sdl_mapping_type::hat, 0, hat_component::left, false}; - emulated_logitech_g27_mapping shifter_press{this, "shifter_press", 0, sdl_mapping_type::hat, 0, hat_component::right, false}; cfg::_bool reverse_effects{this, "reverse_effects", false}; + cfg::uint<0, 4> compatibility_limit{this, "compatibility_limit", 4}; cfg::uint<0, 0xFFFFFFFFFFFFFFFF> ffb_device_type_id{this, "ffb_device_type_id", 0}; cfg::uint<0, 0xFFFFFFFFFFFFFFFF> led_device_type_id{this, "led_device_type_id", 0}; diff --git a/rpcs3/Emu/Io/TopShotElite.cpp b/rpcs3/Emu/Io/TopShotElite.cpp index 0cbbd5287c..ed4042fdaa 100644 --- a/rpcs3/Emu/Io/TopShotElite.cpp +++ b/rpcs3/Emu/Io/TopShotElite.cpp @@ -219,7 +219,7 @@ void usb_device_topshotelite::control_transfer(u8 bmRequestType, u8 bRequest, u1 extern bool is_input_allowed(); -static void set_sensor_pos(struct TopShotElite_data* ts, s32 led_lx, s32 led_ly, s32 led_rx, s32 led_ry, s32 detect_l, s32 detect_r) +static void set_sensor_pos(TopShotElite_data* ts, s32 led_lx, s32 led_ly, s32 led_rx, s32 led_ry, s32 detect_l, s32 detect_r) { ts->led_lx_hi = led_lx >> 2; ts->led_lx_lo = led_lx & 0x3; @@ -250,7 +250,7 @@ void usb_device_topshotelite::interrupt_transfer(u32 buf_size, u8* buf, u32 /*en transfer->expected_result = HC_CC_NOERR; transfer->expected_time = get_timestamp() + 4000; - struct TopShotElite_data ts{}; + TopShotElite_data ts{}; ts.dpad = Dpad_None; ts.stick_lx = ts.stick_ly = ts.stick_rx = ts.stick_ry = 0x7f; if (m_mode) diff --git a/rpcs3/Emu/Io/TopShotFearmaster.cpp b/rpcs3/Emu/Io/TopShotFearmaster.cpp index eed7ade977..12d56b921c 100644 --- a/rpcs3/Emu/Io/TopShotFearmaster.cpp +++ b/rpcs3/Emu/Io/TopShotFearmaster.cpp @@ -247,7 +247,7 @@ static int get_heartrate_sensor_value(u8 heartrate) return sensor_data[heartrate - 30]; } -static void set_sensor_pos(struct TopShotFearmaster_data* ts, s32 led_lx, s32 led_ly, s32 led_rx, s32 led_ry, s32 detect_l, s32 detect_r) +static void set_sensor_pos(TopShotFearmaster_data* ts, s32 led_lx, s32 led_ly, s32 led_rx, s32 led_ry, s32 detect_l, s32 detect_r) { ts->led_lx_hi = led_lx >> 2; ts->led_lx_lo = led_lx & 0x3; @@ -278,7 +278,7 @@ void usb_device_topshotfearmaster::interrupt_transfer(u32 buf_size, u8* buf, u32 transfer->expected_result = HC_CC_NOERR; transfer->expected_time = get_timestamp() + 4000; - struct TopShotFearmaster_data ts{}; + TopShotFearmaster_data ts{}; ts.dpad = Dpad_None; ts.stick_lx = ts.stick_ly = ts.stick_rx = ts.stick_ry = 0x7f; if (m_mode) diff --git a/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.cpp b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.cpp index a65f62648b..5955e91b72 100644 --- a/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.cpp +++ b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.cpp @@ -18,7 +18,7 @@ #include #include -LOG_CHANNEL(logitech_g27_cfg_log, "LOGIG27"); +LOG_CHANNEL(logitech_g27_cfg_log, "logitech_g27"); static const QString DEFAULT_STATUS = " "; @@ -57,9 +57,11 @@ enum class mapping_device DIAL_CLOCKWISE, DIAL_ANTICLOCKWISE, + DIAL_CENTER, SELECT, - PAUSE, + START, + PS, SHIFTER_1, SHIFTER_2, @@ -68,7 +70,6 @@ enum class mapping_device SHIFTER_5, SHIFTER_6, SHIFTER_R, - SHIFTER_PRESS, // Enum count COUNT @@ -97,12 +98,14 @@ QString device_name(mapping_device dev) case mapping_device::L3: return QObject::tr("L3"); case mapping_device::R2: return QObject::tr("R2"); case mapping_device::R3: return QObject::tr("R3"); - case mapping_device::PLUS: return QObject::tr("L4"); - case mapping_device::MINUS: return QObject::tr("L5"); - case mapping_device::DIAL_CLOCKWISE: return QObject::tr("R4"); - case mapping_device::DIAL_ANTICLOCKWISE: return QObject::tr("R5"); + case mapping_device::PLUS: return QObject::tr("L4\nPlus"); + case mapping_device::MINUS: return QObject::tr("L5\nMinus"); + case mapping_device::DIAL_CLOCKWISE: return QObject::tr("R4\nDial CW"); + case mapping_device::DIAL_ANTICLOCKWISE: return QObject::tr("R5\nDial CCW"); + case mapping_device::DIAL_CENTER: return QObject::tr("Dial Center"); case mapping_device::SELECT: return QObject::tr("Select"); - case mapping_device::PAUSE: return QObject::tr("Pause"); + case mapping_device::START: return QObject::tr("Start"); + case mapping_device::PS: return QObject::tr("PS"); case mapping_device::SHIFTER_1: return QObject::tr("Gear 1"); case mapping_device::SHIFTER_2: return QObject::tr("Gear 2"); case mapping_device::SHIFTER_3: return QObject::tr("Gear 3"); @@ -110,7 +113,6 @@ QString device_name(mapping_device dev) case mapping_device::SHIFTER_5: return QObject::tr("Gear 5"); case mapping_device::SHIFTER_6: return QObject::tr("Gear 6"); case mapping_device::SHIFTER_R: return QObject::tr("Gear R"); - case mapping_device::SHIFTER_PRESS: return QObject::tr("Shifter press"); case mapping_device::COUNT: return ""; } return ""; @@ -144,8 +146,10 @@ emulated_logitech_g27_mapping& device_cfg(mapping_device dev) case mapping_device::MINUS: return cfg.minus; case mapping_device::DIAL_CLOCKWISE: return cfg.dial_clockwise; case mapping_device::DIAL_ANTICLOCKWISE: return cfg.dial_anticlockwise; + case mapping_device::DIAL_CENTER: return cfg.dial_center; case mapping_device::SELECT: return cfg.select; - case mapping_device::PAUSE: return cfg.pause; + case mapping_device::START: return cfg.start; + case mapping_device::PS: return cfg.ps; case mapping_device::SHIFTER_1: return cfg.shifter_1; case mapping_device::SHIFTER_2: return cfg.shifter_2; case mapping_device::SHIFTER_3: return cfg.shifter_3; @@ -153,7 +157,6 @@ emulated_logitech_g27_mapping& device_cfg(mapping_device dev) case mapping_device::SHIFTER_5: return cfg.shifter_5; case mapping_device::SHIFTER_6: return cfg.shifter_6; case mapping_device::SHIFTER_R: return cfg.shifter_r; - case mapping_device::SHIFTER_PRESS: return cfg.shifter_press; default: fmt::throw_exception("Unexpected mapping_device %d", static_cast(dev)); } } @@ -165,6 +168,7 @@ public: : QWidget(parent) { auto layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); QLabel* label = new QLabel(this); @@ -245,6 +249,7 @@ public: QWidget* horizontal_container = new QWidget(this); QHBoxLayout* horizontal_layout = new QHBoxLayout(horizontal_container); + horizontal_layout->setContentsMargins(0, 0, 0, 0); horizontal_container->setLayout(horizontal_layout); layout->addWidget(horizontal_container); @@ -542,6 +547,7 @@ void emulated_logitech_g27_settings_dialog::save_ui_state_to_config() g_cfg_logitech_g27.enabled.set(m_enabled->isChecked()); g_cfg_logitech_g27.reverse_effects.set(m_reverse_effects->isChecked()); + g_cfg_logitech_g27.compatibility_limit.set(m_compatibility_limit->currentData().toInt()); if (m_ffb_device->get_device_choice() == mapping_device::NONE) { @@ -591,6 +597,7 @@ void emulated_logitech_g27_settings_dialog::load_ui_state_from_config() m_enabled->setChecked(g_cfg_logitech_g27.enabled.get()); m_reverse_effects->setChecked(g_cfg_logitech_g27.reverse_effects.get()); + m_compatibility_limit->setCurrentIndex(4 - g_cfg_logitech_g27.compatibility_limit.get()); } emulated_logitech_g27_settings_dialog::emulated_logitech_g27_settings_dialog(QWidget* parent) @@ -651,6 +658,18 @@ emulated_logitech_g27_settings_dialog::emulated_logitech_g27_settings_dialog(QWi m_reverse_effects = new QCheckBox(tr("Reverse force feedback effects"), this); v_layout->addWidget(m_reverse_effects); + QHBoxLayout* compat_layout = new QHBoxLayout(this); + compat_layout->setContentsMargins(0, 0, 0, 0); + QLabel* compatibility_label = new QLabel(tr("Compatibility limit:"), this); + compat_layout->addWidget(compatibility_label); + m_compatibility_limit = new QComboBox(this); + m_compatibility_limit->addItem(tr("G27 (Default)"), static_cast(logitech_personality::g27)); + m_compatibility_limit->addItem(tr("Driving Force GT"), static_cast(logitech_personality::driving_force_gt)); + m_compatibility_limit->addItem(tr("G25"), static_cast(logitech_personality::g25)); + m_compatibility_limit->addItem(tr("Driving Force Pro"), static_cast(logitech_personality::driving_force_pro)); + compat_layout->addWidget(m_compatibility_limit); + v_layout->addLayout(compat_layout); + m_state_text = new QLabel(DEFAULT_STATUS, this); v_layout->addWidget(m_state_text); diff --git a/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.h b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.h index f06f1308c1..efd5cb589d 100644 --- a/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.h +++ b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #ifndef _MSC_VER @@ -57,6 +58,7 @@ private: QCheckBox* m_enabled = nullptr; QCheckBox* m_reverse_effects = nullptr; + QComboBox* m_compatibility_limit = nullptr; std::map m_mappings; From c5511200d5a8997b5413b37cb3a4860ab3189a63 Mon Sep 17 00:00:00 2001 From: AniLeo Date: Wed, 31 Dec 2025 18:33:56 +0000 Subject: [PATCH 49/59] rpcs3_version: Bump to 0.0.39 --- rpcs3/rpcs3_version.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcs3/rpcs3_version.cpp b/rpcs3/rpcs3_version.cpp index ab18b56caf..e7666e7b81 100644 --- a/rpcs3/rpcs3_version.cpp +++ b/rpcs3/rpcs3_version.cpp @@ -28,7 +28,7 @@ namespace rpcs3 // Currently accessible by Windows and Linux build scripts, see implementations when doing MACOSX const utils::version& get_version() { - static constexpr utils::version version{ 0, 0, 38, utils::version_type::alpha, 1, RPCS3_GIT_VERSION }; + static constexpr utils::version version{ 0, 0, 39, utils::version_type::alpha, 1, RPCS3_GIT_VERSION }; return version; } From f50b9cd5c4f55e8d72c290c0fb8e85828c7d6838 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Mon, 8 Dec 2025 05:11:45 +0100 Subject: [PATCH 50/59] Update libpng to 1.6.53 --- 3rdparty/libpng/libpng | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/libpng/libpng b/3rdparty/libpng/libpng index 49363adcfa..4e3f57d50f 160000 --- a/3rdparty/libpng/libpng +++ b/3rdparty/libpng/libpng @@ -1 +1 @@ -Subproject commit 49363adcfaf098748d7a4c8c624ad8c45a8c3a86 +Subproject commit 4e3f57d50f552841550a36eabbb3fbcecacb7750 From 05ae739a221de41baad3072f555bd06312d9c3c5 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 27 Dec 2025 16:58:27 +0100 Subject: [PATCH 51/59] Update OpenAl to 1.25.0 --- 3rdparty/OpenAL/openal-soft | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/OpenAL/openal-soft b/3rdparty/OpenAL/openal-soft index 0e5e98e4ac..75c0059630 160000 --- a/3rdparty/OpenAL/openal-soft +++ b/3rdparty/OpenAL/openal-soft @@ -1 +1 @@ -Subproject commit 0e5e98e4ac8adae92e4f7653dd6eee17aa9c8791 +Subproject commit 75c00596307bf05ba7bbc8c7022836bf52f17477 From cfbc4165215528a063ac3f3128d924caacd93027 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Thu, 1 Jan 2026 05:48:38 +0100 Subject: [PATCH 52/59] Update SDL to 3.4.0 --- 3rdparty/libsdl-org/SDL | 2 +- 3rdparty/libsdl-org/SDL.vcxproj | 36 ++++-- 3rdparty/libsdl-org/SDL.vcxproj.filters | 147 +++++++++++++++++++----- 3 files changed, 147 insertions(+), 38 deletions(-) diff --git a/3rdparty/libsdl-org/SDL b/3rdparty/libsdl-org/SDL index 7f3ae3d574..a962f40bbb 160000 --- a/3rdparty/libsdl-org/SDL +++ b/3rdparty/libsdl-org/SDL @@ -1 +1 @@ -Subproject commit 7f3ae3d57459e59943a4ecfefc8f6277ec6bf540 +Subproject commit a962f40bbba175e9716557a25d5d7965f134a3d3 diff --git a/3rdparty/libsdl-org/SDL.vcxproj b/3rdparty/libsdl-org/SDL.vcxproj index f0b38ca09f..fd2bcf2f03 100644 --- a/3rdparty/libsdl-org/SDL.vcxproj +++ b/3rdparty/libsdl-org/SDL.vcxproj @@ -23,6 +23,7 @@ + @@ -102,6 +103,7 @@ + @@ -130,6 +132,8 @@ + + @@ -140,7 +144,11 @@ + + + + @@ -156,6 +164,7 @@ + @@ -175,7 +184,6 @@ - @@ -184,20 +192,35 @@ + + + + + + + + + + + + + + + @@ -241,6 +264,7 @@ + @@ -256,13 +280,14 @@ + + - @@ -303,7 +328,6 @@ - @@ -333,7 +357,6 @@ - @@ -393,7 +416,6 @@ - @@ -464,6 +486,7 @@ + @@ -471,12 +494,11 @@ - + - diff --git a/3rdparty/libsdl-org/SDL.vcxproj.filters b/3rdparty/libsdl-org/SDL.vcxproj.filters index 5839899c0d..8b7f293ef3 100644 --- a/3rdparty/libsdl-org/SDL.vcxproj.filters +++ b/3rdparty/libsdl-org/SDL.vcxproj.filters @@ -214,6 +214,9 @@ {000028b2ea36d7190d13777a4dc70000} + + {695ffc61-5497-4227-b415-15e9bdd5b6bf} + @@ -699,9 +702,6 @@ video\yuv2rgb - - video\windows - video\windows @@ -831,9 +831,6 @@ render\software - - render\software - render\software @@ -911,12 +908,6 @@ - - - - - - render\vulkan @@ -950,6 +941,60 @@ + + video\yuv2rgb + + + video\yuv2rgb + + + video\yuv2rgb + + + video\yuv2rgb + + + video\yuv2rgb + + + video\yuv2rgb + + + video + + + video + + + video + + + misc + + + haptic\hidapi + + + haptic\hidapi + + + core + + + joystick\hidapi + + + joystick\hidapi + + + joystick\hidapi + + + joystick\hidapi + + + API Headers + @@ -1037,9 +1082,6 @@ core - - core\windows - core\windows @@ -1166,9 +1208,6 @@ joystick\dummy - - joystick\gdk - joystick\hidapi @@ -1328,9 +1367,6 @@ video\dummy - - video\windows - video\windows @@ -1343,9 +1379,6 @@ video\windows - - video\windows - video\windows @@ -1508,9 +1541,6 @@ render\software - - render\software - render\software @@ -1535,9 +1565,6 @@ - - - render\vulkan @@ -1579,6 +1606,66 @@ + + video\windows + + + video\yuv2rgb + + + video\yuv2rgb + + + video\yuv2rgb + + + video + + + misc + + + haptic\hidapi + + + haptic\hidapi + + + core\windows + + + core\windows + + + joystick\gdk + + + joystick\hidapi + + + joystick\hidapi + + + joystick\hidapi + + + joystick\hidapi + + + joystick\hidapi + + + joystick\hidapi + + + joystick\hidapi + + + joystick\hidapi + + + joystick\hidapi + From ae72de38816c961c9529bde4ec0592cc357fe6fe Mon Sep 17 00:00:00 2001 From: Megamouse Date: Thu, 1 Jan 2026 07:42:25 +0100 Subject: [PATCH 53/59] Refactor camera code to support different camera handlers I split this off the sdl camera PR due to rebase conflicts. It can also be seen as a partial integration test. --- rpcs3/Emu/Io/camera_config.cpp | 33 +-- rpcs3/Emu/Io/camera_config.h | 10 +- rpcs3/Input/camera_video_sink.cpp | 245 ++++++++++++++++++++++ rpcs3/Input/camera_video_sink.h | 43 ++++ rpcs3/rpcs3.vcxproj | 2 + rpcs3/rpcs3.vcxproj.filters | 6 + rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/camera_settings_dialog.cpp | 173 ++++++++++++---- rpcs3/rpcs3qt/camera_settings_dialog.h | 10 + rpcs3/rpcs3qt/camera_settings_dialog.ui | 22 +- rpcs3/rpcs3qt/main_window.cpp | 4 +- rpcs3/rpcs3qt/movie_item_base.cpp | 4 +- rpcs3/rpcs3qt/qt_camera_handler.cpp | 4 +- rpcs3/rpcs3qt/qt_camera_video_sink.cpp | 253 +---------------------- rpcs3/rpcs3qt/qt_camera_video_sink.h | 41 +--- 15 files changed, 496 insertions(+), 355 deletions(-) create mode 100644 rpcs3/Input/camera_video_sink.cpp create mode 100644 rpcs3/Input/camera_video_sink.h diff --git a/rpcs3/Emu/Io/camera_config.cpp b/rpcs3/Emu/Io/camera_config.cpp index f498de72b1..4016447321 100644 --- a/rpcs3/Emu/Io/camera_config.cpp +++ b/rpcs3/Emu/Io/camera_config.cpp @@ -36,32 +36,38 @@ void cfg_camera::save() const } } -cfg_camera::camera_setting cfg_camera::get_camera_setting(std::string_view camera, bool& success) +cfg_camera::camera_setting cfg_camera::get_camera_setting(std::string_view handler, std::string_view camera, bool& success) { - camera_setting setting; - const std::string value = cameras.get_value(camera); + camera_setting setting {}; + const std::string value = cameras.get_value(fmt::format("%s-%s", handler, camera)); success = !value.empty(); if (success) { - setting.from_string(cameras.get_value(camera)); + setting.from_string(value); } return setting; } -void cfg_camera::set_camera_setting(const std::string& camera, const camera_setting& setting) +void cfg_camera::set_camera_setting(std::string_view handler, std::string_view camera, const camera_setting& setting) { + if (handler.empty()) + { + camera_log.error("String '%s' cannot be used as handler key.", handler); + return; + } + if (camera.empty()) { camera_log.error("String '%s' cannot be used as camera key.", camera); return; } - cameras.set_value(camera, setting.to_string()); + cameras.set_value(fmt::format("%s-%s", handler, camera), setting.to_string()); } std::string cfg_camera::camera_setting::to_string() const { - return fmt::format("%d,%d,%f,%f,%d", width, height, min_fps, max_fps, format); + return fmt::format("%d,%d,%f,%f,%d,%d", width, height, min_fps, max_fps, format, colorspace); } void cfg_camera::camera_setting::from_string(std::string_view text) @@ -102,16 +108,19 @@ void cfg_camera::camera_setting::from_string(std::string_view text) return true; }; - if (!to_integer(::at32(list, 0), width) || - !to_integer(::at32(list, 1), height) || - !to_double(::at32(list, 2), min_fps) || - !to_double(::at32(list, 3), max_fps) || - !to_integer(::at32(list, 4), format)) + usz pos = 0; + if (!to_integer(::at32(list, pos++), width) || + !to_integer(::at32(list, pos++), height) || + !to_double(::at32(list, pos++), min_fps) || + !to_double(::at32(list, pos++), max_fps) || + !to_integer(::at32(list, pos++), format) || + !to_integer(::at32(list, pos++), colorspace)) { width = 0; height = 0; min_fps = 0; max_fps = 0; format = 0; + colorspace = 0; } } diff --git a/rpcs3/Emu/Io/camera_config.h b/rpcs3/Emu/Io/camera_config.h index 1c576e32c1..862d87dd63 100644 --- a/rpcs3/Emu/Io/camera_config.h +++ b/rpcs3/Emu/Io/camera_config.h @@ -15,18 +15,20 @@ struct cfg_camera final : cfg::node double min_fps = 0; double max_fps = 0; int format = 0; + int colorspace = 0; - static constexpr u32 member_count = 5; + static constexpr u32 member_count = 6; std::string to_string() const; void from_string(std::string_view text); }; - camera_setting get_camera_setting(std::string_view camera, bool& success); - void set_camera_setting(const std::string& camera, const camera_setting& setting); + + camera_setting get_camera_setting(std::string_view handler, std::string_view camera, bool& success); + void set_camera_setting(std::string_view handler, std::string_view camera, const camera_setting& setting); const std::string path; - cfg::map_entry cameras{ this, "Cameras" }; // : ,,,, + cfg::map_entry cameras{ this, "Cameras" }; // : ,,,,, }; extern cfg_camera g_cfg_camera; diff --git a/rpcs3/Input/camera_video_sink.cpp b/rpcs3/Input/camera_video_sink.cpp new file mode 100644 index 0000000000..7eb04472e1 --- /dev/null +++ b/rpcs3/Input/camera_video_sink.cpp @@ -0,0 +1,245 @@ +#include "stdafx.h" +#include "camera_video_sink.h" + +#include "Emu/Cell/Modules/cellCamera.h" +#include "Emu/system_config.h" + +LOG_CHANNEL(camera_log, "Camera"); + +camera_video_sink::camera_video_sink(bool front_facing) + : m_front_facing(front_facing) +{ +} + +camera_video_sink::~camera_video_sink() +{ +} + +bool camera_video_sink::present(u32 src_width, u32 src_height, u32 src_pitch, u32 src_bytes_per_pixel, std::function src_line_ptr) +{ + ensure(!!src_line_ptr); + + const u64 new_size = m_bytesize; + image_buffer& image_buffer = m_image_buffer[m_write_index]; + + // Reset buffer if necessary + if (image_buffer.data.size() != new_size) + { + image_buffer.data.clear(); + } + + // Create buffer if necessary + if (image_buffer.data.empty() && new_size > 0) + { + image_buffer.data.resize(new_size); + image_buffer.width = m_width; + image_buffer.height = m_height; + } + + if (!image_buffer.data.empty() && src_width && src_height) + { + // Convert image to proper layout + // TODO: check if pixel format and bytes per pixel match and convert if necessary + // TODO: implement or improve more conversions + + const u32 width = std::min(image_buffer.width, src_width); + const u32 height = std::min(image_buffer.height, src_height); + + switch (m_format) + { + case CELL_CAMERA_RAW8: // The game seems to expect BGGR + { + // Let's use a very simple algorithm to convert the image to raw BGGR + u8* dst = image_buffer.data.data(); + + for (u32 y = 0; y < height; y++) + { + const u8* src = src_line_ptr(y); + const u8* srcu = src_line_ptr(std::max(0, y - 1)); + const u8* srcd = src_line_ptr(std::min(height - 1, y + 1)); + const bool is_top_pixel = (y % 2) == 0; + + // We apply gaussian blur to get better demosaicing results later when debayering again + const auto blurred = [&](s32 x, s32 c) + { + const s32 i = x * 4 + c; + const s32 il = std::max(0, x - 1) * 4 + c; + const s32 ir = std::min(width - 1, x + 1) * 4 + c; + const s32 sum = + srcu[i] + + src[il] + 4 * src[i] + src[ir] + + srcd[i]; + return static_cast(std::clamp((sum + 4) / 8, 0, 255)); + }; + + // Split loops (roughly twice the performance by removing one condition) + if (is_top_pixel) + { + for (u32 x = 0; x < width; x++, dst++) + { + const bool is_left_pixel = (x % 2) == 0; + + if (is_left_pixel) + { + *dst = blurred(x, 2); // Blue + } + else + { + *dst = blurred(x, 1); // Green + } + } + } + else + { + for (u32 x = 0; x < width; x++, dst++) + { + const bool is_left_pixel = (x % 2) == 0; + + if (is_left_pixel) + { + *dst = blurred(x, 1); // Green + } + else + { + *dst = blurred(x, 0); // Red + } + } + } + } + break; + } + //case CELL_CAMERA_YUV422: + case CELL_CAMERA_Y0_U_Y1_V: + case CELL_CAMERA_V_Y1_U_Y0: + { + // Simple RGB to Y0_U_Y1_V conversion from stackoverflow. + constexpr s32 yuv_bytes_per_pixel = 2; + const s32 yuv_pitch = image_buffer.width * yuv_bytes_per_pixel; + + const s32 y0_offset = (m_format == CELL_CAMERA_Y0_U_Y1_V) ? 0 : 3; + const s32 u_offset = (m_format == CELL_CAMERA_Y0_U_Y1_V) ? 1 : 2; + const s32 y1_offset = (m_format == CELL_CAMERA_Y0_U_Y1_V) ? 2 : 1; + const s32 v_offset = (m_format == CELL_CAMERA_Y0_U_Y1_V) ? 3 : 0; + + for (u32 y = 0; y < height; y++) + { + const u8* src = src_line_ptr(y); + u8* yuv_row_ptr = &image_buffer.data[y * yuv_pitch]; + + for (u32 x = 0; x < width - 1; x += 2, src += 8) + { + const f32 r1 = src[0]; + const f32 g1 = src[1]; + const f32 b1 = src[2]; + const f32 r2 = src[4]; + const f32 g2 = src[5]; + const f32 b2 = src[6]; + + const f32 y0 = (0.257f * r1) + (0.504f * g1) + (0.098f * b1) + 16.0f; + const f32 u = -(0.148f * r1) - (0.291f * g1) + (0.439f * b1) + 128.0f; + const f32 v = (0.439f * r1) - (0.368f * g1) - (0.071f * b1) + 128.0f; + const f32 y1 = (0.257f * r2) + (0.504f * g2) + (0.098f * b2) + 16.0f; + + const s32 yuv_index = x * yuv_bytes_per_pixel; + yuv_row_ptr[yuv_index + y0_offset] = static_cast(std::clamp(y0, 0.0f, 255.0f)); + yuv_row_ptr[yuv_index + u_offset] = static_cast(std::clamp( u, 0.0f, 255.0f)); + yuv_row_ptr[yuv_index + y1_offset] = static_cast(std::clamp(y1, 0.0f, 255.0f)); + yuv_row_ptr[yuv_index + v_offset] = static_cast(std::clamp( v, 0.0f, 255.0f)); + } + } + break; + } + case CELL_CAMERA_JPG: + case CELL_CAMERA_RGBA: + case CELL_CAMERA_RAW10: + case CELL_CAMERA_YUV420: + case CELL_CAMERA_FORMAT_UNKNOWN: + default: + const u32 bytes_per_line = src_bytes_per_pixel * src_width; + if (src_pitch == bytes_per_line) + { + std::memcpy(image_buffer.data.data(), src_line_ptr(0), std::min(image_buffer.data.size(), src_height * bytes_per_line)); + } + else + { + for (u32 y = 0, pos = 0; y < src_height && pos < image_buffer.data.size(); y++, pos += bytes_per_line) + { + std::memcpy(&image_buffer.data[pos], src_line_ptr(y), std::min(image_buffer.data.size() - pos, bytes_per_line)); + } + } + break; + } + } + + camera_log.trace("Wrote image to video surface. index=%d, m_frame_number=%d, width=%d, height=%d, bytesize=%d", + m_write_index, m_frame_number.load(), m_width, m_height, m_bytesize); + + // Toggle write/read index + std::lock_guard lock(m_mutex); + image_buffer.frame_number = m_frame_number++; + m_write_index = read_index(); + + return true; +} + +void camera_video_sink::set_format(s32 format, u32 bytesize) +{ + camera_log.notice("Setting format: format=%d, bytesize=%d", format, bytesize); + + m_format = format; + m_bytesize = bytesize; +} + +void camera_video_sink::set_resolution(u32 width, u32 height) +{ + camera_log.notice("Setting resolution: width=%d, height=%d", width, height); + + m_width = width; + m_height = height; +} + +void camera_video_sink::set_mirrored(bool mirrored) +{ + camera_log.notice("Setting mirrored: mirrored=%d", mirrored); + + m_mirrored = mirrored; +} + +u64 camera_video_sink::frame_number() const +{ + return m_frame_number.load(); +} + +void camera_video_sink::get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read) +{ + // Lock read buffer + std::lock_guard lock(m_mutex); + const image_buffer& image_buffer = m_image_buffer[read_index()]; + + width = image_buffer.width; + height = image_buffer.height; + frame_number = image_buffer.frame_number; + + // Copy to out buffer + if (buf && !image_buffer.data.empty()) + { + bytes_read = std::min(image_buffer.data.size(), size); + std::memcpy(buf, image_buffer.data.data(), bytes_read); + + if (image_buffer.data.size() != size) + { + camera_log.error("Buffer size mismatch: in=%d, out=%d. Cropping to incoming size. Please contact a developer.", size, image_buffer.data.size()); + } + } + else + { + bytes_read = 0; + } +} + +u32 camera_video_sink::read_index() const +{ + // The read buffer index cannot be the same as the write index + return (m_write_index + 1u) % ::narrow(m_image_buffer.size()); +} + diff --git a/rpcs3/Input/camera_video_sink.h b/rpcs3/Input/camera_video_sink.h new file mode 100644 index 0000000000..b2a3311199 --- /dev/null +++ b/rpcs3/Input/camera_video_sink.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +class camera_video_sink +{ +public: + camera_video_sink(bool front_facing); + virtual ~camera_video_sink(); + + void set_format(s32 format, u32 bytesize); + void set_resolution(u32 width, u32 height); + void set_mirrored(bool mirrored); + + u64 frame_number() const; + + bool present(u32 src_width, u32 src_height, u32 src_pitch, u32 src_bytes_per_pixel, std::function src_line_ptr); + + void get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read); + +protected: + u32 read_index() const; + + bool m_front_facing = false; + bool m_mirrored = false; // Set by cellCamera + s32 m_format = 2; // CELL_CAMERA_RAW8, set by cellCamera + u32 m_bytesize = 0; + u32 m_width = 640; + u32 m_height = 480; + + std::mutex m_mutex; + atomic_t m_frame_number{0}; + u32 m_write_index{0}; + + struct image_buffer + { + u64 frame_number = 0; + u32 width = 0; + u32 height = 0; + std::vector data; + }; + std::array m_image_buffer; +}; diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index a824f65267..b38aa78567 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -190,6 +190,7 @@ + @@ -944,6 +945,7 @@ + diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 470ab36812..fbe4fa4132 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -1245,6 +1245,9 @@ Gui\rpcn + + Io\camera + @@ -1475,6 +1478,9 @@ Gui\widgets + + Io\camera + diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 98be856a25..ba6d95806a 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -146,6 +146,7 @@ add_library(rpcs3_ui STATIC ../Input/basic_keyboard_handler.cpp ../Input/basic_mouse_handler.cpp + ../Input/camera_video_sink.cpp ../Input/ds3_pad_handler.cpp ../Input/ds4_pad_handler.cpp ../Input/dualsense_pad_handler.cpp diff --git a/rpcs3/rpcs3qt/camera_settings_dialog.cpp b/rpcs3/rpcs3qt/camera_settings_dialog.cpp index da3576183a..fca7432370 100644 --- a/rpcs3/rpcs3qt/camera_settings_dialog.cpp +++ b/rpcs3/rpcs3qt/camera_settings_dialog.cpp @@ -3,11 +3,14 @@ #include "ui_camera_settings_dialog.h" #include "permissions.h" #include "Emu/Io/camera_config.h" +#include "Emu/System.h" +#include "Emu/system_config.h" #include #include #include #include +#include LOG_CHANNEL(camera_log, "Camera"); @@ -61,15 +64,13 @@ camera_settings_dialog::camera_settings_dialog(QWidget* parent) { ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + load_config(); - for (const QCameraDevice& camera_info : QMediaDevices::videoInputs()) - { - if (camera_info.isNull()) continue; - ui->combo_camera->addItem(camera_info.description(), QVariant::fromValue(camera_info)); - camera_log.notice("Found camera: '%s'", camera_info.description()); - } + ui->combo_handlers->addItem("Qt", QVariant::fromValue(static_cast(camera_handler::qt))); + connect(ui->combo_handlers, &QComboBox::currentIndexChanged, this, &camera_settings_dialog::handle_handler_change); connect(ui->combo_camera, &QComboBox::currentIndexChanged, this, &camera_settings_dialog::handle_camera_change); connect(ui->combo_settings, &QComboBox::currentIndexChanged, this, &camera_settings_dialog::handle_settings_change); connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) @@ -85,33 +86,132 @@ camera_settings_dialog::camera_settings_dialog(QWidget* parent) } }); - if (ui->combo_camera->count() == 0) - { - ui->combo_camera->setEnabled(false); - ui->combo_settings->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); - } - else + const int handler_index = ui->combo_handlers->findData(static_cast(g_cfg.io.camera.get())); + ui->combo_handlers->setCurrentIndex(std::max(0, handler_index)); +} + +camera_settings_dialog::~camera_settings_dialog() +{ + reset_cameras(); +} + +void camera_settings_dialog::enable_combos() +{ + const bool is_enabled = ui->combo_camera->count() > 0; + + ui->combo_camera->setEnabled(is_enabled); + ui->combo_settings->setEnabled(is_enabled); + ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(is_enabled); + ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(is_enabled); + + if (is_enabled) { // TODO: show camera ID somewhere ui->combo_camera->setCurrentIndex(0); } } -camera_settings_dialog::~camera_settings_dialog() +void camera_settings_dialog::reset_cameras() { + m_media_capture_session.reset(); + m_camera.reset(); +} + +void camera_settings_dialog::handle_handler_change(int index) +{ + reset_cameras(); + + if (index < 0 || !ui->combo_handlers->itemData(index).canConvert()) + { + ui->combo_settings->clear(); + ui->combo_camera->clear(); + enable_combos(); + return; + } + + m_handler = static_cast(ui->combo_handlers->itemData(index).value()); + + ui->combo_settings->blockSignals(true); + ui->combo_camera->blockSignals(true); + + ui->combo_settings->clear(); + ui->combo_camera->clear(); + + switch (m_handler) + { + case camera_handler::qt: + { + for (const QCameraDevice& camera_info : QMediaDevices::videoInputs()) + { + if (camera_info.isNull()) continue; + ui->combo_camera->addItem(camera_info.description(), QVariant::fromValue(camera_info)); + camera_log.notice("Found camera: '%s'", camera_info.description()); + } + break; + } + default: + fmt::throw_exception("Unexpected camera handler %d", static_cast(m_handler)); + } + + ui->combo_settings->blockSignals(false); + ui->combo_camera->blockSignals(false); + + enable_combos(); } void camera_settings_dialog::handle_camera_change(int index) { - if (index < 0 || !ui->combo_camera->itemData(index).canConvert()) + if (index < 0) { ui->combo_settings->clear(); return; } - const QCameraDevice camera_info = ui->combo_camera->itemData(index).value(); + reset_cameras(); + + switch (m_handler) + { + case camera_handler::qt: + handle_qt_camera_change(ui->combo_camera->itemData(index)); + break; + default: + fmt::throw_exception("Unexpected camera handler %d", static_cast(m_handler)); + } +} + +void camera_settings_dialog::handle_settings_change(int index) +{ + if (index < 0) + { + return; + } + + if (!gui::utils::check_camera_permission(this, + [this, index](){ handle_settings_change(index); }, + [this](){ QMessageBox::warning(this, tr("Camera permissions denied!"), tr("RPCS3 has no permissions to access cameras on this device.")); })) + { + return; + } + + switch (m_handler) + { + case camera_handler::qt: + handle_qt_settings_change(ui->combo_settings->itemData(index)); + break; + default: + fmt::throw_exception("Unexpected camera handler %d", static_cast(m_handler)); + } +} + +void camera_settings_dialog::handle_qt_camera_change(const QVariant& item_data) +{ + if (!item_data.canConvert()) + { + ui->combo_settings->clear(); + return; + } + + const QCameraDevice camera_info = item_data.value(); if (camera_info.isNull()) { @@ -119,10 +219,10 @@ void camera_settings_dialog::handle_camera_change(int index) return; } - m_camera.reset(new QCamera(camera_info)); - m_media_capture_session.reset(new QMediaCaptureSession(nullptr)); + m_camera = std::make_unique(camera_info); + m_media_capture_session = std::make_unique(nullptr); m_media_capture_session->setCamera(m_camera.get()); - m_media_capture_session->setVideoSink(ui->videoWidget->videoSink()); + m_media_capture_session->setVideoOutput(ui->videoWidget); if (!m_camera->isAvailable()) { @@ -173,14 +273,14 @@ void camera_settings_dialog::handle_camera_change(int index) int index = 0; bool success = false; const std::string key = camera_info.id().toStdString(); - cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(key, success); + cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(fmt::format("%s", camera_handler::qt), key, success); if (success) { camera_log.notice("Found config entry for camera \"%s\"", key); - // Select matching drowdown entry - const double epsilon = 0.001; + // Select matching dropdown entry + constexpr double epsilon = 0.001; for (int i = 0; i < ui->combo_settings->count(); i++) { @@ -202,19 +302,10 @@ void camera_settings_dialog::handle_camera_change(int index) ui->combo_settings->setCurrentIndex(std::max(0, index)); ui->combo_settings->setEnabled(true); - - // Update config to match user interface outcome - const QCameraFormat setting = ui->combo_settings->currentData().value(); - cfg_setting.width = setting.resolution().width(); - cfg_setting.height = setting.resolution().height(); - cfg_setting.min_fps = setting.minFrameRate(); - cfg_setting.max_fps = setting.maxFrameRate(); - cfg_setting.format = static_cast(setting.pixelFormat()); - g_cfg_camera.set_camera_setting(key, cfg_setting); } } -void camera_settings_dialog::handle_settings_change(int index) +void camera_settings_dialog::handle_qt_settings_change(const QVariant& item_data) { if (!m_camera) { @@ -227,28 +318,22 @@ void camera_settings_dialog::handle_settings_change(int index) return; } - if (!gui::utils::check_camera_permission(this, - [this, index](){ handle_settings_change(index); }, - [this](){ QMessageBox::warning(this, tr("Camera permissions denied!"), tr("RPCS3 has no permissions to access cameras on this device.")); })) + if (item_data.canConvert() && ui->combo_camera->currentData().canConvert()) { - return; - } - - if (index >= 0 && ui->combo_settings->itemData(index).canConvert() && ui->combo_camera->currentData().canConvert()) - { - const QCameraFormat setting = ui->combo_settings->itemData(index).value(); + const QCameraFormat setting = item_data.value(); if (!setting.isNull()) { m_camera->setCameraFormat(setting); } - cfg_camera::camera_setting cfg_setting; + cfg_camera::camera_setting cfg_setting {}; cfg_setting.width = setting.resolution().width(); cfg_setting.height = setting.resolution().height(); cfg_setting.min_fps = setting.minFrameRate(); cfg_setting.max_fps = setting.maxFrameRate(); cfg_setting.format = static_cast(setting.pixelFormat()); - g_cfg_camera.set_camera_setting(ui->combo_camera->currentData().value().id().toStdString(), cfg_setting); + cfg_setting.colorspace = 0; + g_cfg_camera.set_camera_setting(fmt::format("%s", camera_handler::qt), ui->combo_camera->currentData().value().id().toStdString(), cfg_setting); } m_camera->start(); diff --git a/rpcs3/rpcs3qt/camera_settings_dialog.h b/rpcs3/rpcs3qt/camera_settings_dialog.h index da18f64cca..b7c0750c2c 100644 --- a/rpcs3/rpcs3qt/camera_settings_dialog.h +++ b/rpcs3/rpcs3qt/camera_settings_dialog.h @@ -1,5 +1,7 @@ #pragma once +#include "Emu/system_config_types.h" + #include #include #include @@ -18,14 +20,22 @@ public: virtual ~camera_settings_dialog(); private Q_SLOTS: + void handle_handler_change(int index); void handle_camera_change(int index); void handle_settings_change(int index); private: + void enable_combos(); + void reset_cameras(); + void load_config(); void save_config(); + void handle_qt_camera_change(const QVariant& item_data); + void handle_qt_settings_change(const QVariant& item_data); + std::unique_ptr ui; std::unique_ptr m_camera; std::unique_ptr m_media_capture_session; + camera_handler m_handler = camera_handler::qt; }; diff --git a/rpcs3/rpcs3qt/camera_settings_dialog.ui b/rpcs3/rpcs3qt/camera_settings_dialog.ui index 8afe262f22..dfdf6beed2 100644 --- a/rpcs3/rpcs3qt/camera_settings_dialog.ui +++ b/rpcs3/rpcs3qt/camera_settings_dialog.ui @@ -15,7 +15,23 @@ - + + + + + Handler + + + + + + No handlers found + + + + + + @@ -75,10 +91,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Save + QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 58b7ef7e4c..fea8dc8ca4 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -2905,8 +2905,8 @@ void main_window::CreateConnects() connect(ui->confCamerasAct, &QAction::triggered, this, [this]() { - camera_settings_dialog dlg(this); - dlg.exec(); + camera_settings_dialog* dlg = new camera_settings_dialog(this); + dlg->open(); }); const auto open_rpcn_settings = [this] diff --git a/rpcs3/rpcs3qt/movie_item_base.cpp b/rpcs3/rpcs3qt/movie_item_base.cpp index fdf820ae5f..669aec515d 100644 --- a/rpcs3/rpcs3qt/movie_item_base.cpp +++ b/rpcs3/rpcs3qt/movie_item_base.cpp @@ -16,8 +16,8 @@ movie_item_base::~movie_item_base() void movie_item_base::init_pointers() { - m_icon_loading_aborted.reset(new atomic_t(false)); - m_size_on_disk_loading_aborted.reset(new atomic_t(false)); + m_icon_loading_aborted = std::make_shared>(false); + m_size_on_disk_loading_aborted = std::make_shared>(false); } void movie_item_base::call_icon_load_func(int index) diff --git a/rpcs3/rpcs3qt/qt_camera_handler.cpp b/rpcs3/rpcs3qt/qt_camera_handler.cpp index 5b0caeb642..91f0f1b05c 100644 --- a/rpcs3/rpcs3qt/qt_camera_handler.cpp +++ b/rpcs3/rpcs3qt/qt_camera_handler.cpp @@ -341,14 +341,14 @@ void qt_camera_handler::update_camera_settings() // Load selected settings from config file bool success = false; - cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(camera_id, success); + cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(fmt::format("%s", camera_handler::qt), camera_id, success); if (success) { camera_log.notice("Found config entry for camera \"%s\" (m_camera_id='%s')", camera_id, m_camera_id); // List all available settings and choose the proper value if possible. - const double epsilon = 0.001; + constexpr double epsilon = 0.001; success = false; for (const QCameraFormat& supported_setting : m_camera->cameraDevice().videoFormats()) { diff --git a/rpcs3/rpcs3qt/qt_camera_video_sink.cpp b/rpcs3/rpcs3qt/qt_camera_video_sink.cpp index d4736f01e2..315d32d6e0 100644 --- a/rpcs3/rpcs3qt/qt_camera_video_sink.cpp +++ b/rpcs3/rpcs3qt/qt_camera_video_sink.cpp @@ -1,7 +1,6 @@ #include "stdafx.h" #include "qt_camera_video_sink.h" -#include "Emu/Cell/Modules/cellCamera.h" #include "Emu/system_config.h" #include @@ -9,7 +8,7 @@ LOG_CHANNEL(camera_log, "Camera"); qt_camera_video_sink::qt_camera_video_sink(bool front_facing, QObject *parent) - : QVideoSink(parent), m_front_facing(front_facing) + : camera_video_sink(front_facing), QVideoSink(parent) { connect(this, &QVideoSink::videoFrameChanged, this, &qt_camera_video_sink::present); } @@ -36,6 +35,8 @@ bool qt_camera_video_sink::present(const QVideoFrame& frame) // Get image. This usually also converts the image to ARGB32. QImage image = frame.toImage(); + const u32 width = image.isNull() ? 0 : static_cast(image.width()); + const u32 height = image.isNull() ? 0 : static_cast(image.height()); if (image.isNull()) { @@ -44,7 +45,7 @@ bool qt_camera_video_sink::present(const QVideoFrame& frame) else { // Scale image if necessary - if (m_width > 0 && m_height > 0 && m_width != static_cast(image.width()) && m_height != static_cast(image.height())) + if (m_width > 0 && m_height > 0 && m_width != width && m_height != height) { image = image.scaled(m_width, m_height, Qt::AspectRatioMode::IgnoreAspectRatio, Qt::SmoothTransformation); } @@ -87,254 +88,10 @@ bool qt_camera_video_sink::present(const QVideoFrame& frame) } } - const u64 new_size = m_bytesize; - image_buffer& image_buffer = m_image_buffer[m_write_index]; - - // Reset buffer if necessary - if (image_buffer.data.size() != new_size) - { - image_buffer.data.clear(); - } - - // Create buffer if necessary - if (image_buffer.data.empty() && new_size > 0) - { - image_buffer.data.resize(new_size); - image_buffer.width = m_width; - image_buffer.height = m_height; - } - - if (!image_buffer.data.empty() && !image.isNull()) - { - // Convert image to proper layout - // TODO: check if pixel format and bytes per pixel match and convert if necessary - // TODO: implement or improve more conversions - - const u32 width = std::min(image_buffer.width, image.width()); - const u32 height = std::min(image_buffer.height, image.height()); - - switch (m_format) - { - case CELL_CAMERA_RAW8: // The game seems to expect BGGR - { - // Let's use a very simple algorithm to convert the image to raw BGGR - const auto convert_to_bggr = [this, &image_buffer, &image, width, height](u32 y_begin, u32 y_end) - { - u8* dst = &image_buffer.data[image_buffer.width * y_begin]; - - for (u32 y = y_begin; y < height && y < y_end; y++) - { - const u8* src = image.constScanLine(y); - const u8* srcu = image.constScanLine(std::max(0, y - 1)); - const u8* srcd = image.constScanLine(std::min(height - 1, y + 1)); - const bool is_top_pixel = (y % 2) == 0; - - // We apply gaussian blur to get better demosaicing results later when debayering again - const auto blurred = [&](s32 x, s32 c) - { - const s32 i = x * 4 + c; - const s32 il = std::max(0, x - 1) * 4 + c; - const s32 ir = std::min(width - 1, x + 1) * 4 + c; - const s32 sum = - srcu[i] + - src[il] + 4 * src[i] + src[ir] + - srcd[i]; - return static_cast(std::clamp((sum + 4) / 8, 0, 255)); - }; - - // Split loops (roughly twice the performance by removing one condition) - if (is_top_pixel) - { - for (u32 x = 0; x < width; x++, dst++) - { - const bool is_left_pixel = (x % 2) == 0; - - if (is_left_pixel) - { - *dst = blurred(x, 2); // Blue - } - else - { - *dst = blurred(x, 1); // Green - } - } - } - else - { - for (u32 x = 0; x < width; x++, dst++) - { - const bool is_left_pixel = (x % 2) == 0; - - if (is_left_pixel) - { - *dst = blurred(x, 1); // Green - } - else - { - *dst = blurred(x, 0); // Red - } - } - } - } - }; - - // Use a multithreaded workload. The faster we get this done, the better. - constexpr u32 thread_count = 4; - const u32 lines_per_thread = std::ceil(image_buffer.height / static_cast(thread_count)); - u32 y_begin = 0; - u32 y_end = lines_per_thread; - - QFutureSynchronizer synchronizer; - for (u32 i = 0; i < thread_count; i++) - { - synchronizer.addFuture(QtConcurrent::run(convert_to_bggr, y_begin, y_end)); - y_begin = y_end; - y_end += lines_per_thread; - } - synchronizer.waitForFinished(); - break; - } - //case CELL_CAMERA_YUV422: - case CELL_CAMERA_Y0_U_Y1_V: - case CELL_CAMERA_V_Y1_U_Y0: - { - // Simple RGB to Y0_U_Y1_V conversion from stackoverflow. - const auto convert_to_yuv422 = [&image_buffer, &image, width, height, format = m_format](u32 y_begin, u32 y_end) - { - constexpr s32 yuv_bytes_per_pixel = 2; - const s32 yuv_pitch = image_buffer.width * yuv_bytes_per_pixel; - - const s32 y0_offset = (format == CELL_CAMERA_Y0_U_Y1_V) ? 0 : 3; - const s32 u_offset = (format == CELL_CAMERA_Y0_U_Y1_V) ? 1 : 2; - const s32 y1_offset = (format == CELL_CAMERA_Y0_U_Y1_V) ? 2 : 1; - const s32 v_offset = (format == CELL_CAMERA_Y0_U_Y1_V) ? 3 : 0; - - for (u32 y = y_begin; y < height && y < y_end; y++) - { - const u8* src = image.constScanLine(y); - u8* yuv_row_ptr = &image_buffer.data[y * yuv_pitch]; - - for (u32 x = 0; x < width - 1; x += 2, src += 8) - { - const f32 r1 = src[0]; - const f32 g1 = src[1]; - const f32 b1 = src[2]; - const f32 r2 = src[4]; - const f32 g2 = src[5]; - const f32 b2 = src[6]; - - const s32 y0 = (0.257f * r1) + (0.504f * g1) + (0.098f * b1) + 16.0f; - const s32 u = -(0.148f * r1) - (0.291f * g1) + (0.439f * b1) + 128.0f; - const s32 v = (0.439f * r1) - (0.368f * g1) - (0.071f * b1) + 128.0f; - const s32 y1 = (0.257f * r2) + (0.504f * g2) + (0.098f * b2) + 16.0f; - - const s32 yuv_index = x * yuv_bytes_per_pixel; - yuv_row_ptr[yuv_index + y0_offset] = static_cast(std::clamp(y0, 0, 255)); - yuv_row_ptr[yuv_index + u_offset] = static_cast(std::clamp( u, 0, 255)); - yuv_row_ptr[yuv_index + y1_offset] = static_cast(std::clamp(y1, 0, 255)); - yuv_row_ptr[yuv_index + v_offset] = static_cast(std::clamp( v, 0, 255)); - } - } - }; - - // Use a multithreaded workload. The faster we get this done, the better. - constexpr u32 thread_count = 4; - const u32 lines_per_thread = std::ceil(image_buffer.height / static_cast(thread_count)); - u32 y_begin = 0; - u32 y_end = lines_per_thread; - - QFutureSynchronizer synchronizer; - for (u32 i = 0; i < thread_count; i++) - { - synchronizer.addFuture(QtConcurrent::run(convert_to_yuv422, y_begin, y_end)); - y_begin = y_end; - y_end += lines_per_thread; - } - synchronizer.waitForFinished(); - break; - } - case CELL_CAMERA_JPG: - case CELL_CAMERA_RGBA: - case CELL_CAMERA_RAW10: - case CELL_CAMERA_YUV420: - case CELL_CAMERA_FORMAT_UNKNOWN: - default: - std::memcpy(image_buffer.data.data(), image.constBits(), std::min(image_buffer.data.size(), image.height() * image.bytesPerLine())); - break; - } - } + camera_video_sink::present(width, height, image.bytesPerLine(), 4, [&image](u32 y){ return image.constScanLine(y); }); // Unmap frame memory tmp.unmap(); - camera_log.trace("Wrote image to video surface. index=%d, m_frame_number=%d, width=%d, height=%d, bytesize=%d", - m_write_index, m_frame_number.load(), m_width, m_height, m_bytesize); - - // Toggle write/read index - std::lock_guard lock(m_mutex); - image_buffer.frame_number = m_frame_number++; - m_write_index = read_index(); - return true; } - -void qt_camera_video_sink::set_format(s32 format, u32 bytesize) -{ - camera_log.notice("Setting format: format=%d, bytesize=%d", format, bytesize); - - m_format = format; - m_bytesize = bytesize; -} - -void qt_camera_video_sink::set_resolution(u32 width, u32 height) -{ - camera_log.notice("Setting resolution: width=%d, height=%d", width, height); - - m_width = width; - m_height = height; -} - -void qt_camera_video_sink::set_mirrored(bool mirrored) -{ - camera_log.notice("Setting mirrored: mirrored=%d", mirrored); - - m_mirrored = mirrored; -} - -u64 qt_camera_video_sink::frame_number() const -{ - return m_frame_number.load(); -} - -void qt_camera_video_sink::get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read) -{ - // Lock read buffer - std::lock_guard lock(m_mutex); - const image_buffer& image_buffer = m_image_buffer[read_index()]; - - width = image_buffer.width; - height = image_buffer.height; - frame_number = image_buffer.frame_number; - - // Copy to out buffer - if (buf && !image_buffer.data.empty()) - { - bytes_read = std::min(image_buffer.data.size(), size); - std::memcpy(buf, image_buffer.data.data(), bytes_read); - - if (image_buffer.data.size() != size) - { - camera_log.error("Buffer size mismatch: in=%d, out=%d. Cropping to incoming size. Please contact a developer.", size, image_buffer.data.size()); - } - } - else - { - bytes_read = 0; - } -} - -u32 qt_camera_video_sink::read_index() const -{ - // The read buffer index cannot be the same as the write index - return (m_write_index + 1u) % ::narrow(m_image_buffer.size()); -} diff --git a/rpcs3/rpcs3qt/qt_camera_video_sink.h b/rpcs3/rpcs3qt/qt_camera_video_sink.h index e3f405b55c..8f228bb94f 100644 --- a/rpcs3/rpcs3qt/qt_camera_video_sink.h +++ b/rpcs3/rpcs3qt/qt_camera_video_sink.h @@ -1,50 +1,15 @@ #pragma once -#include "util/atomic.hpp" -#include "util/types.hpp" +#include "Input/camera_video_sink.h" + #include #include -#include -#include -#include - -class qt_camera_video_sink final : public QVideoSink +class qt_camera_video_sink final : public camera_video_sink, public QVideoSink { public: qt_camera_video_sink(bool front_facing, QObject *parent = nullptr); virtual ~qt_camera_video_sink(); bool present(const QVideoFrame& frame); - - void set_format(s32 format, u32 bytesize); - void set_resolution(u32 width, u32 height); - void set_mirrored(bool mirrored); - - u64 frame_number() const; - - void get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read); - -private: - u32 read_index() const; - - bool m_front_facing = false; - bool m_mirrored = false; // Set by cellCamera - s32 m_format = 2; // CELL_CAMERA_RAW8, set by cellCamera - u32 m_bytesize = 0; - u32 m_width = 640; - u32 m_height = 480; - - std::mutex m_mutex; - atomic_t m_frame_number{0}; - u32 m_write_index{0}; - - struct image_buffer - { - u64 frame_number = 0; - u32 width = 0; - u32 height = 0; - std::vector data; - }; - std::array m_image_buffer; }; From 8d7484df894cc9652aadda6675c9908697650862 Mon Sep 17 00:00:00 2001 From: qurious-pixel <62252937+qurious-pixel@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:57:54 -0800 Subject: [PATCH 54/59] [BSD] Use FreeBSD 14.3 vm Fixes runner stopping when running out of disk space. --- .github/workflows/rpcs3.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rpcs3.yml b/.github/workflows/rpcs3.yml index bd0e206308..a4a99fd015 100644 --- a/.github/workflows/rpcs3.yml +++ b/.github/workflows/rpcs3.yml @@ -438,6 +438,8 @@ jobs: with: envs: 'QT_VER_MAIN LLVM_COMPILER_VER CCACHE_DIR CC CXX LLVM_CONFIG' usesh: true + copyback: false + release: "14.3" run: .ci/install-freebsd.sh && .ci/build-freebsd.sh - name: Save Build Ccache From ee06dccdea91814bf7d76b3037371a6477458722 Mon Sep 17 00:00:00 2001 From: kd-11 <15904127+kd-11@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:36:31 +0300 Subject: [PATCH 55/59] vk: Disable VK_EXT_attachment_feedback_loop_layout support for adrenalin driver --- rpcs3/Emu/RSX/NV47/HW/nv308a.cpp | 80 ++++++++++++++--------------- rpcs3/Emu/RSX/VK/vkutils/device.cpp | 2 +- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/rpcs3/Emu/RSX/NV47/HW/nv308a.cpp b/rpcs3/Emu/RSX/NV47/HW/nv308a.cpp index 163ddb9a4c..ab811eb2b9 100644 --- a/rpcs3/Emu/RSX/NV47/HW/nv308a.cpp +++ b/rpcs3/Emu/RSX/NV47/HW/nv308a.cpp @@ -89,27 +89,25 @@ namespace rsx rsx::reservation_lock rsx_lock(dst_address, data_length); if (RSX(ctx)->fifo_ctrl->last_cmd() & RSX_METHOD_NON_INCREMENT_CMD_MASK) [[unlikely]] - { - // Move last 32 bits - reinterpret_cast(dst)[0] = reinterpret_cast(src)[count - 1]; - RSX(ctx)->invalidate_fragment_program(dst_dma, dst_offset, 4); - } - else { - if (dst_dma & CELL_GCM_LOCATION_MAIN) - { - // May overlap - std::memmove(dst, src, data_length); - } - else - { - // Never overlaps - std::memcpy(dst, src, data_length); - } - - RSX(ctx)->invalidate_fragment_program(dst_dma, dst_offset, count * 4); + // Move last 32 bits + reinterpret_cast(dst)[0] = reinterpret_cast(src)[count - 1]; + RSX(ctx)->invalidate_fragment_program(dst_dma, dst_offset, 4); + return; } + if (dst_dma & CELL_GCM_LOCATION_MAIN) + { + // May overlap + std::memmove(dst, src, data_length); + } + else + { + // Never overlaps + std::memcpy(dst, src, data_length); + } + + RSX(ctx)->invalidate_fragment_program(dst_dma, dst_offset, count * 4); break; } case blit_engine::transfer_destination_format::r5g6b5: @@ -129,33 +127,33 @@ namespace rsx rsx::reservation_lock rsx_lock(dst_address, data_length); auto convert = [](u32 input) -> u16 - { - // Input is considered to be ARGB8 - u32 r = (input >> 16) & 0xFF; - u32 g = (input >> 8) & 0xFF; - u32 b = input & 0xFF; + { + // Input is considered to be ARGB8 + u32 r = (input >> 16) & 0xFF; + u32 g = (input >> 8) & 0xFF; + u32 b = input & 0xFF; - r = (r * 32) / 255; - g = (g * 64) / 255; - b = (b * 32) / 255; - return static_cast((r << 11) | (g << 5) | b); - }; + r = (r * 32) / 255; + g = (g * 64) / 255; + b = (b * 32) / 255; + return static_cast((r << 11) | (g << 5) | b); + }; if (RSX(ctx)->fifo_ctrl->last_cmd() & RSX_METHOD_NON_INCREMENT_CMD_MASK) [[unlikely]] - { - // Move last 16 bits - dst[0] = convert(src[count - 1]); - RSX(ctx)->invalidate_fragment_program(dst_dma, dst_offset, 2); - break; - } - - for (u32 i = 0; i < count; i++) - { - dst[i] = convert(src[i]); - } - - RSX(ctx)->invalidate_fragment_program(dst_dma, dst_offset, count * 2); + { + // Move last 16 bits + dst[0] = convert(src[count - 1]); + RSX(ctx)->invalidate_fragment_program(dst_dma, dst_offset, 2); break; + } + + for (u32 i = 0; i < count; i++) + { + dst[i] = convert(src[i]); + } + + RSX(ctx)->invalidate_fragment_program(dst_dma, dst_offset, count * 2); + break; } default: { diff --git a/rpcs3/Emu/RSX/VK/vkutils/device.cpp b/rpcs3/Emu/RSX/VK/vkutils/device.cpp index f7b7ffb19c..14752f160c 100644 --- a/rpcs3/Emu/RSX/VK/vkutils/device.cpp +++ b/rpcs3/Emu/RSX/VK/vkutils/device.cpp @@ -102,7 +102,7 @@ namespace vk multidraw_support.max_batch_size = 65536; optional_features_support.barycentric_coords = !!shader_barycentric_info.fragmentShaderBarycentric; - optional_features_support.framebuffer_loops = !!fbo_loops_info.attachmentFeedbackLoopLayout; + optional_features_support.framebuffer_loops = !!fbo_loops_info.attachmentFeedbackLoopLayout && get_driver_vendor() != driver_vendor::AMD; optional_features_support.extended_device_fault = !!device_fault_info.deviceFault; features = features2.features; From 8d55db334e0416f77188134bf1db128ec440a545 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 27 Dec 2025 16:46:27 +0100 Subject: [PATCH 56/59] Qt: deploy and apply Qt translations --- .ci/deploy-windows-clang.sh | 2 +- .ci/setup-windows.sh | 2 + rpcs3/rpcs3.vcxproj | 4 +- rpcs3/rpcs3qt/gui_application.cpp | 69 +++++++++++++++++++++++++++---- rpcs3/rpcs3qt/gui_application.h | 3 +- 5 files changed, 67 insertions(+), 13 deletions(-) diff --git a/.ci/deploy-windows-clang.sh b/.ci/deploy-windows-clang.sh index c95f82e7b8..0bf731e7c8 100644 --- a/.ci/deploy-windows-clang.sh +++ b/.ci/deploy-windows-clang.sh @@ -38,7 +38,7 @@ else echo "Failed to download translations.zip. Continuing without translations." exit 0 } - unzip -o translations.zip -d "./bin/share/qt6/translations" >/dev/null 2>&1 || \ + 7z x translations.zip -o"./bin/share/qt6/translations" >/dev/null 2>&1 || \ echo "Failed to extract translations.zip. Continuing without translations." rm -f translations.zip fi diff --git a/.ci/setup-windows.sh b/.ci/setup-windows.sh index f637cec9ad..d874c7a7f0 100755 --- a/.ci/setup-windows.sh +++ b/.ci/setup-windows.sh @@ -14,6 +14,7 @@ QT_DECL_URL="${QT_HOST}${QT_PREFIX}${QT_PREFIX_2}qtdeclarative${QT_SUFFIX}" QT_TOOL_URL="${QT_HOST}${QT_PREFIX}${QT_PREFIX_2}qttools${QT_SUFFIX}" QT_MM_URL="${QT_HOST}${QT_PREFIX}addons.qtmultimedia.${QT_PREFIX_2}qtmultimedia${QT_SUFFIX}" QT_SVG_URL="${QT_HOST}${QT_PREFIX}${QT_PREFIX_2}qtsvg${QT_SUFFIX}" +QT_TRANSLATIONS_URL="${QT_HOST}${QT_PREFIX}${QT_PREFIX_2}qttranslations${QT_SUFFIX}" LLVMLIBS_URL="https://github.com/RPCS3/llvm-mirror/releases/download/custom-build-win-${LLVM_VER}/llvmlibs_mt.7z" VULKAN_SDK_URL="https://www.dropbox.com/scl/fi/sjjh0fc4ld281pjbl2xzu/VulkanSDK-${VULKAN_VER}-Installer.exe?rlkey=f6wzc0lvms5vwkt2z3qabfv9d&dl=1" CCACHE_URL="https://github.com/ccache/ccache/releases/download/v4.11.2/ccache-4.11.2-windows-x86_64.zip" @@ -24,6 +25,7 @@ DEP_URLS=" \ $QT_TOOL_URL \ $QT_MM_URL \ $QT_SVG_URL \ + $QT_TRANSLATIONS_URL \ $LLVMLIBS_URL \ $VULKAN_SDK_URL\ $CCACHE_URL" diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index b38aa78567..83de843746 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -110,7 +110,7 @@ - $(QTDIR)\bin\windeployqt6 --no-compiler-runtime --no-opengl-sw --no-patchqt --no-translations --no-quick --no-system-d3d-compiler --no-system-dxc-compiler --no-quick-import --plugindir "$(TargetDir)qt6\plugins" --release "$(TargetPath)" + $(QTDIR)\bin\windeployqt6 --no-compiler-runtime --no-opengl-sw --no-patchqt --no-quick --no-system-d3d-compiler --no-system-dxc-compiler --no-quick-import --plugindir "$(TargetDir)qt6\plugins" --translationdir "$(TargetDir)qt6\translations" --release "$(TargetPath)" xcopy /y /d "$(SolutionDir)3rdparty\opencv\opencv\opencv412\build\x64\bin\opencv_world4120.dll" "$(OutDir)" @@ -169,7 +169,7 @@ - $(QTDIR)\bin\windeployqt6 --no-compiler-runtime --no-opengl-sw --no-patchqt --no-translations --no-quick --no-system-d3d-compiler --no-system-dxc-compiler --no-quick-import --plugindir "$(TargetDir)qt6\plugins" --debug "$(TargetPath)" + $(QTDIR)\bin\windeployqt6 --no-compiler-runtime --no-opengl-sw --no-patchqt --no-quick --no-system-d3d-compiler --no-system-dxc-compiler --no-quick-import --plugindir "$(TargetDir)qt6\plugins" --translationdir "$(TargetDir)qt6\translations" --debug "$(TargetPath)" xcopy /y /d "$(SolutionDir)3rdparty\opencv\opencv\opencv412\build\x64\bin\opencv_world4120.dll" "$(OutDir)" diff --git a/rpcs3/rpcs3qt/gui_application.cpp b/rpcs3/rpcs3qt/gui_application.cpp index e028aaefa8..443dcb4e0c 100644 --- a/rpcs3/rpcs3qt/gui_application.cpp +++ b/rpcs3/rpcs3qt/gui_application.cpp @@ -216,29 +216,74 @@ bool gui_application::Init() return true; } -void gui_application::SwitchTranslator(QTranslator& translator, const QString& filename, const QString& language_code) +void gui_application::SwitchTranslator(const QString& language_code) { // remove the old translator - removeTranslator(&translator); + removeTranslator(&m_translator); + for (QTranslator* qt_translator : m_qt_translators) + { + removeTranslator(qt_translator); + qt_translator->deleteLater(); + } + m_qt_translators.clear(); + const QString default_code = QLocale(QLocale::English).bcp47Name(); const QString lang_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath) + QStringLiteral("/"); - const QString file_path = lang_path + filename; + // Load qt translation files + const QDir dir(lang_path); + if (dir.exists()) + { + QStringList qm_files = dir.entryList(QStringList() << QStringLiteral("qt*_%1.qm").arg(language_code), QDir::Files | QDir::Readable); + if (qm_files.empty()) + { + qm_files = dir.entryList(QStringList() << QStringLiteral("qt*_%1.qm").arg(QLocale::languageToCode(QLocale(language_code).language())), QDir::Files | QDir::Readable); + } + + for (const QString& qm_file : qm_files) + { + const QString file_path = lang_path + qm_file; + QTranslator* qt_translator = new QTranslator(this); + + if (qt_translator->load(file_path)) + { + gui_log.notice("Installing translation: '%s'", file_path); + installTranslator(qt_translator); + m_qt_translators.push_back(std::move(qt_translator)); + } + else + { + gui_log.error("Failed to load translation: '%s'", file_path); + qt_translator->deleteLater(); + } + } + } + else + { + gui_log.error("Qt translation dir '%s' does not exist", lang_path); + } + + const QString file_path = lang_path + QStringLiteral("rpcs3_%1.qm").arg(language_code); if (QFileInfo(file_path).isFile()) { // load the new translator - if (translator.load(file_path)) + if (m_translator.load(file_path)) { - installTranslator(&translator); + gui_log.notice("Installing translation: '%s'", file_path); + installTranslator(&m_translator); + } + else + { + gui_log.error("Failed to load translation: '%s'", file_path); } } - else if (QString default_code = QLocale(QLocale::English).bcp47Name(); language_code != default_code) + else if (language_code != default_code) { // show error, but ignore default case "en", since it is handled in source code - gui_log.error("No translation file found in: %s", file_path); + gui_log.error("No translation file found in: '%s'", file_path); // reset current language to default "en" - set_language_code(std::move(default_code)); + set_language_code(default_code); } } @@ -260,7 +305,7 @@ void gui_application::LoadLanguage(const QString& language_code) // As per QT recommendations to avoid conflicts for POSIX functions std::setlocale(LC_NUMERIC, "C"); - SwitchTranslator(m_translator, QStringLiteral("rpcs3_%1.qm").arg(language_code), language_code); + SwitchTranslator(language_code); if (m_main_window) { @@ -285,6 +330,7 @@ QStringList gui_application::GetAvailableLanguageCodes() QStringList language_codes; const QString language_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath); + gui_log.notice("Checking languages in '%s'", language_path); if (QFileInfo(language_path).isDir()) { @@ -303,10 +349,15 @@ QStringList gui_application::GetAvailableLanguageCodes() } else { + gui_log.notice("Found language '%s' (%s)", language_code, filename); language_codes << language_code; } } } + else + { + gui_log.error("Language dir not found: '%s'", language_path); + } return language_codes; } diff --git a/rpcs3/rpcs3qt/gui_application.h b/rpcs3/rpcs3qt/gui_application.h index bec8424da4..948965ef47 100644 --- a/rpcs3/rpcs3qt/gui_application.h +++ b/rpcs3/rpcs3qt/gui_application.h @@ -81,7 +81,7 @@ private: return thread(); } - void SwitchTranslator(QTranslator& translator, const QString& filename, const QString& language_code); + void SwitchTranslator(const QString& language_code); void LoadLanguage(const QString& language_code); static QStringList GetAvailableLanguageCodes(); @@ -101,6 +101,7 @@ private: } m_native_event_filter; + std::vector m_qt_translators; QTranslator m_translator; QString m_language_code; static s32 m_language_id; From 050d2d3b2f34d80c1a395ae63c7a85c45ec40656 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Fri, 2 Jan 2026 20:10:15 +0100 Subject: [PATCH 57/59] macOs: Deploy Qt translations --- .ci/deploy-mac-arm64.sh | 4 ++++ .ci/deploy-mac.sh | 4 ++++ rpcs3/CMakeLists.txt | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.ci/deploy-mac-arm64.sh b/.ci/deploy-mac-arm64.sh index 8a27d04676..a4661dc0dd 100755 --- a/.ci/deploy-mac-arm64.sh +++ b/.ci/deploy-mac-arm64.sh @@ -57,6 +57,10 @@ else rm -f translations.zip fi +# Copy Qt translations manually +QT_TRANS="$WORKDIR/qt-downloader/$QT_VER/clang_64/translations" +cp $QT_TRANS/qt*.qm rpcs3.app/Contents/translations + # Hack install_name_tool -delete_rpath /opt/homebrew/lib RPCS3.app/Contents/MacOS/rpcs3 || echo "Hack for deleting rpath /opt/homebrew/lib not needed" install_name_tool -delete_rpath /opt/homebrew/opt/llvm@$LLVM_COMPILER_VER/lib RPCS3.app/Contents/MacOS/rpcs3 || echo "Hack for deleting rpath /opt/homebrew/opt/llvm@$LLVM_COMPILER_VER/lib not needed" diff --git a/.ci/deploy-mac.sh b/.ci/deploy-mac.sh index ec71fe0262..b0bfb4b455 100755 --- a/.ci/deploy-mac.sh +++ b/.ci/deploy-mac.sh @@ -58,6 +58,10 @@ else rm -f translations.zip fi +# Copy Qt translations manually +QT_TRANS="$WORKDIR/qt-downloader/$QT_VER/clang_64/translations" +cp $QT_TRANS/qt*.qm rpcs3.app/Contents/translations + # Need to do this rename hack due to case insensitive filesystem mv rpcs3.app RPCS3_.app mv RPCS3_.app RPCS3.app diff --git a/rpcs3/CMakeLists.txt b/rpcs3/CMakeLists.txt index 02a6bfeed4..e32597f792 100644 --- a/rpcs3/CMakeLists.txt +++ b/rpcs3/CMakeLists.txt @@ -148,8 +148,9 @@ if (NOT ANDROID) COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/bin/GuiConfigs $/GuiConfigs COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/bin/test $/test COMMAND "${WINDEPLOYQT_EXECUTABLE}" --no-compiler-runtime --no-opengl-sw --no-patchqt - --no-translations --no-system-d3d-compiler --no-system-dxc-compiler --no-quick-import + --no-system-d3d-compiler --no-system-dxc-compiler --no-quick-import --plugindir "$,$/plugins,$/share/qt6/plugins>" + --translationdir "$,$/translations,$/share/qt6/translations>" --verbose 0 "$") endif() From ea6bb77d57357908706f4fdf7081e2d924c08f1f Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 3 Jan 2026 01:54:44 +0100 Subject: [PATCH 58/59] Qt: Fix image dimensions passed to camera_video_sink::present --- rpcs3/rpcs3qt/camera_settings_dialog.cpp | 2 +- rpcs3/rpcs3qt/qt_camera_video_sink.cpp | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/rpcs3/rpcs3qt/camera_settings_dialog.cpp b/rpcs3/rpcs3qt/camera_settings_dialog.cpp index fca7432370..e680dd0436 100644 --- a/rpcs3/rpcs3qt/camera_settings_dialog.cpp +++ b/rpcs3/rpcs3qt/camera_settings_dialog.cpp @@ -273,7 +273,7 @@ void camera_settings_dialog::handle_qt_camera_change(const QVariant& item_data) int index = 0; bool success = false; const std::string key = camera_info.id().toStdString(); - cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(fmt::format("%s", camera_handler::qt), key, success); + const cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(fmt::format("%s", camera_handler::qt), key, success); if (success) { diff --git a/rpcs3/rpcs3qt/qt_camera_video_sink.cpp b/rpcs3/rpcs3qt/qt_camera_video_sink.cpp index 315d32d6e0..3dee6fa6e3 100644 --- a/rpcs3/rpcs3qt/qt_camera_video_sink.cpp +++ b/rpcs3/rpcs3qt/qt_camera_video_sink.cpp @@ -34,13 +34,13 @@ bool qt_camera_video_sink::present(const QVideoFrame& frame) } // Get image. This usually also converts the image to ARGB32. - QImage image = frame.toImage(); - const u32 width = image.isNull() ? 0 : static_cast(image.width()); - const u32 height = image.isNull() ? 0 : static_cast(image.height()); + QImage image = tmp.toImage(); + u32 width = image.isNull() ? 0 : static_cast(image.width()); + u32 height = image.isNull() ? 0 : static_cast(image.height()); if (image.isNull()) { - camera_log.warning("Image is invalid: pixel_format=%s, format=%d", tmp.pixelFormat(), static_cast(QVideoFrameFormat::imageFormatFromPixelFormat(tmp.pixelFormat()))); + camera_log.warning("Image is invalid: pixel_format=%s, format=%d", tmp.pixelFormat(), static_cast(QVideoFrameFormat::imageFormatFromPixelFormat(tmp.pixelFormat()))); } else { @@ -48,6 +48,8 @@ bool qt_camera_video_sink::present(const QVideoFrame& frame) if (m_width > 0 && m_height > 0 && m_width != width && m_height != height) { image = image.scaled(m_width, m_height, Qt::AspectRatioMode::IgnoreAspectRatio, Qt::SmoothTransformation); + width = static_cast(image.width()); + height = static_cast(image.height()); } // Determine image flip From 643d1102ccedd56f9915b86c53ae9d7251e4c736 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Mon, 27 Jan 2025 20:32:29 +0100 Subject: [PATCH 59/59] Add SDL camera handler --- rpcs3/Emu/Cell/Modules/cellGem.cpp | 9 +- .../HomeMenu/overlay_home_menu_settings.cpp | 10 +- rpcs3/Emu/system_config.h | 1 + rpcs3/Emu/system_config_types.cpp | 3 + rpcs3/Emu/system_config_types.h | 5 +- rpcs3/Input/sdl_camera_handler.cpp | 490 ++++++++++++++++++ rpcs3/Input/sdl_camera_handler.h | 49 ++ rpcs3/Input/sdl_camera_video_sink.cpp | 168 ++++++ rpcs3/Input/sdl_camera_video_sink.h | 34 ++ rpcs3/Input/sdl_instance.cpp | 2 +- rpcs3/headless_application.cpp | 3 + rpcs3/rpcs3.vcxproj | 4 + rpcs3/rpcs3.vcxproj.filters | 12 + rpcs3/rpcs3qt/CMakeLists.txt | 4 +- rpcs3/rpcs3qt/camera_settings_dialog.cpp | 358 +++++++++++++ rpcs3/rpcs3qt/camera_settings_dialog.h | 32 ++ rpcs3/rpcs3qt/emu_settings.cpp | 3 + rpcs3/rpcs3qt/gui_application.cpp | 10 + 18 files changed, 1192 insertions(+), 5 deletions(-) create mode 100644 rpcs3/Input/sdl_camera_handler.cpp create mode 100644 rpcs3/Input/sdl_camera_handler.h create mode 100644 rpcs3/Input/sdl_camera_video_sink.cpp create mode 100644 rpcs3/Input/sdl_camera_video_sink.h diff --git a/rpcs3/Emu/Cell/Modules/cellGem.cpp b/rpcs3/Emu/Cell/Modules/cellGem.cpp index 2de2d2cd3c..3c7b299af7 100644 --- a/rpcs3/Emu/Cell/Modules/cellGem.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGem.cpp @@ -1501,8 +1501,15 @@ void gem_config_data::operator()() vc = vc_attribute; } - if (g_cfg.io.camera != camera_handler::qt) + switch (g_cfg.io.camera) { +#ifdef HAVE_SDL3 + case camera_handler::sdl: +#endif + case camera_handler::qt: + break; + case camera_handler::fake: + case camera_handler::null: video_conversion_in_progress = false; done(); continue; diff --git a/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_settings.cpp b/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_settings.cpp index 4ff3bd7eae..c0e3a72b6f 100644 --- a/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_settings.cpp +++ b/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_settings.cpp @@ -80,9 +80,17 @@ namespace rsx add_checkbox(&g_cfg.io.keep_pads_connected, localized_string_id::HOME_MENU_SETTINGS_INPUT_KEEP_PADS_CONNECTED); add_checkbox(&g_cfg.io.show_move_cursor, localized_string_id::HOME_MENU_SETTINGS_INPUT_SHOW_PS_MOVE_CURSOR); - if (g_cfg.io.camera == camera_handler::qt) + switch (g_cfg.io.camera) { + #ifdef HAVE_SDL3 + case camera_handler::sdl: + #endif + case camera_handler::qt: add_dropdown(&g_cfg.io.camera_flip_option, localized_string_id::HOME_MENU_SETTINGS_INPUT_CAMERA_FLIP); + break; + case camera_handler::fake: + case camera_handler::null: + break; } add_dropdown(&g_cfg.io.pad_mode, localized_string_id::HOME_MENU_SETTINGS_INPUT_PAD_MODE); diff --git a/rpcs3/Emu/system_config.h b/rpcs3/Emu/system_config.h index 7c7b89fcf9..518e6eeb6e 100644 --- a/rpcs3/Emu/system_config.h +++ b/rpcs3/Emu/system_config.h @@ -267,6 +267,7 @@ struct cfg_root : cfg::node cfg::_enum camera_type{ this, "Camera type", fake_camera_type::unknown }; cfg::_enum camera_flip_option{ this, "Camera flip", camera_flip::none, true }; cfg::string camera_id{ this, "Camera ID", "Default", true }; + cfg::string sdl_camera_id{ this, "SDL Camera ID", "Default", true }; cfg::_enum move{ this, "Move", move_handler::null, true }; cfg::_enum buzz{ this, "Buzz emulated controller", buzz_handler::null }; cfg::_enum turntable{this, "Turntable emulated controller", turntable_handler::null}; diff --git a/rpcs3/Emu/system_config_types.cpp b/rpcs3/Emu/system_config_types.cpp index 52564c7d06..7c9c5ef592 100644 --- a/rpcs3/Emu/system_config_types.cpp +++ b/rpcs3/Emu/system_config_types.cpp @@ -358,6 +358,9 @@ void fmt_class_string::format(std::string& out, u64 arg) case camera_handler::null: return "Null"; case camera_handler::fake: return "Fake"; case camera_handler::qt: return "Qt"; +#ifdef HAVE_SDL3 + case camera_handler::sdl: return "SDL"; +#endif } return unknown; diff --git a/rpcs3/Emu/system_config_types.h b/rpcs3/Emu/system_config_types.h index c88555efaa..d58e8664fa 100644 --- a/rpcs3/Emu/system_config_types.h +++ b/rpcs3/Emu/system_config_types.h @@ -116,7 +116,10 @@ enum class camera_handler { null, fake, - qt + qt, +#ifdef HAVE_SDL3 + sdl, +#endif }; enum class camera_flip diff --git a/rpcs3/Input/sdl_camera_handler.cpp b/rpcs3/Input/sdl_camera_handler.cpp new file mode 100644 index 0000000000..4faf4eaf75 --- /dev/null +++ b/rpcs3/Input/sdl_camera_handler.cpp @@ -0,0 +1,490 @@ +#ifdef HAVE_SDL3 + +#include "stdafx.h" +#include "sdl_camera_handler.h" +#include "sdl_camera_video_sink.h" +#include "sdl_instance.h" +#include "Emu/system_config.h" +#include "Emu/System.h" +#include "Emu/Io/camera_config.h" + +LOG_CHANNEL(camera_log, "Camera"); + +#if !(SDL_VERSION_ATLEAST(3, 4, 0)) +namespace SDL_CameraPermissionState +{ + constexpr int SDL_CAMERA_PERMISSION_STATE_DENIED = -1; + constexpr int SDL_CAMERA_PERMISSION_STATE_PENDING = 0; + constexpr int SDL_CAMERA_PERMISSION_STATE_APPROVED = 1; +} +#endif + +template <> +void fmt_class_string::format(std::string& out, u64 arg) +{ + const SDL_CameraSpec& spec = get_object(arg); + out += fmt::format("format=0x%x, colorspace=0x%x, width=%d, height=%d, framerate_numerator=%d, framerate_denominator=%d, fps=%f", + static_cast(spec.format), static_cast(spec.colorspace), spec.width, spec.height, + spec.framerate_numerator, spec.framerate_denominator, spec.framerate_numerator / static_cast(spec.framerate_denominator)); +} + +std::vector sdl_camera_handler::get_drivers() +{ + std::vector drivers; + + if (const int num_drivers = SDL_GetNumCameraDrivers(); num_drivers > 0) + { + for (int i = 0; i < num_drivers; i++) + { + if (const char* driver = SDL_GetCameraDriver(i)) + { + camera_log.notice("Found driver: %s", driver); + drivers.push_back(driver); + continue; + } + + camera_log.error("Failed to get driver %d. SDL Error: %s", i, SDL_GetError()); + } + } + else + { + camera_log.error("No SDL camera drivers found"); + } + + return drivers; +} + +std::map sdl_camera_handler::get_cameras() +{ + int camera_count = 0; + if (SDL_CameraID* cameras = SDL_GetCameras(&camera_count)) + { + std::map camera_map; + + for (int i = 0; i < camera_count && cameras[i]; i++) + { + if (const char* name = SDL_GetCameraName(cameras[i])) + { + camera_log.notice("Found camera: name=%s", name); + camera_map[cameras[i]] = name; + continue; + } + + camera_log.error("Found camera (Failed to get name. SDL Error: %s", SDL_GetError()); + } + + SDL_free(cameras); + + if (camera_map.empty()) + { + camera_log.notice("No SDL cameras found"); + } + + return camera_map; + } + + camera_log.error("Could not get cameras! SDL Error: %s", SDL_GetError()); + return {}; +} + +sdl_camera_handler::sdl_camera_handler() : camera_handler_base() +{ + if (!g_cfg_camera.load()) + { + camera_log.notice("Could not load camera config. Using defaults."); + } + + if (!sdl_instance::get_instance().initialize()) + { + camera_log.error("Could not initialize SDL"); + return; + } + + // List available camera drivers + sdl_camera_handler::get_drivers(); + + // List available cameras + sdl_camera_handler::get_cameras(); +} + +sdl_camera_handler::~sdl_camera_handler() +{ + Emu.BlockingCallFromMainThread([&]() + { + close_camera(); + }); +} + +void sdl_camera_handler::reset() +{ + m_video_sink.reset(); + + if (m_camera) + { + SDL_CloseCamera(m_camera); + m_camera = nullptr; + } +} + +void sdl_camera_handler::open_camera() +{ + camera_log.notice("Loading camera"); + + if (const std::string camera_id = g_cfg.io.sdl_camera_id.to_string(); + m_camera_id != camera_id) + { + camera_log.notice("Switching camera from %s to %s", m_camera_id, camera_id); + camera_log.notice("Stopping old camera..."); + if (m_camera) + { + set_expected_state(camera_handler_state::open); + reset(); + } + m_camera_id = camera_id; + } + + // List available cameras + int camera_count = 0; + SDL_CameraID* cameras = SDL_GetCameras(&camera_count); + + if (!cameras) + { + camera_log.error("Could not get cameras! SDL Error: %s", SDL_GetError()); + set_state(camera_handler_state::closed); + return; + } + + if (camera_count <= 0) + { + camera_log.error("No cameras found"); + set_state(camera_handler_state::closed); + SDL_free(cameras); + return; + } + + m_sdl_camera_id = 0; + + if (m_camera_id == g_cfg.io.sdl_camera_id.def) + { + m_sdl_camera_id = cameras[0]; + } + else if (!m_camera_id.empty()) + { + for (int i = 0; i < camera_count && cameras[i]; i++) + { + if (const char* name = SDL_GetCameraName(cameras[i])) + { + if (m_camera_id == name) + { + m_sdl_camera_id = cameras[i]; + break; + } + } + } + } + + SDL_free(cameras); + + if (!m_sdl_camera_id) + { + camera_log.error("Camera %s not found", m_camera_id); + set_state(camera_handler_state::closed); + return; + } + + std::string camera_id; + + if (const char* name = SDL_GetCameraName(m_sdl_camera_id)) + { + camera_log.notice("Using camera: name=%s", name); + camera_id = name; + } + + SDL_CameraSpec used_spec + { + .format = SDL_PixelFormat::SDL_PIXELFORMAT_RGBA32, + .colorspace = SDL_Colorspace::SDL_COLORSPACE_RGB_DEFAULT, + .width = static_cast(m_width), + .height = static_cast(m_height), + .framerate_numerator = 30, + .framerate_denominator = 1 + }; + + int num_formats = 0; + if (SDL_CameraSpec** specs = SDL_GetCameraSupportedFormats(m_sdl_camera_id, &num_formats)) + { + if (num_formats <= 0) + { + camera_log.error("No SDL camera specs found"); + } + else + { + // Load selected settings from config file + bool success = false; + cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(fmt::format("%s", camera_handler::sdl), camera_id, success); + + if (success) + { + camera_log.notice("Found config entry for camera \"%s\" (m_camera_id='%s')", camera_id, m_camera_id); + + // List all available settings and choose the proper value if possible. + constexpr double epsilon = 0.001; + success = false; + + for (int i = 0; i < num_formats; i++) + { + if (!specs[i]) continue; + + const SDL_CameraSpec& spec = *specs[i]; + const f64 fps = spec.framerate_numerator / static_cast(spec.framerate_denominator); + + if (spec.width == cfg_setting.width && + spec.height == cfg_setting.height && + fps >= (cfg_setting.min_fps - epsilon) && + fps <= (cfg_setting.min_fps + epsilon) && + fps >= (cfg_setting.max_fps - epsilon) && + fps <= (cfg_setting.max_fps + epsilon) && + spec.format == static_cast(cfg_setting.format) && + spec.colorspace == static_cast(cfg_setting.colorspace)) + { + // Apply settings. + camera_log.notice("Setting camera spec: %s", spec); + + // TODO: SDL converts the image for us. We would have to do this manually if we want to use other formats. + //used_spec = spec; + used_spec.width = spec.width; + used_spec.height = spec.height; + used_spec.framerate_numerator = spec.framerate_numerator; + used_spec.framerate_denominator = spec.framerate_denominator; + success = true; + break; + } + } + + if (!success) + { + camera_log.warning("No matching camera setting available for the camera config: max_fps=%f, width=%d, height=%d, format=%d, colorspace=%d", + cfg_setting.max_fps, cfg_setting.width, cfg_setting.height, cfg_setting.format, cfg_setting.colorspace); + } + } + + if (!success) + { + camera_log.notice("Using default camera spec: %s", used_spec); + } + } + SDL_free(specs); + } + else + { + camera_log.error("No SDL camera specs found. SDL Error: %s", SDL_GetError()); + } + + reset(); + + camera_log.notice("Requesting camera spec: %s", used_spec); + + m_camera = SDL_OpenCamera(m_sdl_camera_id, &used_spec); + + if (!m_camera) + { + if (!m_camera_id.empty()) camera_log.notice("Camera disabled"); + else camera_log.error("No camera found"); + set_state(camera_handler_state::closed); + return; + } + + if (const char* driver = SDL_GetCurrentCameraDriver()) + { + camera_log.notice("Using driver: %s", driver); + } + + if (SDL_CameraSpec spec {}; SDL_GetCameraFormat(m_camera, &spec)) + { + camera_log.notice("Using camera spec: %s", spec); + } + else + { + camera_log.error("Could not get camera spec. SDL Error: %s", SDL_GetError()); + } + + const SDL_CameraPosition position = SDL_GetCameraPosition(m_sdl_camera_id); + const bool front_facing = position == SDL_CameraPosition::SDL_CAMERA_POSITION_FRONT_FACING; + + if (const SDL_PropertiesID property_id = SDL_GetCameraProperties(m_camera); property_id != 0) + { + if (!SDL_EnumerateProperties(property_id, [](void* /*userdata*/, SDL_PropertiesID /*props*/, const char* name) + { + if (name) camera_log.notice("SDL camera property available: %s", name); + }, nullptr)) + { + camera_log.warning("SDL_EnumerateProperties failed. SDL Error: %s", SDL_GetError()); + } + } + else + { + camera_log.warning("SDL_GetCameraProperties failed. SDL Error: %s", SDL_GetError()); + } + + m_video_sink = std::make_unique(front_facing, m_camera); + m_video_sink->set_resolution(m_width, m_height); + m_video_sink->set_format(m_format, m_bytesize); + m_video_sink->set_mirrored(m_mirrored); + + set_state(camera_handler_state::open); +} + +void sdl_camera_handler::close_camera() +{ + camera_log.notice("Unloading camera"); + + if (!m_camera) + { + if (m_camera_id.empty()) camera_log.notice("Camera disabled"); + else camera_log.error("No camera found"); + set_state(camera_handler_state::closed); + return; + } + + // Unload/close camera + reset(); + + set_state(camera_handler_state::closed); +} + +void sdl_camera_handler::start_camera() +{ + camera_log.notice("Starting camera"); + + if (!m_camera) + { + if (m_camera_id.empty()) camera_log.notice("Camera disabled"); + else camera_log.error("No camera found"); + set_state(camera_handler_state::closed); + return; + } + + const auto camera_permission = SDL_GetCameraPermissionState(m_camera); + switch (camera_permission) + { + case SDL_CameraPermissionState::SDL_CAMERA_PERMISSION_STATE_DENIED: + camera_log.error("Camera permission denied"); + set_state(camera_handler_state::closed); + reset(); + return; + case SDL_CameraPermissionState::SDL_CAMERA_PERMISSION_STATE_PENDING: + // TODO: try to get permission + break; + case SDL_CameraPermissionState::SDL_CAMERA_PERMISSION_STATE_APPROVED: + break; + default: + fmt::throw_exception("Unknown SDL_CameraPermissionState %d", static_cast(camera_permission)); + } + + // Start camera. We will start receiving frames now. + set_state(camera_handler_state::running); +} + +void sdl_camera_handler::stop_camera() +{ + camera_log.notice("Stopping camera"); + + if (!m_camera) + { + if (m_camera_id.empty()) camera_log.notice("Camera disabled"); + else camera_log.error("No camera found"); + set_state(camera_handler_state::closed); + return; + } + + // Stop camera. The camera will still be drawing power. + set_expected_state(camera_handler_state::open); +} + +void sdl_camera_handler::set_format(s32 format, u32 bytesize) +{ + m_format = format; + m_bytesize = bytesize; + + if (m_video_sink) + { + m_video_sink->set_format(m_format, m_bytesize); + } +} + +void sdl_camera_handler::set_frame_rate(u32 frame_rate) +{ + m_frame_rate = frame_rate; +} + +void sdl_camera_handler::set_resolution(u32 width, u32 height) +{ + m_width = width; + m_height = height; + + if (m_video_sink) + { + m_video_sink->set_resolution(m_width, m_height); + } +} + +void sdl_camera_handler::set_mirrored(bool mirrored) +{ + m_mirrored = mirrored; + + if (m_video_sink) + { + m_video_sink->set_mirrored(m_mirrored); + } +} + +u64 sdl_camera_handler::frame_number() const +{ + return m_video_sink ? m_video_sink->frame_number() : 0; +} + +camera_handler_base::camera_handler_state sdl_camera_handler::get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read) +{ + width = 0; + height = 0; + frame_number = 0; + bytes_read = 0; + + if (const std::string camera_id = g_cfg.io.sdl_camera_id.to_string(); + m_camera_id != camera_id) + { + camera_log.notice("Switching cameras"); + set_state(camera_handler_state::closed); + return camera_handler_state::closed; + } + + if (m_camera_id.empty()) + { + camera_log.notice("Camera disabled"); + set_state(camera_handler_state::closed); + return camera_handler_state::closed; + } + + if (!m_camera || !m_video_sink) + { + camera_log.fatal("Error: camera invalid"); + set_state(camera_handler_state::closed); + return camera_handler_state::closed; + } + + // Backup current state. State may change through events. + const camera_handler_state current_state = get_state(); + + if (current_state == camera_handler_state::running) + { + m_video_sink->get_image(buf, size, width, height, frame_number, bytes_read); + } + else + { + camera_log.error("Camera not running (m_state=%d)", static_cast(current_state)); + } + + return current_state; +} + +#endif diff --git a/rpcs3/Input/sdl_camera_handler.h b/rpcs3/Input/sdl_camera_handler.h new file mode 100644 index 0000000000..1e177ff32e --- /dev/null +++ b/rpcs3/Input/sdl_camera_handler.h @@ -0,0 +1,49 @@ +#pragma once + +#ifdef HAVE_SDL3 + +#include "Emu/Io/camera_handler_base.h" + +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "SDL3/SDL.h" +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif + +#include + +class sdl_camera_video_sink; + +class sdl_camera_handler : public camera_handler_base +{ +public: + sdl_camera_handler(); + virtual ~sdl_camera_handler(); + + void open_camera() override; + void close_camera() override; + void start_camera() override; + void stop_camera() override; + void set_format(s32 format, u32 bytesize) override; + void set_frame_rate(u32 frame_rate) override; + void set_resolution(u32 width, u32 height) override; + void set_mirrored(bool mirrored) override; + u64 frame_number() const override; + camera_handler_state get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read) override; + + static std::vector get_drivers(); + static std::map get_cameras(); + +private: + void reset(); + + std::string m_camera_id; + SDL_CameraID m_sdl_camera_id = 0; + SDL_Camera* m_camera = nullptr; + std::unique_ptr m_video_sink; +}; + +#endif diff --git a/rpcs3/Input/sdl_camera_video_sink.cpp b/rpcs3/Input/sdl_camera_video_sink.cpp new file mode 100644 index 0000000000..346e0a6874 --- /dev/null +++ b/rpcs3/Input/sdl_camera_video_sink.cpp @@ -0,0 +1,168 @@ +#ifdef HAVE_SDL3 + +#include "stdafx.h" +#include "sdl_camera_video_sink.h" +#include "Utilities/Thread.h" +#include "Emu/system_config.h" + +LOG_CHANNEL(camera_log, "Camera"); + +sdl_camera_video_sink::sdl_camera_video_sink(bool front_facing, SDL_Camera* camera) + : camera_video_sink(front_facing), m_camera(camera) +{ + ensure(m_camera); + + m_thread = std::make_unique(&sdl_camera_video_sink::run, this); +} + +sdl_camera_video_sink::~sdl_camera_video_sink() +{ + m_terminate = true; + + if (m_thread && m_thread->joinable()) + { + m_thread->join(); + m_thread.reset(); + } +} + +void sdl_camera_video_sink::present(SDL_Surface* frame) +{ + const int bytes_per_pixel = SDL_BYTESPERPIXEL(frame->format); + const u32 src_width_in_bytes = std::max(0, frame->w * bytes_per_pixel); + const u32 dst_width_in_bytes = std::max(0, m_width * bytes_per_pixel); + const u8* pixels = reinterpret_cast(frame->pixels); + + bool use_buffer = false; + + // Scale image if necessary + const bool scale_image = m_width > 0 && m_height > 0 && m_width != static_cast(frame->w) && m_height != static_cast(frame->h); + + // Determine image flip + const camera_flip flip_setting = g_cfg.io.camera_flip_option; + + bool flip_horizontally = m_front_facing; // Front facing cameras are flipped already + if (flip_setting == camera_flip::horizontal || flip_setting == camera_flip::both) + { + flip_horizontally = !flip_horizontally; + } + if (m_mirrored) // Set by the game + { + flip_horizontally = !flip_horizontally; + } + + bool flip_vertically = false; + if (flip_setting == camera_flip::vertical || flip_setting == camera_flip::both) + { + flip_vertically = !flip_vertically; + } + + // Flip image if necessary + if (flip_horizontally || flip_vertically || scale_image) + { + m_buffer.resize(m_height * dst_width_in_bytes); + use_buffer = true; + + if (m_width > 0 && m_height > 0 && frame->w > 0 && frame->h > 0) + { + const f32 scale_x = frame->w / static_cast(m_width); + const f32 scale_y = frame->h / static_cast(m_height); + + if (flip_horizontally && flip_vertically) + { + for (u32 y = 0; y < m_height; y++) + { + const u32 src_y = frame->h - static_cast(scale_y * y) - 1; + const u8* src = pixels + src_y * src_width_in_bytes; + u8* dst = &m_buffer[y * dst_width_in_bytes]; + + for (u32 x = 0; x < m_width; x++) + { + const u32 src_x = frame->w - static_cast(scale_x * x) - 1; + std::memcpy(dst + x * bytes_per_pixel, src + src_x * bytes_per_pixel, bytes_per_pixel); + } + } + } + else if (flip_horizontally) + { + for (u32 y = 0; y < m_height; y++) + { + const u32 src_y = static_cast(scale_y * y); + const u8* src = pixels + src_y * src_width_in_bytes; + u8* dst = &m_buffer[y * dst_width_in_bytes]; + + for (u32 x = 0; x < m_width; x++) + { + const u32 src_x = frame->w - static_cast(scale_x * x) - 1; + std::memcpy(dst + x * bytes_per_pixel, src + src_x * bytes_per_pixel, bytes_per_pixel); + } + } + } + else if (flip_vertically) + { + for (u32 y = 0; y < m_height; y++) + { + const u32 src_y = frame->h - static_cast(scale_y * y) - 1; + const u8* src = pixels + src_y * src_width_in_bytes; + u8* dst = &m_buffer[y * dst_width_in_bytes]; + + for (u32 x = 0; x < m_width; x++) + { + const u32 src_x = static_cast(scale_x * x); + std::memcpy(dst + x * bytes_per_pixel, src + src_x * bytes_per_pixel, bytes_per_pixel); + } + } + } + else + { + for (u32 y = 0; y < m_height; y++) + { + const u32 src_y = static_cast(scale_y * y); + const u8* src = pixels + src_y * src_width_in_bytes; + u8* dst = &m_buffer[y * dst_width_in_bytes]; + + for (u32 x = 0; x < m_width; x++) + { + const u32 src_x = static_cast(scale_x * x); + std::memcpy(dst + x * bytes_per_pixel, src + src_x * bytes_per_pixel, bytes_per_pixel); + } + } + } + } + } + + if (use_buffer) + { + camera_video_sink::present(m_width, m_height, dst_width_in_bytes, bytes_per_pixel, [src = m_buffer.data(), dst_width_in_bytes](u32 y){ return src + y * dst_width_in_bytes; }); + } + else + { + camera_video_sink::present(frame->w, frame->h, frame->pitch, bytes_per_pixel, [pixels, pitch = frame->pitch](u32 y){ return pixels + y * pitch; }); + } +} + +void sdl_camera_video_sink::run() +{ + thread_base::set_name("SDL Capture Thread"); + + camera_log.notice("SDL Capture Thread started"); + + while (!m_terminate) + { + // Copy latest image into out buffer. + u64 timestamp_ns = 0; + SDL_Surface* frame = SDL_AcquireCameraFrame(m_camera, ×tamp_ns); + if (!frame) + { + // No new frame + std::this_thread::sleep_for(100us); + continue; + } + + present(frame); + + SDL_ReleaseCameraFrame(m_camera, frame); + } +} + +#endif diff --git a/rpcs3/Input/sdl_camera_video_sink.h b/rpcs3/Input/sdl_camera_video_sink.h new file mode 100644 index 0000000000..010834ff58 --- /dev/null +++ b/rpcs3/Input/sdl_camera_video_sink.h @@ -0,0 +1,34 @@ +#pragma once + +#ifdef HAVE_SDL3 + +#include "Input/camera_video_sink.h" + +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "SDL3/SDL.h" +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif + +#include + +class sdl_camera_video_sink final : public camera_video_sink +{ +public: + sdl_camera_video_sink(bool front_facing, SDL_Camera* camera); + virtual ~sdl_camera_video_sink(); + +private: + void present(SDL_Surface* frame); + void run(); + + std::vector m_buffer; + atomic_t m_terminate = false; + SDL_Camera* m_camera = nullptr; + std::unique_ptr m_thread; +}; + +#endif diff --git a/rpcs3/Input/sdl_instance.cpp b/rpcs3/Input/sdl_instance.cpp index 7310cf5f8c..9850974492 100644 --- a/rpcs3/Input/sdl_instance.cpp +++ b/rpcs3/Input/sdl_instance.cpp @@ -102,7 +102,7 @@ bool sdl_instance::initialize_impl() set_hint(SDL_HINT_JOYSTICK_HIDAPI_PS3, "1"); #endif - if (!SDL_Init(SDL_INIT_GAMEPAD | SDL_INIT_HAPTIC)) + if (!SDL_Init(SDL_INIT_GAMEPAD | SDL_INIT_HAPTIC | SDL_INIT_CAMERA)) { sdl_log.error("Could not initialize! SDL Error: %s", SDL_GetError()); return false; diff --git a/rpcs3/headless_application.cpp b/rpcs3/headless_application.cpp index 58047671a6..135a61c491 100644 --- a/rpcs3/headless_application.cpp +++ b/rpcs3/headless_application.cpp @@ -102,6 +102,9 @@ void headless_application::InitializeCallbacks() return std::make_shared(); } case camera_handler::qt: +#ifdef HAVE_SDL3 + case camera_handler::sdl: +#endif { fmt::throw_exception("Headless mode can not be used with this camera handler. Current handler: %s", g_cfg.io.camera.get()); } diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 83de843746..9bef8696fe 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -201,6 +201,8 @@ + + @@ -1070,6 +1072,8 @@ + + diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index fbe4fa4132..5d9f844e63 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -1248,6 +1248,12 @@ Io\camera + + Io\camera + + + Io\camera + @@ -1481,6 +1487,12 @@ Io\camera + + Io\camera + + + Io\camera + diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index ba6d95806a..9352f58823 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -163,8 +163,10 @@ add_library(rpcs3_ui STATIC ../Input/ps_move_tracker.cpp ../Input/raw_mouse_config.cpp ../Input/raw_mouse_handler.cpp - ../Input/sdl_pad_handler.cpp + ../Input/sdl_camera_handler.cpp + ../Input/sdl_camera_video_sink.cpp ../Input/sdl_instance.cpp + ../Input/sdl_pad_handler.cpp ../Input/skateboard_pad_handler.cpp ../Input/xinput_pad_handler.cpp diff --git a/rpcs3/rpcs3qt/camera_settings_dialog.cpp b/rpcs3/rpcs3qt/camera_settings_dialog.cpp index e680dd0436..9bbac21ea4 100644 --- a/rpcs3/rpcs3qt/camera_settings_dialog.cpp +++ b/rpcs3/rpcs3qt/camera_settings_dialog.cpp @@ -12,6 +12,11 @@ #include #include +#ifdef HAVE_SDL3 +#include "Input/sdl_instance.h" +#include "Input/sdl_camera_handler.h" +#endif + LOG_CHANNEL(camera_log, "Camera"); template <> @@ -56,6 +61,81 @@ void fmt_class_string::format(std::string& out, }); } +#ifdef HAVE_SDL3 +static QString sdl_pixelformat_to_string(SDL_PixelFormat format) +{ + switch (format) + { + case SDL_PixelFormat::SDL_PIXELFORMAT_UNKNOWN: return "UNKNOWN"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX1LSB: return "INDEX1LSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX1MSB: return "INDEX1MSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX2LSB: return "INDEX2LSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX2MSB: return "INDEX2MSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX4LSB: return "INDEX4LSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX4MSB: return "INDEX4MSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX8: return "INDEX8"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB332: return "RGB332"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XRGB4444: return "XRGB4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XBGR4444: return "XBGR4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XRGB1555: return "XRGB1555"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XBGR1555: return "XBGR1555"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB4444: return "ARGB4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA4444: return "RGBA4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR4444: return "ABGR4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA4444: return "BGRA4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB1555: return "ARGB1555"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA5551: return "RGBA5551"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR1555: return "ABGR1555"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA5551: return "BGRA5551"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB565: return "RGB565"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGR565: return "BGR565"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB24: return "RGB24"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGR24: return "BGR24"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XRGB8888: return "XRGB8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBX8888: return "RGBX8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XBGR8888: return "XBGR8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRX8888: return "BGRX8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB8888: return "ARGB8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA8888: return "RGBA8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR8888: return "ABGR8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA8888: return "BGRA8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XRGB2101010: return "XRGB2101010"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XBGR2101010: return "XBGR2101010"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB2101010: return "ARGB2101010"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR2101010: return "ABGR2101010"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB48: return "RGB48"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGR48: return "BGR48"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA64: return "RGBA64"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB64: return "ARGB64"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA64: return "BGRA64"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR64: return "ABGR64"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB48_FLOAT: return "RGB48_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGR48_FLOAT: return "BGR48_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA64_FLOAT: return "RGBA64_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB64_FLOAT: return "ARGB64_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA64_FLOAT: return "BGRA64_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR64_FLOAT: return "ABGR64_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB96_FLOAT: return "RGB96_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGR96_FLOAT: return "BGR96_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA128_FLOAT: return "RGBA128_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB128_FLOAT: return "ARGB128_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA128_FLOAT: return "BGRA128_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR128_FLOAT: return "ABGR128_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_YV12: return "YV12"; + case SDL_PixelFormat::SDL_PIXELFORMAT_IYUV: return "IYUV"; + case SDL_PixelFormat::SDL_PIXELFORMAT_YUY2: return "YUY2"; + case SDL_PixelFormat::SDL_PIXELFORMAT_UYVY: return "UYVY"; + case SDL_PixelFormat::SDL_PIXELFORMAT_YVYU: return "YVYU"; + case SDL_PixelFormat::SDL_PIXELFORMAT_NV12: return "NV12"; + case SDL_PixelFormat::SDL_PIXELFORMAT_NV21: return "NV21"; + case SDL_PixelFormat::SDL_PIXELFORMAT_P010: return "P010"; + case SDL_PixelFormat::SDL_PIXELFORMAT_EXTERNAL_OES: return "EXTERNAL_OES"; + case SDL_PixelFormat::SDL_PIXELFORMAT_MJPG: return "MJPG"; + default: return QObject::tr("Unknown: %0").arg(static_cast(format)); + } +} +#endif + Q_DECLARE_METATYPE(QCameraDevice); camera_settings_dialog::camera_settings_dialog(QWidget* parent) @@ -69,6 +149,9 @@ camera_settings_dialog::camera_settings_dialog(QWidget* parent) load_config(); ui->combo_handlers->addItem("Qt", QVariant::fromValue(static_cast(camera_handler::qt))); +#ifdef HAVE_SDL3 + ui->combo_handlers->addItem("SDL", QVariant::fromValue(static_cast(camera_handler::sdl))); +#endif connect(ui->combo_handlers, &QComboBox::currentIndexChanged, this, &camera_settings_dialog::handle_handler_change); connect(ui->combo_camera, &QComboBox::currentIndexChanged, this, &camera_settings_dialog::handle_camera_change); @@ -115,6 +198,24 @@ void camera_settings_dialog::reset_cameras() { m_media_capture_session.reset(); m_camera.reset(); + +#ifdef HAVE_SDL3 + m_video_frame_input.reset(); + + if (m_sdl_thread) + { + auto& thread = *m_sdl_thread; + thread = thread_state::aborting; + thread(); + m_sdl_thread.reset(); + } + + if (m_sdl_camera) + { + SDL_CloseCamera(m_sdl_camera); + m_sdl_camera = nullptr; + } +#endif } void camera_settings_dialog::handle_handler_change(int index) @@ -149,6 +250,29 @@ void camera_settings_dialog::handle_handler_change(int index) } break; } +#ifdef HAVE_SDL3 + case camera_handler::sdl: + { + if (!sdl_instance::get_instance().initialize()) + { + camera_log.error("Could not initialize SDL"); + break; + } + + // Log camera drivers + sdl_camera_handler::get_drivers(); + + // Get cameras + const std::map cameras = sdl_camera_handler::get_cameras(); + + // Add cameras + for (const auto& [camera_id, name] : cameras) + { + ui->combo_camera->addItem(QString::fromStdString(name), QVariant::fromValue(static_cast(camera_id))); + } + break; + } +#endif default: fmt::throw_exception("Unexpected camera handler %d", static_cast(m_handler)); } @@ -174,6 +298,11 @@ void camera_settings_dialog::handle_camera_change(int index) case camera_handler::qt: handle_qt_camera_change(ui->combo_camera->itemData(index)); break; +#ifdef HAVE_SDL3 + case camera_handler::sdl: + handle_sdl_camera_change(ui->combo_camera->itemText(index), ui->combo_camera->itemData(index)); + break; +#endif default: fmt::throw_exception("Unexpected camera handler %d", static_cast(m_handler)); } @@ -198,6 +327,11 @@ void camera_settings_dialog::handle_settings_change(int index) case camera_handler::qt: handle_qt_settings_change(ui->combo_settings->itemData(index)); break; +#ifdef HAVE_SDL3 + case camera_handler::sdl: + handle_sdl_settings_change(ui->combo_settings->itemData(index)); + break; +#endif default: fmt::throw_exception("Unexpected camera handler %d", static_cast(m_handler)); } @@ -339,6 +473,230 @@ void camera_settings_dialog::handle_qt_settings_change(const QVariant& item_data m_camera->start(); } +#ifdef HAVE_SDL3 +void camera_settings_dialog::handle_sdl_camera_change(const QString& name, const QVariant& item_data) +{ + if (!item_data.canConvert()) + { + ui->combo_settings->clear(); + return; + } + + const u32 camera_id = item_data.value(); + + if (!camera_id) + { + ui->combo_settings->clear(); + return; + } + + ui->combo_settings->blockSignals(true); + ui->combo_settings->clear(); + + std::vector settings; + + int num_formats = 0; + if (SDL_CameraSpec** specs = SDL_GetCameraSupportedFormats(camera_id, &num_formats)) + { + if (num_formats <= 0) + { + camera_log.error("No SDL camera specs found"); + } + else + { + for (int i = 0; i < num_formats; i++) + { + if (!specs[i]) continue; + settings.push_back(*specs[i]); + } + } + SDL_free(specs); + } + else + { + camera_log.error("No SDL camera specs found. SDL Error: %s", SDL_GetError()); + } + + std::sort(settings.begin(), settings.end(), [](const SDL_CameraSpec& l, const SDL_CameraSpec& r) -> bool + { + const f32 l_fps = l.framerate_numerator / static_cast(l.framerate_denominator); + const f32 r_fps = r.framerate_numerator / static_cast(r.framerate_denominator); + + if (l.width > r.width) return true; + if (l.width < r.width) return false; + if (l.height > r.height) return true; + if (l.height < r.height) return false; + if (l_fps > r_fps) return true; + if (l_fps < r_fps) return false; + if (l.format > r.format) return true; + if (l.format < r.format) return false; + if (l.colorspace > r.colorspace) return true; + if (l.colorspace < r.colorspace) return false; + return false; + }); + + for (const SDL_CameraSpec& setting : settings) + { + const f32 fps = setting.framerate_numerator / static_cast(setting.framerate_denominator); + const QString description = tr("%0x%1, %2 FPS, Format=%3") + .arg(setting.width) + .arg(setting.height) + .arg(fps) + .arg(sdl_pixelformat_to_string(setting.format)); + ui->combo_settings->addItem(description, QVariant::fromValue(setting)); + } + ui->combo_settings->blockSignals(false); + + if (ui->combo_settings->count() == 0) + { + ui->combo_settings->setEnabled(false); + return; + } + + // Load selected settings from config file + int index = 0; + bool success = false; + cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(fmt::format("%s", camera_handler::sdl), name.toStdString(), success); + + if (success) + { + camera_log.notice("Found config entry for camera \"%s\"", name); + + // Select matching dropdown entry + constexpr double epsilon = 0.001; + + for (int i = 0; i < ui->combo_settings->count(); i++) + { + const QVariant var = ui->combo_settings->itemData(i); + if (!var.canConvert()) + { + camera_log.error("Failed to convert itemData to SDL_CameraSpec"); + continue; + } + + const SDL_CameraSpec tmp = var.value(); + const f32 fps = tmp.framerate_numerator / static_cast(tmp.framerate_denominator); + + if (tmp.width == cfg_setting.width && + tmp.height == cfg_setting.height && + fps >= (cfg_setting.min_fps - epsilon) && + fps <= (cfg_setting.min_fps + epsilon) && + fps >= (cfg_setting.max_fps - epsilon) && + fps <= (cfg_setting.max_fps + epsilon) && + tmp.format == static_cast(cfg_setting.format) && + tmp.colorspace == static_cast(cfg_setting.colorspace)) + { + index = i; + break; + } + } + } + + m_sdl_camera_id = camera_id; + + ui->combo_settings->setCurrentIndex(std::max(0, index)); + ui->combo_settings->setEnabled(true); +} + +void camera_settings_dialog::handle_sdl_settings_change(const QVariant& item_data) +{ + reset_cameras(); + + if (item_data.canConvert()) + { + // TODO: SDL converts the image for us. We would have to do this manually if we want to use other formats. + const SDL_CameraSpec setting = item_data.value(); + const SDL_CameraSpec used_spec + { + .format = SDL_PixelFormat::SDL_PIXELFORMAT_RGBA32, + .colorspace = SDL_Colorspace::SDL_COLORSPACE_RGB_DEFAULT, + .width = setting.width, + .height = setting.height, + .framerate_numerator = setting.framerate_numerator, + .framerate_denominator = setting.framerate_denominator + }; + + m_sdl_camera = SDL_OpenCamera(m_sdl_camera_id, &used_spec); + + m_video_frame_input = std::make_unique(); + + m_media_capture_session = std::make_unique(nullptr); + m_media_capture_session->setVideoFrameInput(m_video_frame_input.get()); + m_media_capture_session->setVideoOutput(ui->videoWidget); + + connect(this, &camera_settings_dialog::sdl_frame_ready, m_video_frame_input.get(), [this]() + { + // It was observed that connecting sendVideoFrame directly can soft-lock the software. + // So let's just create the video frame here and call it manually. + std::unique_lock lock(m_sdl_image_mutex, std::defer_lock); + if (lock.try_lock() && m_video_frame_input && !m_sdl_image.isNull()) + { + const QVideoFrame video_frame(m_sdl_image); + if (video_frame.isValid()) + { + m_video_frame_input->sendVideoFrame(video_frame); + } + } + }); + + const f32 fps = setting.framerate_numerator / static_cast(setting.framerate_denominator); + + cfg_camera::camera_setting cfg_setting {}; + cfg_setting.width = setting.width; + cfg_setting.height = setting.height; + cfg_setting.min_fps = fps; + cfg_setting.max_fps = fps; + cfg_setting.format = static_cast(setting.format); + cfg_setting.colorspace = static_cast(setting.colorspace); + g_cfg_camera.set_camera_setting(fmt::format("%s", camera_handler::sdl), ui->combo_camera->currentText().toStdString(), cfg_setting); + } + + if (!m_sdl_camera) + { + camera_log.error("Failed to open SDL camera %d. SDL Error: %s", m_sdl_camera_id, SDL_GetError()); + QMessageBox::warning(this, tr("Camera not available"), tr("The selected camera is not available.\nIt might be blocked by another application.")); + return; + } + + m_sdl_thread = std::make_unique>>("GUI SDL Capture Thread", [this](){ run_sdl(); }); +} + +void camera_settings_dialog::run_sdl() +{ + camera_log.notice("GUI SDL Capture Thread started"); + + while (thread_ctrl::state() != thread_state::aborting) + { + // Copy latest image into out buffer. + u64 timestamp_ns = 0; + SDL_Surface* frame = SDL_AcquireCameraFrame(m_sdl_camera, ×tamp_ns); + if (!frame) + { + // No new frame + thread_ctrl::wait_for(1000); + continue; + } + + { + // Map image + const QImage::Format format = SDL_ISPIXELFORMAT_ALPHA(frame->format) ? QImage::Format_RGBA8888 : QImage::Format_RGB888; + const QImage image = QImage(reinterpret_cast(frame->pixels), frame->w, frame->h, format); + + // Copy image to prevent memory access violations + { + std::lock_guard lock(m_sdl_image_mutex); + m_sdl_image = image.copy(); + } + + // Notify UI + Q_EMIT sdl_frame_ready(); + } + + SDL_ReleaseCameraFrame(m_sdl_camera, frame); + } +} +#endif + void camera_settings_dialog::load_config() { if (!g_cfg_camera.load()) diff --git a/rpcs3/rpcs3qt/camera_settings_dialog.h b/rpcs3/rpcs3qt/camera_settings_dialog.h index b7c0750c2c..879686722f 100644 --- a/rpcs3/rpcs3qt/camera_settings_dialog.h +++ b/rpcs3/rpcs3qt/camera_settings_dialog.h @@ -1,10 +1,25 @@ #pragma once #include "Emu/system_config_types.h" +#include "Utilities/Thread.h" #include #include #include +#include + +#include + +#ifdef HAVE_SDL3 +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "SDL3/SDL.h" +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif +#endif namespace Ui { @@ -19,6 +34,9 @@ public: camera_settings_dialog(QWidget* parent = nullptr); virtual ~camera_settings_dialog(); +Q_SIGNALS: + void sdl_frame_ready(); + private Q_SLOTS: void handle_handler_change(int index); void handle_camera_change(int index); @@ -34,6 +52,20 @@ private: void handle_qt_camera_change(const QVariant& item_data); void handle_qt_settings_change(const QVariant& item_data); +#ifdef HAVE_SDL3 + void handle_sdl_camera_change(const QString& name, const QVariant& item_data); + void handle_sdl_settings_change(const QVariant& item_data); + + void run_sdl(); + + SDL_Camera* m_sdl_camera = nullptr; + SDL_CameraID m_sdl_camera_id = 0; + QImage m_sdl_image; + std::mutex m_sdl_image_mutex; + std::unique_ptr>> m_sdl_thread; + std::unique_ptr m_video_frame_input; +#endif + std::unique_ptr ui; std::unique_ptr m_camera; std::unique_ptr m_media_capture_session; diff --git a/rpcs3/rpcs3qt/emu_settings.cpp b/rpcs3/rpcs3qt/emu_settings.cpp index 935bfd2cc8..20bb58ba93 100644 --- a/rpcs3/rpcs3qt/emu_settings.cpp +++ b/rpcs3/rpcs3qt/emu_settings.cpp @@ -1119,6 +1119,9 @@ QString emu_settings::GetLocalizedSetting(const QString& original, emu_settings_ case camera_handler::null: return tr("Null", "Camera handler"); case camera_handler::fake: return tr("Fake", "Camera handler"); case camera_handler::qt: return tr("Qt", "Camera handler"); +#ifdef HAVE_SDL3 + case camera_handler::sdl: return tr("SDL", "Camera handler"); +#endif } break; case emu_settings_type::MusicHandler: diff --git a/rpcs3/rpcs3qt/gui_application.cpp b/rpcs3/rpcs3qt/gui_application.cpp index 443dcb4e0c..113f5a6ebb 100644 --- a/rpcs3/rpcs3qt/gui_application.cpp +++ b/rpcs3/rpcs3qt/gui_application.cpp @@ -20,6 +20,10 @@ #include "_discord_utils.h" #endif +#ifdef HAVE_SDL3 +#include "Input/sdl_camera_handler.h" +#endif + #include "Emu/Audio/audio_utils.h" #include "Emu/Cell/Modules/cellSysutil.h" #include "Emu/Io/Null/null_camera_handler.h" @@ -696,6 +700,12 @@ void gui_application::InitializeCallbacks() { return std::make_shared(); } +#ifdef HAVE_SDL3 + case camera_handler::sdl: + { + return std::make_shared(); + } +#endif } return nullptr; };