diff --git a/AGENTS.md b/AGENTS.md index 5168058ea3..71413fd163 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,6 +177,11 @@ small examples to copy from). A module is a `const HLEFunction []` table o build-tested here, so double check them by hand against how an existing neighboring file (e.g. `sceVaudio.cpp`) is listed in each. Note: New files in the unittest project have to be updated in the unittest part in android/jni/Android.mk. +## Adding HLE functions + +Always add new functions at the *end* of an array of const HLEFunction. We store the function index in savestates +(in resolved syscall opcodes), so inserting a new function in the middle of an existing array will break things. + ## WebSocket debugger PPSSPP has a JSON/WebSocket debugger and automation API (connect, read/write memory, search memory for values or byte diff --git a/CMakeLists.txt b/CMakeLists.txt index b91acbe094..b9dfd4f445 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -976,7 +976,6 @@ elseif(IOS AND NOT LIBRETRO) ios/iCade/iCadeReaderView.m ios/main.mm ios/CameraHelper.mm - ios/AudioEngine.mm ios/LocationHelper.mm ios/Controls.mm Core/Util/DarwinFileSystemServices.mm diff --git a/Common/Net/Resolve.cpp b/Common/Net/Resolve.cpp index 445df830fe..a395dc2fa5 100644 --- a/Common/Net/Resolve.cpp +++ b/Common/Net/Resolve.cpp @@ -467,9 +467,10 @@ struct DNSHeader { // Function to convert a domain name to DNS query format // http://www.tcpipguide.com/free/t_DNSNameNotationandMessageCompressionTechnique.htm -static void encode_domain_name(const char *domain, unsigned char *encoded) { +static bool encode_domain_name(const char *domain, unsigned char *encoded, size_t max_len) { const char *pos = domain; unsigned char *ptr = encoded; + const unsigned char *end = encoded + max_len; while (*pos) { const char *start = pos; @@ -477,21 +478,36 @@ static void encode_domain_name(const char *domain, unsigned char *encoded) { pos++; } - *ptr++ = (unsigned char)(pos - start); // length field - memcpy(ptr, start, pos - start); - ptr += pos - start; + size_t label_len = pos - start; + if (label_len > 63 || ptr + label_len + 1 >= end) { + return false; // Label too long or buffer overflow + } + + *ptr++ = (unsigned char)label_len; // length field + memcpy(ptr, start, label_len); + ptr += label_len; if (*pos == '.') { pos++; } } + if (ptr >= end) { + return false; + } *ptr = 0; // End of domain name + return true; } // Function to parse and print the DNS response static bool parse_dns_response(unsigned char *buffer, size_t response_len, uint32_t *output) { + if (response_len < sizeof(DNSHeader)) { + ERROR_LOG(Log::sceNet, "DNS response too short"); + return false; + } + DNSHeader *dns = (DNSHeader *)buffer; unsigned char *ptr = buffer + sizeof(struct DNSHeader); + unsigned char *end = buffer + response_len; DEBUG_LOG(Log::Net, "DNS Response:"); DEBUG_LOG(Log::Net, "ID: 0x%x", ntohs(dns->id)); @@ -504,10 +520,19 @@ static bool parse_dns_response(unsigned char *buffer, size_t response_len, uint3 // Skip over the question section const int q_count = ntohs(dns->q_count); for (int i = 0; i < q_count; i++) { - while (*ptr != 0) { - ptr += (*ptr) + 1; + while (ptr < end && *ptr != 0) { + int jump = *ptr; + ptr += jump + 1; + if (ptr >= end) { + ERROR_LOG(Log::sceNet, "DNS response malformed (question section)"); + return false; + } } ptr += 5; // Null byte + QTYPE (2 bytes) + QCLASS (2 bytes) + if (ptr > end) { + ERROR_LOG(Log::sceNet, "DNS response malformed (question section end)"); + return false; + } } *output = 0; @@ -518,13 +543,34 @@ static bool parse_dns_response(unsigned char *buffer, size_t response_len, uint3 DEBUG_LOG(Log::Net, "Answer %d:\n", i + 1); // Skip the name (can be a pointer or a sequence) + if (ptr >= end) { + ERROR_LOG(Log::sceNet, "DNS response malformed (answer %d name)", i); + return false; + } + if ((*ptr & 0xC0) == 0xC0) { + if (ptr + 2 > end) { + ERROR_LOG(Log::sceNet, "DNS response malformed (answer %d name pointer)", i); + return false; + } ptr += 2; // Pointer (2 bytes) } else { - while (*ptr != 0) ptr += (*ptr) + 1; + while (ptr < end && *ptr != 0) { + int jump = *ptr; + ptr += jump + 1; + if (ptr >= end) { + ERROR_LOG(Log::sceNet, "DNS response malformed (answer %d name loop)", i); + return false; + } + } ptr++; } + if (ptr + 10 > end) { + ERROR_LOG(Log::sceNet, "DNS response too short for answer %d header", i); + return false; + } + // TODO: Use a struct or something. uint16_t type = ntohs(*((uint16_t *)ptr)); ptr += 2; @@ -540,6 +586,11 @@ static bool parse_dns_response(unsigned char *buffer, size_t response_len, uint3 DEBUG_LOG(Log::Net, " TTL: %u", ttl); DEBUG_LOG(Log::Net, " Data length: %d", (int)data_len); + if (ptr + data_len > end) { + ERROR_LOG(Log::sceNet, "DNS response data exceeds buffer"); + return false; + } + if (type == DNS_QUERY_TYPE_A && data_len == 4) { // IPv4 address char ip[INET_ADDRSTRLEN]; @@ -586,11 +637,28 @@ bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0); // Create UDP socket - if (sockfd < 0) { + if (sockfd == INVALID_SOCKET) { ERROR_LOG(Log::Net, "Socket creation for direct DNS failed"); return false; } +#ifndef _WIN32 + // On non-Windows, we can't use select() if fd >= FD_SETSIZE + // For DNS, just set a socket timeout instead + struct timeval timeout; + timeout.tv_sec = 5; + timeout.tv_usec = 0; + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { + WARN_LOG(Log::sceNet, "Failed to set socket timeout for DNS query"); + } +#else + // Windows version + DWORD timeout = 5000; // 5 seconds + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)) < 0) { + WARN_LOG(Log::sceNet, "Failed to set socket timeout for DNS query"); + } +#endif + struct sockaddr_in server_addr{}; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(DNS_PORT); @@ -609,14 +677,19 @@ bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t dns->q_count = htons(1); // One question unsigned char *qname = buffer + sizeof(DNSHeader); - encode_domain_name(domain, qname); + size_t qname_space = sizeof(buffer) - sizeof(DNSHeader) - 4; // Reserve 4 bytes for qtype and qclass + if (!encode_domain_name(domain, qname, qname_space)) { + ERROR_LOG(Log::sceNet, "Domain name too long or invalid: %s", domain); + closesocket(sockfd); + return false; + } unsigned char *qinfo = qname + strlen((const char *)qname) + 1; *((uint16_t *)qinfo) = htons(DNS_QUERY_TYPE_A); // Query type: A *((uint16_t *)(qinfo + 2)) = htons(DNS_QUERY_CLASS_IN); // Query class: IN // Send DNS query - size_t query_len = sizeof(DNSHeader) + (qinfo - buffer) + 4; + size_t query_len = (qinfo + 4) - buffer; if (sendto(sockfd, (const char *)buffer, (int)query_len, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { ERROR_LOG(Log::Net, "Failed to send DNS query"); closesocket(sockfd); @@ -625,9 +698,9 @@ bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t // Receive DNS response socklen_t server_len = sizeof(server_addr); - size_t response_len; - if ((response_len = recvfrom(sockfd, (char *)buffer, sizeof(buffer), 0, (struct sockaddr *)&server_addr, &server_len)) < 0) { - ERROR_LOG(Log::Net, "Failed to receive DNS response"); + int response_len = recvfrom(sockfd, (char *)buffer, sizeof(buffer), 0, (struct sockaddr *)&server_addr, &server_len); + if (response_len < 0) { + ERROR_LOG(Log::sceNet, "Failed to receive DNS response (timeout or error)"); closesocket(sockfd); return false; } @@ -636,7 +709,7 @@ bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t closesocket(sockfd); // Done communicating, time to parse. - if (!parse_dns_response(buffer, response_len, ipv4_addr)) { + if (!parse_dns_response(buffer, (size_t)response_len, ipv4_addr)) { return false; } diff --git a/Common/UI/Root.cpp b/Common/UI/Root.cpp index dfb775eb95..564381e87c 100644 --- a/Common/UI/Root.cpp +++ b/Common/UI/Root.cpp @@ -124,6 +124,8 @@ void EnableFocusMovement(bool enable) { focusMoves.clear(); heldKeys.clear(); focusedView = nullptr; + } else { + enable = enable; } } @@ -361,7 +363,7 @@ restart: } } -DialogResult UpdateViewHierarchy(ViewGroup *root) { +DialogResult UpdateViewHierarchy(ViewGroup *root, bool canEnableFocusMovement) { ProcessHeldKeys(root); frameCount++; @@ -370,7 +372,7 @@ DialogResult UpdateViewHierarchy(ViewGroup *root) { return DR_NONE; } - if (focusMoves.size()) { + if (focusMoves.size() && canEnableFocusMovement) { EnableFocusMovement(true); if (!GetFocusedView()) { // Find a view to focus. @@ -397,8 +399,8 @@ DialogResult UpdateViewHierarchy(ViewGroup *root) { } } } - focusMoves.clear(); } + focusMoves.clear(); root->Update(); return DispatchEvents(); diff --git a/Common/UI/Root.h b/Common/UI/Root.h index 861941f6b7..e5d341503a 100644 --- a/Common/UI/Root.h +++ b/Common/UI/Root.h @@ -26,7 +26,7 @@ DialogResult DispatchEvents(); class ViewGroup; void LayoutViewHierarchy(const UIContext &dc, const UI::Margins &rootMargins, UI::ViewGroup *root, ViewLayoutMode layoutMode, bool immersiveMode); -DialogResult UpdateViewHierarchy(ViewGroup *root); +DialogResult UpdateViewHierarchy(ViewGroup *root, bool canEnableFocusMovement = true); enum class KeyEventResult { IGNORE_KEY, // Don't let it be processed. diff --git a/Common/UI/Screen.cpp b/Common/UI/Screen.cpp index 3b0ca9ab43..c447483e4b 100644 --- a/Common/UI/Screen.cpp +++ b/Common/UI/Screen.cpp @@ -532,3 +532,15 @@ void ScreenManager::SetBackgroundOverlayScreens(Screen *backgroundScreen, Screen overlayScreen_ = overlayScreen; overlayScreen_->setScreenManager(this); } + +const char *DialogResultToString(DialogResult result) { + switch (result) { + case DR_NONE: return "DR_NONE"; + case DR_OK: return "DR_OK"; + case DR_CANCEL: return "DR_CANCEL"; + case DR_YES: return "DR_YES"; + case DR_NO: return "DR_NO"; + case DR_BACK: return "DR_BACK"; + default: return "(N/A)"; + } +} diff --git a/Common/UI/Screen.h b/Common/UI/Screen.h index e726037f14..347f8ae456 100644 --- a/Common/UI/Screen.h +++ b/Common/UI/Screen.h @@ -36,6 +36,7 @@ enum DialogResult { DR_NO, DR_BACK, }; +const char *DialogResultToString(DialogResult result); class ScreenManager; class UIContext; diff --git a/Common/UI/UIScreen.cpp b/Common/UI/UIScreen.cpp index ed723aba67..97d3b26d7c 100644 --- a/Common/UI/UIScreen.cpp +++ b/Common/UI/UIScreen.cpp @@ -104,7 +104,7 @@ void UIScreen::update() { DoRecreateViews(); if (root_) { - DialogResult result = UpdateViewHierarchy(root_); + DialogResult result = UpdateViewHierarchy(root_, AllowFocusMovement()); if (result != DR_NONE) { TriggerFinish(result); } diff --git a/Common/UI/UIScreen.h b/Common/UI/UIScreen.h index 7202455124..149b05068c 100644 --- a/Common/UI/UIScreen.h +++ b/Common/UI/UIScreen.h @@ -50,6 +50,7 @@ public: protected: virtual void CreateViews() = 0; + virtual bool AllowFocusMovement() const { return true; } Bounds GetLayoutBounds(UIContext &dc) const; diff --git a/Core/HLE/sceKernel.cpp b/Core/HLE/sceKernel.cpp index 85df5f83ef..e36c32b415 100644 --- a/Core/HLE/sceKernel.cpp +++ b/Core/HLE/sceKernel.cpp @@ -968,15 +968,92 @@ void Register_LoadExecForUser() RegisterHLEModule("LoadExecForUser", ARRAY_SIZE(LoadExecForUser), LoadExecForUser); } -const HLEFunction LoadExecForKernel[] = -{ +// sceKernelExitVSHVSH and sceKernelExitVSHKernel (two NIDs each, across firmware versions) are +// the VSH's own way of tearing itself down - when called from a regular game/homebrew context +// (which can happen, since these live in a kernel-mode module games can still reach) they have +// the same observable effect as sceKernelExitGame. See SceKernelLoadExecVSHParam in sceKernel.h. +static int sceKernelExitVSH(u32 paramPtr) { + if (Memory::IsValidRange(paramPtr, sizeof(SceKernelLoadExecVSHParam))) { + auto param = PSPPointer::Create(paramPtr); + INFO_LOG(Log::sceKernel, "sceKernelExitVSH: size=%d, args=%08x, argp=%08x, flags=%08x", param->size, param->args, param->argp, param->flags); + } + + INFO_LOG(Log::sceKernel, "sceKernelExitVSH"); + __KernelSwitchOffThread("VSH exited"); + Core_Stop(); + + g_OSD.Show(OSDType::MESSAGE_INFO, "sceKernelExitVSH()", 0.0f, "kernelexit"); + return hleNoLog(0); +} + +// The VSH's own version of sceKernelLoadExec, used for pushing a game to run over USB/WLAN +// (e.g. from a PC) rather than loading it from a file already on the memory stick/UMD. +static int sceKernelLoadExecBufferVSHUsbWlan(int bufferSize, u32 bufferAddr, u32 paramPtr) { + if (!Memory::IsValidRange(bufferAddr, bufferSize)) { + return hleLogError(Log::sceKernel, SCE_KERNEL_ERROR_ILLEGAL_ADDR, "invalid buffer"); + } + + const u8 *data = Memory::GetPointerUnchecked(bufferAddr); + std::string error_string; + if (!__KernelLoadExecFromBuffer(data, (size_t)bufferSize, paramPtr, &error_string)) { + Core_UpdateState(CORE_RUNTIME_ERROR); + return hleLogError(Log::sceKernel, -1, "failed: %s", error_string.c_str()); + } + if (gpu) { + gpu->Reinitialize(); + } + return hleLogInfo(Log::sceKernel, 0); +} + +const HLEFunction LoadExecForKernel[] = { {0x4AC57943, &WrapI_I, "sceKernelRegisterExitCallback", 'i', "i", HLE_KERNEL_SYSCALL }, - {0XA3D5E142, nullptr, "sceKernelExitVSHVSH", '?', "" }, + {0XA3D5E142, &WrapI_U, "sceKernelExitVSHVSH", 'i', "x", HLE_KERNEL_SYSCALL }, {0X28D0D249, &WrapI_CU, "sceKernelLoadExecVSHMs2", 'i', "sx" }, - {0x6D302D3D, &WrapV_V, "sceKernelExitVSHKernel", 'v', "x", HLE_KERNEL_SYSCALL },// when called in game mode it will have the same effect that sceKernelExitGame + {0x6D302D3D, &WrapI_U, "sceKernelExitVSHKernel", 'i', "x", HLE_KERNEL_SYSCALL },// when called in game mode it will have the same effect that sceKernelExitGame {0x05572A5F, &WrapV_V, "sceKernelExitGame", 'v', "", HLE_KERNEL_SYSCALL }, + {0X08F7166C, &WrapI_U, "sceKernelExitVSHVSH", 'i', "x", HLE_KERNEL_SYSCALL }, + {0XD940C83C, &WrapI_CU, "sceKernelLoadExecVSHMs2", 'i', "sx" }, + {0XF9CFCF2F, &WrapI_CU, "sceKernelLoadExec_F9CFCF2F", 'i', "sx" }, + {0XD8320A28, &WrapI_CU, "sceKernelLoadExecVSHDisc", 'i', "sx" }, + {0XC3474C2A, &WrapI_U, "sceKernelExitVSHKernel", 'i', "x", HLE_KERNEL_SYSCALL }, + {0XBEF585EC, &WrapI_IUU, "sceKernelLoadExecBufferVSHUsbWlan", 'i', "ixx" }, + // Everything below here is only known by NID - even JPCSP, which is further along in VSH + // support, only knows them by name/NID (or not even that) and stubs them all out. + {0X11412288, nullptr, "sceKernelLoadExec_11412288", '?', "" }, + {0XA5ECA6E3, nullptr, "sceKernelLoadExec_11412288", '?', "" }, + {0X00745486, nullptr, "sceKernelLoadExecVSHMs4", '?', "" }, + {0X4FB44D27, nullptr, "sceKernelLoadExecVSHMs1", '?', "" }, + {0XCC6A47D2, nullptr, "sceKernelLoadExecVSHMs3", '?', "" }, + {0X7CABED9B, nullptr, "sceKernelLoadExecVSHMs5", '?', "" }, + {0X1B305B09, nullptr, "sceKernelLoadExecVSHDiscDebug", '?', "" }, + {0XD4B49C4B, nullptr, "sceKernelLoadExecVSHDiscUpdater", '?', "" }, + {0X2B8813AF, nullptr, "sceKernelLoadExecBufferVSHUsbWlanDebug", '?', "" }, + {0X1F08547A, nullptr, "sceKernelInvokeExitCallback", '?', "" }, + {0X1F88A490, nullptr, "sceKernelRegisterExitCallback", '?', "" }, + {0X24114598, nullptr, "sceKernelUnregisterExitCallback", '?', "" }, + {0XB57D0DEC, nullptr, "sceKernelCheckExitCallback", '?', "" }, + {0X032A7938, nullptr, "LoadExecForKernel_032A7938", '?', "" }, + {0X077BA314, nullptr, "LoadExecForKernel_077BA314", '?', "" }, + {0X16A68007, nullptr, "LoadExecForKernel_16A68007", '?', "" }, + {0X1B8AB02E, nullptr, "LoadExecForKernel_1B8AB02E", '?', "" }, + {0X40564748, nullptr, "LoadExecForKernel_40564748", '?', "" }, + {0X47A5A49C, nullptr, "LoadExecForKernel_47A5A49C", '?', "" }, + {0X7CAFE77F, nullptr, "LoadExecForKernel_7CAFE77F", '?', "" }, + {0X87C3589C, nullptr, "LoadExecForKernel_87C3589C", '?', "" }, + {0X8C4679D3, nullptr, "LoadExecForKernel_8C4679D3", '?', "" }, + {0X9BD32619, nullptr, "LoadExecForKernel_9BD32619", '?', "" }, + {0XA6658F10, nullptr, "LoadExecForKernel_A6658F10", '?', "" }, + {0XB343FDAB, nullptr, "LoadExecForKernel_B343FDAB", '?', "" }, + {0XBC26BEEF, nullptr, "LoadExecForKernel_BC26BEEF", '?', "" }, + {0XC11E6DF1, nullptr, "LoadExecForKernel_C11E6DF1", '?', "" }, + {0XC540E3B3, nullptr, "LoadExecForKernel_C540E3B3", '?', "" }, + {0XC7C83B1E, nullptr, "LoadExecForKernel_C7C83B1E", '?', "" }, + {0XDBD0CF1B, nullptr, "LoadExecForKernel_DBD0CF1B", '?', "" }, + {0XE1972A24, nullptr, "LoadExecForKernel_E1972A24", '?', "" }, + {0XE704ECC3, nullptr, "LoadExecForKernel_E704ECC3", '?', "" }, + {0XAE9EFC0D, nullptr, "LoadExecForKernel_AE9EFC0D", '?', "" }, }; - + void Register_LoadExecForKernel() { RegisterHLEModule("LoadExecForKernel", ARRAY_SIZE(LoadExecForKernel), LoadExecForKernel); diff --git a/Core/HLE/sceKernel.h b/Core/HLE/sceKernel.h index b1d79f9476..c1a16c3ecb 100644 --- a/Core/HLE/sceKernel.h +++ b/Core/HLE/sceKernel.h @@ -82,6 +82,28 @@ struct SceKernelLoadExecParam u32_le keyp; // Encryption key? Not yet used }; +// Used by the VSH's own LoadExec variants (sceKernelLoadExecVSHMs2 and friends, see +// LoadExecForKernel in sceKernel.cpp) instead of SceKernelLoadExecParam above. The first four +// fields share the same layout as SceKernelLoadExecParam, which is why the plain +// sceKernelLoadExec happens to work fine even when handed one of these. Fields past "flags" +// are only present if size >= 0x30 - see JPCSP's SceKernelLoadExecVSHParam for the reference +// this was ported from. +struct SceKernelLoadExecVSHParam +{ + SceSize_le size; // Size of the structure + SceSize_le args; // Size of the arg string + u32_le argp; // Pointer to the arg string + u32_le keyp; // Pointer to the encryption key string + SceSize_le vshmainArgsSize; // Size of the vshmain-specific arg buffer + u32_le vshmainArgs; // Pointer to the vshmain-specific arg buffer + u32_le configFilep; // Pointer to a config file path string + u32_le unknownString; + u32_le flags; + u32_le extArgs; + u32_le extArgp; + u32_le opt11; +}; + void __KernelInit(); void __KernelShutdown(); void __KernelDoState(PointerWrap &p); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 4ef998c915..4ad422b684 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -1776,7 +1776,10 @@ void __KernelLoadReset() { __KernelInit(); } -bool __KernelLoadExec(const char *filename, u32 paramPtr, std::string *error_string) { +// Shared by __KernelLoadExec (loading from a file) and __KernelLoadExecFromBuffer (loading +// from a buffer already in RAM, as used by the VSH's USB/WLAN game-push feature) - everything +// past the point where we have the executable's bytes in hand. +bool __KernelLoadExecFromPtr(const u8 *data, size_t size, const char *filename, u32 paramPtr, std::string *error_string) { SceKernelLoadExecParam param{}; auto paramData = PSPPointer::Create(paramPtr); @@ -1801,18 +1804,7 @@ bool __KernelLoadExec(const char *filename, u32 paramPtr, std::string *error_str __KernelLoadReset(); - std::vector fileData; - if (pspFileSystem.ReadEntireFile(filename, fileData) < 0) { - ERROR_LOG(Log::Loader, "Failed to load executable %s - file doesn't exist", filename); - *error_string = StringFromFormat("Could not find executable %s", filename); - delete[] param_argp; - delete[] param_key; - __KernelShutdown(); - return false; - } - - size_t size = fileData.size(); - PSPModule *module = __KernelLoadModule(fileData.data(), size, 0, filename, error_string); + PSPModule *module = __KernelLoadModule((u8 *)data, size, 0, filename, error_string); if (!module || module->isFake) { if (module) { @@ -1872,6 +1864,22 @@ bool __KernelLoadExec(const char *filename, u32 paramPtr, std::string *error_str return true; } +bool __KernelLoadExec(const char *filename, u32 paramPtr, std::string *error_string) { + std::vector fileData; + if (pspFileSystem.ReadEntireFile(filename, fileData) < 0) { + ERROR_LOG(Log::Loader, "Failed to load executable %s - file doesn't exist", filename); + *error_string = StringFromFormat("Could not find executable %s", filename); + __KernelShutdown(); + return false; + } + + return __KernelLoadExecFromPtr(fileData.data(), fileData.size(), filename, paramPtr, error_string); +} + +bool __KernelLoadExecFromBuffer(const u8 *data, size_t size, u32 paramPtr, std::string *error_string) { + return __KernelLoadExecFromPtr(data, size, "vshbuffer", paramPtr, error_string); +} + bool __KernelLoadGEDump(std::string_view base_filename, std::string *error_string) { __KernelLoadReset(); @@ -2538,8 +2546,8 @@ static u32 sceKernelLoadModuleDNAS(const char *name, u32 flags) return hleNoLog(0); } -// Pretty sure this is a badly brute-forced function name... -static SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr) +// Suspecting this is a badly brute-forced function name... Although, it's not entirely implausible. Buffer makes sense. +SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr) { if (flags != 0) { WARN_LOG_REPORT(Log::Loader, "sceKernelLoadModuleBufferUsbWlan: unsupported flags: %08x", flags); diff --git a/Core/HLE/sceKernelModule.h b/Core/HLE/sceKernelModule.h index 2a2d39dd20..0cdcf97f31 100644 --- a/Core/HLE/sceKernelModule.h +++ b/Core/HLE/sceKernelModule.h @@ -235,6 +235,10 @@ u32 __KernelGetModuleGP(SceUID module); bool KernelModuleIsKernelMode(SceUID module); bool __KernelLoadGEDump(std::string_view base_filename, std::string *error_string); bool __KernelLoadExec(const char *filename, u32 paramPtr, std::string *error_string); +// Exposed so sceVshBridge can reuse it directly for vshKernelLoadModuleBufferVSH, matching JPCSP. +SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr); +bool __KernelLoadExecFromBuffer(const u8 *data, size_t size, u32 paramPtr, std::string *error_string); +// Exposed for HLE.cpp's "Unknown syscall" diagnostic - see the definition for details. bool KernelFindImportByStubAddr(u32 stubAddr, std::string *importModuleName, u32 *nid, std::string *importingModuleName); // Describes which loaded module (and section within it) an address falls in, e.g. "EBOOT.BIN.text+1234". // Returns an empty string if the address isn't inside any currently loaded module. diff --git a/UI/DiscordIntegration.cpp b/UI/DiscordIntegration.cpp index 8318e7a723..cc10a37212 100644 --- a/UI/DiscordIntegration.cpp +++ b/UI/DiscordIntegration.cpp @@ -146,6 +146,7 @@ void Discord::SetPresenceMenu() { discordPresence.startTimestamp = time(0); discordPresence.largeImageText = "PPSSPP is the best PlayStation Portable emulator around!"; discordPresence.largeImageKey = System_GetPropertyBool(SYSPROP_APP_GOLD) ? "icon_gold_png" : "icon_regular_png"; + Discord_UpdatePresence(&discordPresence); #endif } diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index e1639f4d1f..004a858d3d 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1738,6 +1738,14 @@ ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { return screenRenderFlags; } +bool EmuScreen::AllowFocusMovement() const { + if (chatMenu_ && chatMenu_->GetVisibility() == UI::V_VISIBLE) { + return true; + } + // We have just a game controller and possibly a chat and debug button. Don't allow focus movement. + return false; +} + ScreenRenderFlags EmuScreen::RunEmulation(bool skipBufferEffects) { using namespace Draw; ScreenRenderFlags flags = ScreenRenderFlags::NONE; diff --git a/UI/EmuScreen.h b/UI/EmuScreen.h index 68fb0b13eb..8ca0a385ac 100644 --- a/UI/EmuScreen.h +++ b/UI/EmuScreen.h @@ -77,6 +77,8 @@ protected: void SetPSPAnalog(int rotation, int stick, float x, float y) override; ViewLayoutMode LayoutMode() const override; + bool AllowFocusMovement() const override; + private: void CreateViews() override; ScreenRenderFlags RunEmulation(bool skipBufferEffects); diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 1787301c2e..e0b978f4c2 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -1165,6 +1165,14 @@ void GameSettingsScreen::CreateSystemSettings(UI::ViewGroup *systemSettings) { switchMode->OnChoice.Add([](EventParams &e) { System_Notify(SystemNotification::APP_SWITCH_MODE_CHANGED); }); + + { + // Note: On iPhone, iOS hides the status bar in landscape no matter what this is set to. + DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation()); + systemSettings->Add(new CheckBox(&config.bImmersiveMode, sy->T("Hide status bar")))->OnClick.Add([](EventParams &e) { + System_Notify(SystemNotification::IMMERSIVE_MODE_CHANGE); + }); + } #endif #if PPSSPP_PLATFORM(ANDROID) diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index 0310e3f941..32b2502e8a 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -389,9 +389,7 @@ GamePauseScreen::~GamePauseScreen() { void GamePauseScreen::OnVKey(VirtKey virtualKeyCode, bool down) { // Simple de-bounce using createdTime_, just to be safe. if (down && virtualKeyCode == VIRTKEY_PAUSE && time_now_d() > createdTime_ + 0.1) { - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrame_ = true; - finishNextFrameResult_ = DR_BACK; + FinishNextFrame(DR_BACK); } } @@ -410,9 +408,7 @@ void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems, int slotNum = v->GetSlot(); auto doLoad = [this, slotNum]() { SaveState::LoadSlot(saveStatePrefix_, slotNum, &ShowMessageAfterSaveStateAction); - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrame_ = true; - finishNextFrameResult_ = DR_CANCEL; + FinishNextFrame(DR_CANCEL); }; if (g_Config.bConfirmLoadState) { screenManager()->push(new LoadStateConfirmScreen(saveStatePrefix_, slotNum, [doLoad](bool result) { @@ -783,16 +779,12 @@ void GamePauseScreen::ShowContextMenu(UI::View *menuButton, bool portrait) { screenManager()->push(new UI::MessagePopupScreen(di->T("Reset"), confirmMessage, di->T("Reset"), di->T("Cancel"), [this](bool result) { if (result) { System_PostUIMessage(UIMessage::REQUEST_GAME_RESET); - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrame_ = true; - finishNextFrameResult_ = DR_BACK; // resume + FinishNextFrame(DR_BACK); // resume } })); } else { System_PostUIMessage(UIMessage::REQUEST_GAME_RESET); - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrameResult_ = DR_BACK; // resume - finishNextFrame_ = true; + FinishNextFrame(DR_BACK); // resume } }); auto dev = GetI18NCategory(I18NCat::DEVELOPER); @@ -827,9 +819,7 @@ void GamePauseScreen::dialogFinished(const Screen *dialog, DialogResult dr) { std::string tag = dialog->tag(); if (tag == "ScreenshotView") { if (dr == DR_OK) { - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrame_ = true; - finishNextFrameResult_ = DR_BACK; + FinishNextFrame(DR_BACK); } else if (dr != DR_CANCEL && dr != DR_BACK) { // Just go back to the pause menu, but refresh the savestate thumbnails in case something changed. SaveState::Rescan(saveStatePrefix_); @@ -904,9 +894,7 @@ void GamePauseScreen::OnExit(UI::EventParams &e) { if (g_Config.bPauseMenuExitsEmulator) { System_ExitApp(); } else { - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrameResult_ = DR_OK; // exit game - finishNextFrame_ = true; + FinishNextFrame(DR_OK); // exit game } } })); @@ -962,3 +950,14 @@ void GamePauseScreen::OnDeleteConfig(UI::EventParams &e) { } })); } + +// This is a bit of a hack that we should try to remove. +void GamePauseScreen::FinishNextFrame(DialogResult finishNextFrameResult) { + std::lock_guard lock(finishNextFrameMutex_); + if (!finishNextFrame_) { + finishNextFrameResult_ = finishNextFrameResult; + finishNextFrame_ = true; + } else { + WARN_LOG(Log::UI, "Duplicate call to FinishNextFrame - we were already finishing with result %d, now trying to finish with result %d", finishNextFrameResult_, finishNextFrameResult); + } +} diff --git a/UI/PauseScreen.h b/UI/PauseScreen.h index 7852b5ed01..2053ebd12b 100644 --- a/UI/PauseScreen.h +++ b/UI/PauseScreen.h @@ -66,6 +66,8 @@ private: void AddExtraOptions(UI::ViewGroup *parent); + void FinishNextFrame(DialogResult finishNextFrameResult); + // hack std::mutex finishNextFrameMutex_; bool finishNextFrame_ = false; diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index 109d0d8a35..2f7d45b472 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -1424,6 +1424,7 @@ Default tab = Default tab Dynarec/JIT (recommended) = Dynarec/JIT (recommended) Final processed image = Final processed image Hide navigation bar = Hide navigation bar +Hide status bar = Hide status bar I/O timing method = I/O timing method JIT using IR = JIT using IR Loaded plugin: %1 = Loaded plugin: %1 diff --git a/ext/discord-rpc-build/CMakeLists.txt b/ext/discord-rpc-build/CMakeLists.txt index 82252d745a..079c06496b 100644 --- a/ext/discord-rpc-build/CMakeLists.txt +++ b/ext/discord-rpc-build/CMakeLists.txt @@ -16,15 +16,12 @@ set(ALL_SOURCE_FILES ${SRC_DIR}/serialization.cpp ) -if(APPLE) - set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} - ${SRC_DIR}/discord_register_osx.m - ) -endif() - if(WIN32) set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/connection_win.cpp) set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/discord_register_win.cpp) +elseif(APPLE) + set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/connection_unix.cpp) + set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/discord_register_osx.m) else() set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/connection_unix.cpp) set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/discord_register_linux.cpp) @@ -33,3 +30,11 @@ endif() add_library(discord-rpc STATIC ${ALL_SOURCE_FILES}) target_include_directories(discord-rpc PUBLIC ../discord-rpc/src ../discord-rpc/include ../rapidjson/include) + +if(UNIX) + target_link_libraries(discord-rpc PUBLIC pthread) +endif() + +if(APPLE) + target_link_libraries(discord-rpc PRIVATE "-framework AppKit") +endif() diff --git a/ios/AppDelegate.h b/ios/AppDelegate.h index 4a08b20a37..651d2fe431 100644 --- a/ios/AppDelegate.h +++ b/ios/AppDelegate.h @@ -4,15 +4,11 @@ #include -@protocol PPSSPPViewController; - @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; -@property (strong, nonatomic) UIScreen *screen; @property (nonatomic, strong) NSDictionary *launchOptions; -- (BOOL)launchPPSSPP:(int)argc argv:(char**)argv; - (void)processFilePath:(NSString *)path; // Exports the game library to a caller app via a URL scheme callback. diff --git a/ios/AppDelegate.mm b/ios/AppDelegate.mm index beab1370e9..6af4f6e2af 100644 --- a/ios/AppDelegate.mm +++ b/ios/AppDelegate.mm @@ -248,8 +248,6 @@ static std::vector GatherGameLibrary() { default: return UIInterfaceOrientationMaskAll; } - - return UIInterfaceOrientationMaskAll; // or at least include Portrait } @end diff --git a/ios/AudioEngine.h b/ios/AudioEngine.h deleted file mode 100644 index 6613513b27..0000000000 --- a/ios/AudioEngine.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// AudioEngine.h -// PPSSPP -// -// Created by rock88 on 15/03/2013. -// Copyright (c) 2013 Homebrew. All rights reserved. -// - -#import - -@interface AudioEngine : NSObject - -@end diff --git a/ios/AudioEngine.mm b/ios/AudioEngine.mm deleted file mode 100644 index 5efd7da3e0..0000000000 --- a/ios/AudioEngine.mm +++ /dev/null @@ -1,149 +0,0 @@ -// -// AudioEngine.mm -// PPSSPP -// -// Created by rock88 on 15/03/2013. -// Copyright (c) 2013 Homebrew. All rights reserved. -// - -#import "AudioEngine.h" -#import -#import -#import -#import - -#import - -static volatile BOOL done = 0; - -#define SAMPLE_SIZE 44100 -static short stream[SAMPLE_SIZE]; - -void NativeMix(short *audio, int numSamples, int sampleRateHz, void *userdata); - -@interface AudioEngine () - -@property (nonatomic,assign) ALCdevice *alcDevice; -@property (nonatomic,assign) ALCcontext *alContext; -@property (nonatomic,assign) ALuint buffer; -@property (nonatomic,assign) ALuint source; - -@end - -@implementation AudioEngine -@synthesize alcDevice,alContext,buffer,source; - -- (id)init -{ - self = [super init]; - if (self) - { - [self audioInit]; - [self audioLoop]; - } - return self; -} - -- (void)dealloc -{ - [self audioShutdown]; - [super dealloc]; -} - -- (void)checkALError -{ - ALenum ErrCode; - std::string Err = "OpenAL error: "; - if ((ErrCode = alGetError()) != AL_NO_ERROR) - { - Err += (char *)alGetString(ErrCode); - printf("%s\n",Err.c_str()); - } -} - -- (void)audioInit -{ - done = 0; - alcDevice = alcOpenDevice(NULL); - - if (alcDevice) - { - NSLog(@"OpenAL device opened: %s",alcGetString(alcDevice, ALC_DEVICE_SPECIFIER)); - } - else - { - NSLog(@"WARNING: could not open OpenAL device"); - return; - } - - alContext = alcCreateContext(alcDevice, NULL); - - if (alContext) - { - alcMakeContextCurrent(alContext); - } - else - { - NSLog(@"ERROR: no OpenAL context"); - return; - } - - alGenSources(1, &source); - alGenBuffers(1, &buffer); -} - -- (void)audioShutdown -{ - done = 1; - alcMakeContextCurrent(NULL); - - if (alContext) - { - alcDestroyContext(alContext); - alContext = NULL; - } - - if (alcDevice) - { - alcCloseDevice(alcDevice); - alcDevice = NULL; - } -} - -- (bool)playing -{ - ALenum state; - alGetSourcei(source, AL_SOURCE_STATE, &state); - return (state == AL_PLAYING); -} - -- (void)audioLoop -{ - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){ - const int sampleRateHz = 44100; - while (!done) { - if (![self playing]) { - NativeMix(stream, SAMPLE_SIZE / 2, sampleRateHz, 0); - frames_ready = SAMPLE_SIZE / 2; - } else { - frames_ready = 0; - } - - if (frames_ready > 0) { - const size_t bytes_ready = frames_ready * sizeof(short) * 2; - alSourcei(source, AL_BUFFER, 0); - alBufferData(buffer, AL_FORMAT_STEREO16, stream, bytes_ready, sampleRateHz); - alSourcei(source, AL_BUFFER, buffer); - alSourcePlay(source); - - // TODO: Maybe this could get behind? - usleep((1000000 * frames_ready) / sampleRateHz); - } else { - usleep(100); - } - pthread_yield_np(); - } - }); -} - -@end diff --git a/ios/CameraHelper.mm b/ios/CameraHelper.mm index ffdb282365..5db050bf9a 100644 --- a/ios/CameraHelper.mm +++ b/ios/CameraHelper.mm @@ -53,6 +53,10 @@ NSString *getSelectedCamera() { case AVAuthorizationStatusAuthorized: { return 0; } + default: { + NSLog(@"unknown camera authorization status %ld, treating as denied", (long)status); + return 1; + } } } @@ -145,6 +149,7 @@ NSString *getSelectedCamera() { CGContextDrawImage(outContext, outRect, inImage); CGImageRelease(inImage); CGImageRef outImage = CGBitmapContextCreateImage(outContext); + CGContextRelease(outContext); CGColorSpaceRelease(colorSpace); CVPixelBufferUnlockBaseAddress(imageBuffer, 0); diff --git a/ios/DisplayManager.mm b/ios/DisplayManager.mm deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ios/IAPManager.mm b/ios/IAPManager.mm index e48a6025e0..db8dc9f919 100644 --- a/ios/IAPManager.mm +++ b/ios/IAPManager.mm @@ -130,19 +130,24 @@ switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: case SKPaymentTransactionStateRestored: - NSLog(transaction.transactionState == SKPaymentTransactionStatePurchased ? @"IAP Purchase" : @"IAP Restore"); + NSLog(@"%@", transaction.transactionState == SKPaymentTransactionStatePurchased ? @"IAP Purchase" : @"IAP Restore"); // Perform the unlock (updaing the variable and switching the icon). [self unlockGold]; [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; - g_requestManager.PostSystemSuccess(_pendingRequestID, "", 0); - _pendingRequestID = 0; + // The queue also delivers transactions we didn't ask for, like unfinished ones at startup. + if (_pendingRequestID != 0) { + g_requestManager.PostSystemSuccess(_pendingRequestID, "", 0); + _pendingRequestID = 0; + } break; case SKPaymentTransactionStateFailed: NSLog(@"[IAPManager] Purchase failed (requestID: %d): %@", _pendingRequestID, transaction.error.localizedDescription); [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; // Optionally post failure callback here - g_requestManager.PostSystemFailure(_pendingRequestID); - _pendingRequestID = 0; + if (_pendingRequestID != 0) { + g_requestManager.PostSystemFailure(_pendingRequestID); + _pendingRequestID = 0; + } break; default: break; diff --git a/ios/LocationHelper.mm b/ios/LocationHelper.mm index 237a9e12fe..b8623d70b7 100644 --- a/ios/LocationHelper.mm +++ b/ios/LocationHelper.mm @@ -7,8 +7,11 @@ -(id) init { NSLog(@"LocationHelper::init"); - locationManager = [[CLLocationManager alloc] init]; - [locationManager setDelegate:self]; + self = [super init]; + if (self) { + locationManager = [[CLLocationManager alloc] init]; + [locationManager setDelegate:self]; + } return self; } diff --git a/ios/PPSSPPUIApplication.mm b/ios/PPSSPPUIApplication.mm index 759adce34d..decb39c617 100644 --- a/ios/PPSSPPUIApplication.mm +++ b/ios/PPSSPPUIApplication.mm @@ -81,7 +81,7 @@ key.keyCode = getSmartKeyboardMap((int)eventScanCode); key.deviceId = DEVICE_ID_KEYBOARD; NativeKey(key); - } else if (GSEVENT_TYPE_KEYDOWN) { + } else if (eventType == GSEVENT_TYPE_KEYDOWN) { struct KeyInput key; key.flags = KeyInputFlags::DOWN; key.keyCode = getSmartKeyboardMap((int)eventScanCode); diff --git a/ios/ViewController.mm b/ios/ViewController.mm index 6383d9c08d..d80334194a 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -5,7 +5,6 @@ // Modified by xSacha // Reworked by hrydgard -#import "AppDelegate.h" #import "ViewController.h" #import "iOSCoreAudio.h" @@ -48,17 +47,11 @@ PPSSPPBaseViewController *sharedViewController; @interface PPSSPPViewControllerGL () { GraphicsContext *graphicsContext; - - int imageRequestId; - NSString *imageFilename; } @property (nonatomic, strong) EAGLContext *glContext; @property (nonatomic, strong) GLKView *glView; @property (nonatomic, strong) CADisplayLink *displayLink; -@property (nonatomic, assign) NSTimeInterval lastTimestamp; - -@property (nonatomic, strong) EAGLContext* context; @end @@ -134,10 +127,6 @@ PPSSPPBaseViewController *sharedViewController; } [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; - self.lastTimestamp = 0; - - UIScreen* screen = [(AppDelegate*)[UIApplication sharedApplication].delegate screen]; - self.view.frame = [screen bounds]; self.view.multipleTouchEnabled = YES; graphicsContext = new OpenGLGraphicsContext(); @@ -162,7 +151,6 @@ PPSSPPBaseViewController *sharedViewController; [self hideKeyboard]; - // Initialize the motion manager for accelerometer control. INFO_LOG(Log::G3D, "Done with viewDidLoad."); } @@ -173,14 +161,12 @@ PPSSPPBaseViewController *sharedViewController; - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - // Resume display link unless explicitly paused - INFO_LOG(Log::G3D, "viewWillAppear - resuming display link"); + INFO_LOG(Log::G3D, "viewWillAppear"); } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; - // stop rendering while not visible - INFO_LOG(Log::G3D, "viewWillDisappear - pausing display link"); + INFO_LOG(Log::G3D, "viewWillDisappear"); } - (void)dealloc { @@ -205,16 +191,6 @@ PPSSPPBaseViewController *sharedViewController; } - (void)displayLinkFired:(CADisplayLink *)dl { - // compute delta time - NSTimeInterval timestamp = dl.timestamp; - NSTimeInterval delta = 0; - if (self.lastTimestamp > 0) { - delta = timestamp - self.lastTimestamp; - } else { - delta = dl.duration; // fallback - } - self.lastTimestamp = timestamp; - // Ensure context is current before drawing [EAGLContext setCurrentContext:self.glContext]; @@ -265,15 +241,6 @@ PPSSPPBaseViewController *sharedViewController; _dbg_assert_(graphicsContext); - if (self.context) { - if ([EAGLContext currentContext] == self.context) { - [EAGLContext setCurrentContext:nil]; - } - self.context = nil; - } - - [[NSNotificationCenter defaultCenter] removeObserver:self]; - graphicsContext->ShutdownSurface(); graphicsContext->ShutdownAPI(); delete graphicsContext; @@ -286,10 +253,6 @@ PPSSPPBaseViewController *sharedViewController; [(GLKView*)self.glView bindDrawable]; } -- (UIView *)getView { - return [self view]; -} - // Can't consolidate this yet. - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { diff --git a/ios/ViewControllerCommon.h b/ios/ViewControllerCommon.h index 5ba55cd34d..c42e735271 100644 --- a/ios/ViewControllerCommon.h +++ b/ios/ViewControllerCommon.h @@ -20,12 +20,12 @@ - (void)shareText:(NSString *)text; - (void)shutdown; - (void)bindDefaultFBO; -- (UIView *)getView; - (void)startLocation; - (void)stopLocation; - (void)startVideo:(int)width height:(int)height; - (void)stopVideo; - (void)appSwitchModeChanged; +- (void)immersiveModeChanged; - (void)setupController:(GCController *)controller; // Forwarded from the AppDelegate diff --git a/ios/ViewControllerCommon.mm b/ios/ViewControllerCommon.mm index 4964db869a..46e96cf891 100644 --- a/ios/ViewControllerCommon.mm +++ b/ios/ViewControllerCommon.mm @@ -76,14 +76,17 @@ static int GetPickerRequestId(id picker) { selector:@selector(onOrientationChanged) name:UIDeviceOrientationDidChangeNotification object:nil]; - } - self.accelerometerQueue = [[NSOperationQueue alloc] init]; - self.accelerometerQueue.name = @"AccelerometerQueue"; - self.accelerometerQueue.maxConcurrentOperationCount = 1; + self.accelerometerQueue = [[NSOperationQueue alloc] init]; + self.accelerometerQueue.name = @"AccelerometerQueue"; + self.accelerometerQueue.maxConcurrentOperationCount = 1; + } return self; } +// Only means anything for the GL backend, which overrides this. +- (void)bindDefaultFBO {} + - (void)shutdown { self.gameController = nil; [[NSNotificationCenter defaultCenter] removeObserver:self]; @@ -139,7 +142,7 @@ static int GetPickerRequestId(id picker) { // This needs to be called really late during startup, unfortunately. #if PPSSPP_PLATFORM(IOS_APP_STORE) [IAPManager sharedIAPManager]; // Kick off the IAPManager early. - NSLog(@"Metal viewDidAppear. updating icon"); + NSLog(@"viewDidAppear. updating icon"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [[IAPManager sharedIAPManager] updateIcon:false]; [self hideKeyboard]; @@ -481,26 +484,24 @@ extern float g_safeInsetBottom; #endif #pragma mark - Status Bar Control -// iOS calls this to determine whether to hide the status bar -- (BOOL)prefersStatusBarHidden { - UIInterfaceOrientation orientation; - +// The immersive mode setting is per-orientation, so we need to know which way we're facing. +// Can't just use g_display for this, since it lags behind during rotation. +// Note: Using viewIfLoaded, since this can get called before the view exists, and we don't want to force it into existence. +- (DeviceOrientation)currentDeviceOrientation { if (@available(iOS 13.0, *)) { - UIWindowScene *scene = self.view.window.windowScene; + UIWindowScene *scene = self.viewIfLoaded.window.windowScene; if (scene != nil) { - orientation = scene.interfaceOrientation; - } else { - orientation = UIApplication.sharedApplication.statusBarOrientation; + return UIInterfaceOrientationIsPortrait(scene.interfaceOrientation) ? DeviceOrientation::Portrait : DeviceOrientation::Landscape; } - } else { - orientation = UIApplication.sharedApplication.statusBarOrientation; } + CGSize size = self.viewIfLoaded.bounds.size; + return size.height > size.width ? DeviceOrientation::Portrait : DeviceOrientation::Landscape; +} - BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation); - - bool userWantsStatusBar = true; // g_Config.bShowStatusBar; - // return isLandscape || !userWantsStatusBar; - return false; +// iOS calls this to determine whether to hide the status bar. +// Note that on iPhone, iOS hides it in landscape regardless of what we return here. +- (BOOL)prefersStatusBarHidden { + return g_Config.GetDisplayLayoutConfig([self currentDeviceOrientation]).bImmersiveMode ? YES : NO; } // Optional: choose light/dark text for the status bar @@ -508,15 +509,26 @@ extern float g_safeInsetBottom; return UIStatusBarStyleLightContent; } -// This should also be called when the user preference changes. - (void)onOrientationChanged { [self setNeedsStatusBarAppearanceUpdate]; } +// Called from the C++ side when the user toggles the immersive mode setting. +- (void)immersiveModeChanged { + [self setNeedsStatusBarAppearanceUpdate]; +} + +- (void)viewWillTransitionToSize:(CGSize)size + withTransitionCoordinator:(id)coordinator { + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + // The immersive mode setting is per-orientation, so the status bar may need to change along with the rotation. + [self setNeedsStatusBarAppearanceUpdate]; +} + - (void)updateResolutionWithView:(UIView *)view { - // 1. Get the scale from the window scene (safest for Metal) + // 1. Get the scale from the window scene CGFloat scale = 1.0; - if (view.window.windowScene) { + if (view.window && view.window.windowScene) { scale = view.window.windowScene.screen.nativeScale; } else { scale = [UITraitCollection currentTraitCollection].displayScale; diff --git a/ios/ViewControllerMetal.mm b/ios/ViewControllerMetal.mm index 62645f52d2..bf634c197f 100644 --- a/ios/ViewControllerMetal.mm +++ b/ios/ViewControllerMetal.mm @@ -1,4 +1,3 @@ -#import "AppDelegate.h" #import "ViewControllerMetal.h" #import "iOSCoreAudio.h" @@ -193,8 +192,6 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met INFO_LOG(Log::System, "Metal viewDidLoad"); - UIScreen* screen = [(AppDelegate*)[UIApplication sharedApplication].delegate screen]; - self.view.frame = [screen bounds]; self.view.multipleTouchEnabled = YES; // self.view.insetsLayoutMarginsFromSafeArea = NO; // self.view.clipsToBounds = YES; @@ -206,7 +203,9 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met g_Config.iGPUBackend = (int)GPUBackend::OPENGL; SetGPUBackend(GPUBackend::OPENGL); delete graphicsContext; - // TODO: What to do here? + graphicsContext = nullptr; // The render loop and shutdown check for this. + // TODO: What to do here? We've switched the config over to GL, but we're still the Metal view controller, + // so we won't render anything until the app gets restarted. } [self updateResolutionWithView:self.view]; @@ -218,10 +217,6 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met INFO_LOG(Log::G3D, "Detected size: %dx%d", g_display.pixel_xres, g_display.pixel_yres); } -- (UIView *)getView { - return [self view]; -} - - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; INFO_LOG(Log::G3D, "viewWillAppear"); @@ -245,10 +240,6 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met INFO_LOG(Log::G3D, "viewDidDisappear"); } -- (void)bindDefaultFBO { - // Do nothing -} - - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; diff --git a/ios/iOSCoreAudio.h b/ios/iOSCoreAudio.h index 944e0e81dc..62cfa6e186 100644 --- a/ios/iOSCoreAudio.h +++ b/ios/iOSCoreAudio.h @@ -21,7 +21,5 @@ void iOSCoreAudioInit(); void iOSCoreAudioShutdown(); -// Ignore mute switch when connected to external display. -// Also, obey other settings. -void iOSCoreAudioUpdateSession(); -void iOSCoreAudioSetDisplayConnected(bool connected); \ No newline at end of file +// Applies the audio settings (mix with others, respect silent mode) to the session. +void iOSCoreAudioUpdateSession(); \ No newline at end of file diff --git a/ios/iOSCoreAudio.mm b/ios/iOSCoreAudio.mm index e8f7619583..9d8332db7a 100644 --- a/ios/iOSCoreAudio.mm +++ b/ios/iOSCoreAudio.mm @@ -29,18 +29,9 @@ #define SAMPLE_RATE 44100 static AudioComponentInstance audioInstance = nil; -static bool g_displayConnected = false; void iOSCoreAudioUpdateSession() { NSError *error = nil; - if (g_displayConnected) { - INFO_LOG(Log::Audio, "Display connected, setting Playback mode"); - // Special handling when a display is connected. Always exclusive. - // Let's revisit this later. - [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; - return; - } - INFO_LOG(Log::Audio, "RespectSilentMode: %d MixWithOthers: %d", g_Config.bAudioRespectSilentMode, g_Config.bAudioMixWithOthers); // Hacky hack to force iOS to re-evaluate. @@ -48,7 +39,6 @@ void iOSCoreAudioUpdateSession() { [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAudioProcessing error:&error]; // Here, we apply the settings. - const bool mixWithOthers = g_Config.bAudioMixWithOthers; if (g_Config.bAudioMixWithOthers) { if (g_Config.bAudioRespectSilentMode) { [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:&error]; @@ -69,11 +59,6 @@ void iOSCoreAudioUpdateSession() { } } -void iOSCoreAudioSetDisplayConnected(bool connected) { - g_displayConnected = connected; - iOSCoreAudioUpdateSession(); -} - void NativeMix(short *audio, int numSamples, int sampleRateHz, void *userdata); OSStatus iOSCoreAudioCallback(void *inRefCon, diff --git a/ios/main.mm b/ios/main.mm index 989e50f5d3..51c646b507 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -432,6 +432,13 @@ void System_Notify(SystemNotification notification) { } }); break; + case SystemNotification::IMMERSIVE_MODE_CHANGE: + dispatch_async(dispatch_get_main_queue(), ^{ + if (sharedViewController) { + [sharedViewController immersiveModeChanged]; + } + }); + break; case SystemNotification::UI_STATE_CHANGED: dispatch_async(dispatch_get_main_queue(), ^{ if (sharedViewController) { @@ -452,6 +459,7 @@ void System_Notify(SystemNotification notification) { [UIViewController attemptRotationToDeviceOrientation]; } }); + break; default: break; } @@ -596,7 +604,11 @@ void System_AskForPermission(SystemPermission permission) {} void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) { std::string strUrl(url); - NSURL *nsUrl = [NSURL URLWithString:[NSString stringWithCString:strUrl.c_str() encoding:NSStringEncodingConversionAllowLossy]]; + NSURL *nsUrl = [NSURL URLWithString:[NSString stringWithUTF8String:strUrl.c_str()]]; + if (!nsUrl) { + ERROR_LOG(Log::System, "Failed to parse URL: %s", strUrl.c_str()); + return; + } dispatch_async(dispatch_get_main_queue(), ^{ [[UIApplication sharedApplication] openURL:nsUrl options:@{} completionHandler:nil]; });