From 4c7d83a2bf17fe460af7b4cdeddee410010b5323 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 00:58:30 +0200 Subject: [PATCH 1/2] feat(libSceHttp): implement sceHttpAbortWaitRequest and the unlimited sceHttpWaitRequest Deathloop (PPSA01670) imports sceHttpAbortWaitRequest; Planet Coaster (PPSA01736) and Palworld import sceHttpWaitRequest. Epoll handles are kept in a registry with an abort flag, a destroyed flag and a condition variable. A wait with a negative timeout blocks until sceHttpAbortWaitRequest, sceHttpDestroyEpoll or the sceHttpTerm of the last context wakes it; a bounded wait still returns 0 events. An abort stays set and makes every later wait return SCE_HTTP_ERROR_ABORTED (0x80431080); a wait woken by destroy or term returns 0 events. sceHttpDestroyEpoll no longer frees a handle a waiter is blocked on. sceHttpInit registers its context, and the epoll calls and sceHttpTerm return BEFORE_INIT without a live context and INVALID_ID for an unknown context or handle. Behaviour and error codes: shadPS4 src/core/libraries/network/http.cpp and http_error.h at a8a70da595da2a96e0410309472ba4a7eb558144. Signature (void* eh): Orbital tools/ida/db_types.json at b321fcdbd700cc02da1cb1ad25412781d5ec73c6 and SharpProspero src/SharpProspero/Interop/Net/Http.cs at 60977b29103452b9681ae10c7a9011d44965bf16. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 766997df3f8329fd6e8e7aac2b5d83daf88c8292) --- core/libs/SceTypes.hpp | 2 +- core/libs/prx/libSceHttp/CMakeLists.txt | 3 +- core/libs/prx/libSceHttp/Export.cpp | 96 ++++++++++++++++++--- core/libs/prx/libSceHttp/src/HttpErrors.hpp | 3 + core/libs/tests/GuestHttp.cpp | 61 ++++++++++++- docs/dev/TechnicalDebt.md | 3 +- 6 files changed, 149 insertions(+), 19 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index bd63ff766..f7405fa94 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1253,7 +1253,7 @@ union NetCtlInfo { std::uint8_t opaque[256]; }; using NetCtlCallback = void (*)(int, void*); -struct HttpEpoll {}; +struct HttpEpoll; using HttpEpollHandle = HttpEpoll*; using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*); using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*); diff --git a/core/libs/prx/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt index 5c930a4e2..48a098bec 100644 --- a/core/libs/prx/libSceHttp/CMakeLists.txt +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -5,7 +5,8 @@ configure_windows_unwind(libSceHttp) if(BUILD_TESTING) add_test_executable(guest_http_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttp.cpp) target_include_directories(guest_http_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_http_tests PRIVATE libSceHttp) + target_link_libraries(guest_http_tests PRIVATE libSceHttp libc) + configure_windows_unwind(guest_http_tests) add_test(NAME guest_http COMMAND guest_http_tests) add_test_executable(guest_http_response_header_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttpResponseHeader.cpp) target_include_directories(guest_http_response_header_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 3b9216532..7e0e9872f 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -5,12 +5,16 @@ #include "prx/libSceHttp/src/HttpErrors.hpp" #include #include +#include #include #include +#include #include #include #include -#include +#include +#include +#include // No network is emulated: contexts, templates and requests can be created, but any request // that would touch the network fails with the library's network error. @@ -23,6 +27,28 @@ struct RequestStatusRegistration { static std::mutex g_statusMutex; static std::map g_statusCallbacks; +struct HttpEpoll { + std::mutex mutex; + std::condition_variable wake; + bool aborted = false; + bool destroyed = false; +}; + +static std::mutex g_stateMutex; +static std::unordered_set g_contexts; +static std::unordered_map> g_epolls; + +static std::shared_ptr FindEpoll(HttpEpollHandle eh) { + const auto it = g_epolls.find(eh); + return it == g_epolls.end() ? nullptr : it->second; +} + +static void MarkDestroyed(HttpEpoll& epoll) { + std::lock_guard lock(epoll.mutex); + epoll.destroyed = true; + epoll.wake.notify_all(); +} + extern "C" { int APS5_VABI sceHttpAbortRequest(int request_id) { @@ -55,9 +81,13 @@ int APS5_VABI sceHttpCreateConnectionWithURL(int tmpl_id, const char* url, int e } int APS5_VABI sceHttpCreateEpoll(int http_ctx_id, HttpEpollHandle* eh) { - (void)http_ctx_id; + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; if (!eh) return ERROR_INVALID_VALUE; - *eh = new HttpEpoll{}; + auto epoll = std::make_shared(); + *eh = epoll.get(); + g_epolls.emplace(epoll.get(), std::move(epoll)); return 0; } @@ -102,8 +132,17 @@ int APS5_VABI sceHttpDeleteTemplate(int tmpl_id) { } int APS5_VABI sceHttpDestroyEpoll(int http_ctx_id, HttpEpollHandle eh) { - (void)http_ctx_id; - delete eh; + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; + if (!eh) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + g_epolls.erase(eh); + } + MarkDestroyed(*epoll); return 0; } @@ -131,7 +170,10 @@ int APS5_VABI sceHttpInit_nid_postfix(int memid, int ssl_ctx_id, uint64_t pool_s (void)memid; (void)ssl_ctx_id; (void)pool_size; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_stateMutex); + g_contexts.insert(id); + return id; } int APS5_VABI sceHttpsDisableOption(int id, uint32_t ssl_flags) { @@ -248,7 +290,16 @@ int APS5_VABI sceHttpsSetSslCallback(int id, HttpsCallback cbfunc, void* user_ar } int APS5_VABI sceHttpTerm_nid_postfix(int http_ctx_id) { - (void)http_ctx_id; + std::vector> epolls; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (g_contexts.erase(http_ctx_id) == 0) return ERROR_INVALID_ID; + if (!g_contexts.empty()) return 0; + for (auto& [handle, epoll] : g_epolls) epolls.push_back(std::move(epoll)); + g_epolls.clear(); + } + for (const auto& epoll : epolls) MarkDestroyed(*epoll); return 0; } @@ -258,10 +309,19 @@ int APS5_VABI sceHttpUnsetEpoll(int id) { } int APS5_VABI sceHttpWaitRequest(HttpEpollHandle eh, HttpNBEvent* nbev, int maxevents, int timeout) { - if (!eh || !nbev || maxevents <= 0) return ERROR_INVALID_VALUE; - if (timeout < 0) NotImplemented_nid_no_patch(__func__); - std::this_thread::sleep_for(std::chrono::microseconds(timeout)); - return 0; + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!eh || !nbev || maxevents <= 0) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + } + std::unique_lock lock(epoll->mutex); + const auto woken = [&] { return epoll->aborted || epoll->destroyed; }; + if (timeout < 0) epoll->wake.wait(lock, woken); + else epoll->wake.wait_for(lock, std::chrono::microseconds(timeout), woken); + return epoll->aborted ? ERROR_ABORTED : 0; } int APS5_VABI sceHttpCreateRequestWithURL(int conn_id, int method, const char* url, uint64_t content_length) { @@ -434,8 +494,18 @@ int APS5_VABI sceHttpSetRedirectCallback(int id, HttpRedirectCallback cbfunc, vo return 0; } -int APS5_VABI sceHttpAbortWaitRequest(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttpAbortWaitRequest(HttpEpollHandle eh) { + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!eh) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + } + std::lock_guard lock(epoll->mutex); + epoll->aborted = true; + epoll->wake.notify_all(); return 0; } } diff --git a/core/libs/prx/libSceHttp/src/HttpErrors.hpp b/core/libs/prx/libSceHttp/src/HttpErrors.hpp index b95f8d25b..1b18ec3e1 100644 --- a/core/libs/prx/libSceHttp/src/HttpErrors.hpp +++ b/core/libs/prx/libSceHttp/src/HttpErrors.hpp @@ -1,8 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBSCEHTTP_SRC_HTTPERRORS_HPP #define CORE_LIBS_PRX_LIBSCEHTTP_SRC_HTTPERRORS_HPP +inline constexpr int ERROR_BEFORE_INIT = static_cast(0x80431001); inline constexpr int ERROR_OUT_OF_MEMORY = static_cast(0x80431022); inline constexpr int ERROR_NETWORK = static_cast(0x80431063); +inline constexpr int ERROR_ABORTED = static_cast(0x80431080); +inline constexpr int ERROR_INVALID_ID = static_cast(0x80431100); inline constexpr int ERROR_INVALID_VALUE = static_cast(0x804311FE); inline constexpr int ERROR_INVALID_URL = static_cast(0x80433060); inline constexpr int ERROR_PARSE_HTTP_INVALID_RESPONSE = static_cast(0x80432060); diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 9d18283f0..6183f70b8 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -1,9 +1,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include #include +#include extern "C" { int APS5_VABI sceHttpUriParse(SceHttpUriElement*, const char*, void*, std::size_t*, std::size_t); @@ -18,6 +21,9 @@ int APS5_VABI sceHttpUriSweepPath(char*, const char*, std::size_t); int APS5_VABI sceHttpCreateEpoll(int, HttpEpollHandle*); int APS5_VABI sceHttpDestroyEpoll(int, HttpEpollHandle); int APS5_VABI sceHttpWaitRequest(HttpEpollHandle, HttpNBEvent*, int, int); +int APS5_VABI sceHttpAbortWaitRequest(HttpEpollHandle); +int APS5_VABI sceHttpInit_nid_postfix(int, int, std::uint64_t); +int APS5_VABI sceHttpTerm_nid_postfix(int); int APS5_VABI sceHttpReadData(int, void*, std::size_t); int APS5_VABI sceHttpCreateRequest2(int, const char*, const char*, std::uint64_t); int APS5_VABI sceHttpsEnableOption(int, std::uint32_t); @@ -199,16 +205,65 @@ int main() { Require(sceHttpUriMerge(merged, base, "http://bad host/", &required, sizeof(merged), 0) == invalidUrl); Require(required == 123); + constexpr int beforeInit = static_cast(0x80431001); + constexpr int aborted = static_cast(0x80431080); + constexpr int invalidId = static_cast(0x80431100); HttpEpollHandle epoll = nullptr; - Require(sceHttpCreateEpoll(1, nullptr) == invalidValue); - Require(sceHttpCreateEpoll(1, &epoll) == 0 && epoll != nullptr); HttpNBEvent events[2]{}; + Require(sceHttpCreateEpoll(1, &epoll) == beforeInit); + Require(sceHttpAbortWaitRequest(nullptr) == beforeInit); + Require(sceHttpTerm_nid_postfix(1) == beforeInit); + const int context = sceHttpInit_nid_postfix(0, 0, 0x10000); + Require(context > 0); + Require(sceHttpCreateEpoll(-1, &epoll) == invalidId); + Require(sceHttpCreateEpoll(context, nullptr) == invalidValue); + Require(sceHttpCreateEpoll(context, &epoll) == 0 && epoll != nullptr); Require(sceHttpWaitRequest(epoll, events, 2, 0) == 0); Require(sceHttpWaitRequest(epoll, events, 2, 1000) == 0); Require(sceHttpWaitRequest(epoll, nullptr, 2, 0) == invalidValue); Require(sceHttpWaitRequest(epoll, events, 0, 0) == invalidValue); Require(sceHttpWaitRequest(nullptr, events, 2, 0) == invalidValue); - Require(sceHttpDestroyEpoll(1, epoll) == 0); + const auto unknownEpoll = reinterpret_cast(&events); + Require(sceHttpAbortWaitRequest(nullptr) == invalidValue); + Require(sceHttpAbortWaitRequest(unknownEpoll) == invalidId); + Require(sceHttpWaitRequest(unknownEpoll, events, 2, -1) == invalidId); + Require(sceHttpDestroyEpoll(context, nullptr) == invalidValue); + Require(sceHttpDestroyEpoll(context, unknownEpoll) == invalidId); + Require(sceHttpDestroyEpoll(-1, epoll) == invalidId); + + auto waitWokenBy = [&](auto wake) { + std::atomic started{false}; + std::atomic result{1}; + std::thread waiter([&] { + started = true; + result = sceHttpWaitRequest(epoll, events, 2, -1); + }); + while (!started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(result == 1); + wake(); + waiter.join(); + return result.load(); + }; + Require(waitWokenBy([&] { Require(sceHttpAbortWaitRequest(epoll) == 0); }) == aborted); + Require(sceHttpWaitRequest(epoll, events, 2, 0) == aborted); + Require(sceHttpWaitRequest(epoll, events, 2, -1) == aborted); + Require(sceHttpDestroyEpoll(context, epoll) == 0); + Require(sceHttpAbortWaitRequest(epoll) == invalidId); + Require(sceHttpDestroyEpoll(context, epoll) == invalidId); + + Require(sceHttpCreateEpoll(context, &epoll) == 0); + Require(waitWokenBy([&] { Require(sceHttpDestroyEpoll(context, epoll) == 0); }) == 0); + + Require(sceHttpCreateEpoll(context, &epoll) == 0); + const int otherContext = sceHttpInit_nid_postfix(0, 0, 0x10000); + Require(otherContext > 0 && otherContext != context); + Require(sceHttpTerm_nid_postfix(otherContext) == 0); + Require(sceHttpTerm_nid_postfix(otherContext) == invalidId); + Require(sceHttpWaitRequest(epoll, events, 2, 0) == 0); + Require(waitWokenBy([&] { Require(sceHttpTerm_nid_postfix(context) == 0); }) == 0); + Require(sceHttpWaitRequest(epoll, events, 2, 0) == beforeInit); + Require(sceHttpTerm_nid_postfix(context) == beforeInit); char data[16]; Require(sceHttpReadData(1, data, sizeof(data)) == network); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..711887192 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -335,7 +335,7 @@ Throughout the project, every function at every stage either **does exactly what - [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes -- [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so the call waits for the timeout and returns 0 events, and a wait without limit throws +- [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes - [sceVideoOutAddPreVblankStartEvent](../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data - [sched_get_priority_max, sched_get_priority_min](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `SCHED_OTHER` (2) reports the same 256 to 767 range as `SCHED_FIFO` and `SCHED_RR`; the console's answer is unverified. FreeBSD accepts `SCHED_OTHER` with its own range, and shadPS4 rejects it with `EINVAL`. `pthread_setschedparam` ignores the policy, so a priority taken from this range applies @@ -364,6 +364,7 @@ Throughout the project, every function at every stage either **does exactly what - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw +- [sceHttpAbortWaitRequest, sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the abort takes only the epoll handle, as in [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); behaviour and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), not confirmed on a PS5 title or hardware. An abort stays set: every later wait on the handle, a poll included, returns `SCE_HTTP_ERROR_ABORTED` (0x80431080) until the handle is destroyed. A wait woken by `sceHttpDestroyEpoll`, or by the `sceHttpTerm` that ends the last context, returns 0 events. The epoll calls return `BEFORE_INIT` (0x80431001) while no `sceHttpInit` context is alive and `INVALID_ID` (0x80431100) for an unknown context or handle, and `sceHttpTerm` returns the same errors instead of succeeding for any id. Handles are pointers, so a stale handle whose address a newer epoll reuses is taken as that epoll - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled from `std::random_device`; both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws From 776cff0efb3c96082c71b29e636a7107e53b9ccb Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 15:40:29 +0000 Subject: [PATCH 2/2] fix(libSceHttp): use native Windows condition waits Use an SRW lock and native Windows condition variable for epoll waits, preserving the shared handle lifecycle. This avoids the scheduler-tick overshoot measured with std::condition_variable on Windows. Suggested-by: winstrex --- core/libs/prx/libSceHttp/Export.cpp | 79 ++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 7e0e9872f..b2d1bfbce 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -1,5 +1,9 @@ #include #include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" @@ -27,9 +31,68 @@ struct RequestStatusRegistration { static std::mutex g_statusMutex; static std::map g_statusCallbacks; +#ifdef _WIN32 +struct HostMutex { + SRWLOCK native = SRWLOCK_INIT; + + HostMutex() = default; + HostMutex(const HostMutex&) = delete; + HostMutex& operator=(const HostMutex&) = delete; + + void lock() { AcquireSRWLockExclusive(&native); } + void unlock() { ReleaseSRWLockExclusive(&native); } +}; +#else +using HostMutex = std::mutex; +#endif + +class HostCondition { +public: + HostCondition() = default; + HostCondition(const HostCondition&) = delete; + HostCondition& operator=(const HostCondition&) = delete; + + void NotifyAll() { +#ifdef _WIN32 + WakeAllConditionVariable(&native); +#else + native.notify_all(); +#endif + } + + void Wait(std::unique_lock& lock) { +#ifdef _WIN32 + if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, INFINITE, 0)) + throw std::runtime_error("libSceHttp: native condition wait failed"); +#else + native.wait(lock); +#endif + } + + void WaitUntil(std::unique_lock& lock, std::chrono::steady_clock::time_point deadline) { +#ifdef _WIN32 + const auto now = std::chrono::steady_clock::now(); + if (deadline <= now) return; + const auto milliseconds = std::chrono::ceil(deadline - now).count(); + if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, static_cast(milliseconds), 0) && + GetLastError() != ERROR_TIMEOUT) + throw std::runtime_error("libSceHttp: native condition wait failed"); +#else + native.wait_until(lock, deadline); +#endif + } + +private: +#ifdef _WIN32 + CONDITION_VARIABLE native = CONDITION_VARIABLE_INIT; +#else + std::condition_variable native; +#endif +}; + struct HttpEpoll { - std::mutex mutex; - std::condition_variable wake; + HostMutex mutex; + HostCondition wake; bool aborted = false; bool destroyed = false; }; @@ -46,7 +109,7 @@ static std::shared_ptr FindEpoll(HttpEpollHandle eh) { static void MarkDestroyed(HttpEpoll& epoll) { std::lock_guard lock(epoll.mutex); epoll.destroyed = true; - epoll.wake.notify_all(); + epoll.wake.NotifyAll(); } extern "C" { @@ -319,8 +382,12 @@ int APS5_VABI sceHttpWaitRequest(HttpEpollHandle eh, HttpNBEvent* nbev, int maxe } std::unique_lock lock(epoll->mutex); const auto woken = [&] { return epoll->aborted || epoll->destroyed; }; - if (timeout < 0) epoll->wake.wait(lock, woken); - else epoll->wake.wait_for(lock, std::chrono::microseconds(timeout), woken); + if (timeout < 0) { + while (!woken()) epoll->wake.Wait(lock); + } else { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::microseconds(timeout); + while (!woken() && std::chrono::steady_clock::now() < deadline) epoll->wake.WaitUntil(lock, deadline); + } return epoll->aborted ? ERROR_ABORTED : 0; } @@ -505,7 +572,7 @@ int APS5_VABI sceHttpAbortWaitRequest(HttpEpollHandle eh) { } std::lock_guard lock(epoll->mutex); epoll->aborted = true; - epoll->wake.notify_all(); + epoll->wake.NotifyAll(); return 0; } }