From 1a0c43b69f26260f38ecf9bfc35b7ebfb1ddac04 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:53:18 +0000 Subject: [PATCH 01/63] perf(agc): retain link-time optimization info only where a link is optimized Every graphics pipeline library was created with VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT, and libraries live until the device is destroyed. Only the background relink reads that info, and it exists only when the device's links are optimized: not on the NVIDIA proprietary driver and not with APS5_NO_PIPELINE_LTO set. A library now asks for the info only when its link will be optimized, and whether it will be is part of the library cache key, so a link that is optimized never names a library made without the info, even when a context forces pipelineLinkTimeOptimization on a device whose other contexts do not. Draws, journal replays and shader library prebuilds all create their libraries through this one path. The pipeline readback tests count the libraries created with the flag next to their pixel checks, and a new recorder mode checks that a context without optimized links makes libraries without the info and that a context with them makes its own. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../Graphics/src/PipelineLibrary.cpp | 5 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 95 +++++++++++++++++++ .../execution/PipelineOptimizedReadback.cpp | 16 +++- 4 files changed, 117 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f6f2e95e22..14b9de7595 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1380,8 +1380,12 @@ if(BUILD_TESTING) set_tests_properties(agc_pipeline_library_off PROPERTIES ENVIRONMENT "APS5_NO_GPL=1") add_test(NAME agc_pipeline_library_fast_only COMMAND agc_driver_recorder_tests --pipeline-library-only) set_tests_properties(agc_pipeline_library_fast_only PROPERTIES ENVIRONMENT "APS5_NO_PIPELINE_LTO=1" SKIP_RETURN_CODE 77) + add_test(NAME agc_pipeline_library_lto_info COMMAND agc_driver_recorder_tests --pipeline-library-lto-info-only) + add_test(NAME agc_pipeline_library_lto_info_fast_only COMMAND agc_driver_recorder_tests --pipeline-library-lto-info-only) + set_tests_properties(agc_pipeline_library_lto_info agc_pipeline_library_lto_info_fast_only PROPERTIES SKIP_RETURN_CODE 77) + set_tests_properties(agc_pipeline_library_lto_info_fast_only PROPERTIES ENVIRONMENT "APS5_NO_PIPELINE_LTO=1") if(WIN32) - set_tests_properties(agc_pipeline_library agc_pipeline_library_off agc_pipeline_library_fast_only PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_pipeline_library agc_pipeline_library_off agc_pipeline_library_fast_only agc_pipeline_library_lto_info agc_pipeline_library_lto_info_fast_only PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_tests_properties(agc_pipeline_cache agc_pipeline_cache_file PROPERTIES SKIP_RETURN_CODE 77) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index 7670cde694..f1309d8e0f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -185,7 +185,8 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip std::vector libraries; for (std::size_t part = 0; part < flags.size(); ++part) { if (part == 0 && mesh) continue; - const auto key = Combined(*partKeys[part], *contextKeys[part]); + auto key = Combined(*partKeys[part], *contextKeys[part]); + key.push_back(static_cast(optimize)); auto& cache = device.parts[part]; if (const auto found = cache.find(key); found != cache.end()) { libraries.push_back(found->second); @@ -195,7 +196,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip VkGraphicsPipelineLibraryCreateInfoEXT library{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_LIBRARY_CREATE_INFO_EXT, flags[part] == VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_OUTPUT_INTERFACE_BIT_EXT ? &rendering : &shaderRendering}; library.flags = flags[part]; VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &library}; - create.flags = VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT; + create.flags = optimize ? VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT : VK_PIPELINE_CREATE_LIBRARY_BIT_KHR; create.pDynamicState = info.pDynamicState; create.layout = partLayouts[part]; switch (flags[part]) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 9c1ae9ae14..d9060628d0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -9676,6 +9676,97 @@ void denormalClearTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +class LibraryCreateFlags { +public: + explicit LibraryCreateFlags(const Context& context) : resolver(context.deviceProc), device(context.device) { + create = function("vkCreateGraphicsPipelines"); + active = this; + } + ~LibraryCreateFlags() { active = nullptr; } + LibraryCreateFlags(const LibraryCreateFlags&) = delete; + LibraryCreateFlags& operator=(const LibraryCreateFlags&) = delete; + PFN_vkGetDeviceProcAddr Resolver() const { return resolve; } + std::uint32_t Libraries() const { return libraries.load(); } + std::uint32_t Retaining() const { return retaining.load(); } + +private: + template + TFunction function(const char* name) const { return reinterpret_cast(resolver(device, name)); } + static VKAPI_ATTR VkResult VKAPI_CALL createPipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* callbacks, VkPipeline* pipelines) { + const auto result = active->create(device, cache, count, infos, callbacks, pipelines); + for (std::uint32_t index = 0; index < count; ++index) { + if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) == 0) continue; + active->libraries.fetch_add(1); + if ((infos[index].flags & VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT) != 0) active->retaining.fetch_add(1); + } + return result; + } + static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) return reinterpret_cast(createPipelines); + return active->resolver(device, name); + } + PFN_vkGetDeviceProcAddr resolver; + VkDevice device; + PFN_vkCreateGraphicsPipelines create = nullptr; + std::atomic libraries{0}; + std::atomic retaining{0}; + static inline LibraryCreateFlags* active = nullptr; +}; + +void pipelineLibraryLtoInfoTests(const Device& device) { + std::lock_guard gpu(GpuMutex()); + const auto& context = device.GetContext(); + LibraryCreateFlags created(context); + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearCachedPipelines(device); } + } cleanup{context.device}; + ClearCachedPipelines(context.device); + if (!context.graphicsPipelineLibrary) { + std::cout << "graphics pipeline libraries unavailable or disabled: no library retains link-time optimization info\n"; + return; + } + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.variantId = 41; + fragment.variantId = 42; + vertex.spirv = std::vector(std::begin(TRIANGLE_vert_SPV), std::end(TRIANGLE_vert_SPV)); + fragment.spirv = std::vector(std::begin(TRIANGLE_frag_SPV), std::end(TRIANGLE_frag_SPV)); + const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}; + State state{}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.hasColorTarget = true; + state.color.format = VK_FORMAT_R8G8B8A8_UNORM; + state.colors.push_back(state.color); + state.blend.colorWriteMask = 15; + state.blends.push_back(state.blend); + ShaderResources resources(context, shaders, state.color, static_cast(state.colors.size()), 0, 0); + VertexInputLayout input; + auto fast = context; + fast.deviceProc = created.Resolver(); + fast.pipelineLinkTimeOptimization = false; + auto optimizing = fast; + optimizing.pipelineLinkTimeOptimization = true; + const bool lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; + const auto fastPipeline = CachedPipeline(fast, state, input, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); + const auto fastLibraries = created.Libraries(); + Require(fastLibraries != 0, "the pipeline was not linked from libraries created through the traced device"); + Require(created.Retaining() == 0, std::to_string(created.Retaining()) + " of the " + std::to_string(fastLibraries) + " libraries made for a context without optimized links were created to retain link-time optimization info"); + WaitForOptimizedPipelines(context.device); + Require(!fastPipeline->Optimized(), "a pipeline of a context without optimized links was optimized in the background"); + const auto optimizedPipeline = CachedPipeline(optimizing, state, input, resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + Require(optimizedPipeline != fastPipeline, "the two attachment layouts shared one cached pipeline"); + const auto libraries = created.Libraries() - fastLibraries; + if (lto) { + Require(libraries == fastLibraries, "a link that will be optimized made " + std::to_string(libraries) + " libraries, expected " + std::to_string(fastLibraries) + ": it reused libraries made without link-time optimization info"); + Require(created.Retaining() == libraries, std::to_string(created.Retaining()) + " of the " + std::to_string(libraries) + " libraries of a link that will be optimized were created to retain link-time optimization info"); + } else { + Require(libraries == 0 && created.Retaining() == 0, "with APS5_NO_PIPELINE_LTO set, forcing pipelineLinkTimeOptimization changed the libraries or their create flags"); + } + WaitForOptimizedPipelines(context.device); + Require(optimizedPipeline->Optimized() == lto, lto ? "the pipeline linked from libraries that retain link-time optimization info was not optimized in the background" : "a pipeline was optimized with APS5_NO_PIPELINE_LTO set"); + std::cout << "Pipeline library link-time optimization info tests passed: " << fastLibraries << " libraries without the info, " << created.Retaining() << " with it\n"; +} + void pipelineLibraryTests(const Device& device) { std::lock_guard gpu(GpuMutex()); const auto& context = device.GetContext(); @@ -10023,6 +10114,10 @@ int main(int argc, char** argv) { } return sparseRunImportTests(device, true) ? 0 : 77; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-lto-info-only") { + pipelineLibraryLtoInfoTests(device); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-only") { pipelineLibraryTests(device); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp index f2b56411c0..2ff511dcd1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp @@ -64,6 +64,8 @@ struct PipelineTrace { std::mutex mutex; std::condition_variable released; bool allowOptimization = false; + std::uint32_t libraries = 0; + std::uint32_t retaining = 0; std::vector fast; std::vector optimized; VkCommandBuffer boundCommands = VK_NULL_HANDLE; @@ -90,7 +92,12 @@ VKAPI_ATTR VkResult VKAPI_CALL CreatePipelines(VkDevice device, VkPipelineCache const auto result = Trace->create(device, cache, count, infos, allocator, pipelines); std::lock_guard lock(Trace->mutex); for (std::uint32_t index = 0; index < count; ++index) { - if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) continue; + if (pipelines[index] == VK_NULL_HANDLE) continue; + if ((infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) { + ++Trace->libraries; + if ((infos[index].flags & VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT) != 0) ++Trace->retaining; + continue; + } auto& handles = (infos[index].flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0 ? Trace->optimized : Trace->fast; handles.push_back(pipelines[index]); } @@ -217,12 +224,17 @@ void Run(AgcDriver::VulkanDevice& device, Context context, PipelineTrace& trace, CheckPixels(masked, phase); }; render(false, "fast link"); + { + std::lock_guard lock(trace.mutex); + Require(trace.libraries != 0, "the readback pipeline's libraries were not created through the traced device"); + Require(trace.retaining == (optimize ? trace.libraries : 0u), std::to_string(trace.retaining) + " of the " + std::to_string(trace.libraries) + " libraries were created to retain link-time optimization info, expected " + (optimize ? "all of them" : "none, since no optimized link will be made")); + } trace.ReleaseOptimization(); WaitForOptimizedPipelines(context.device); Require(PipelineLibraryCountersOf(context.device).linked == 1, "the readback pipeline was not linked from graphics libraries"); Require(trace.optimized.size() == (optimize ? 1u : 0u), "the optimized pipeline creation count differs from the requested mode"); render(optimize, "completed link"); - std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size()); + std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu, libraries %u (%u retaining link-time optimization info)\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size(), trace.libraries, trace.retaining); } } From 65920e6fdc69f88b9d07c12e1568ceac08a6ead1 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:52:51 +0100 Subject: [PATCH 02/63] test(libc): cover CRT exit and TLS cleanup lifecycle --- core/libs/tests/GuestLibcThreads.cpp | 80 ++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 4 ++ 2 files changed, 84 insertions(+) diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 001558be39..5e296a0e5d 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -1,13 +1,20 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include +#include #include +#include #include #include extern "C" { +int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*); +int APS5_VABI scePthreadJoin(Pthread, void**); +void APS5_VABI _Thrd_exit_nid_postfix(int); int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); int APS5_VABI _Mtx_lock_nid_postfix(void** handle); @@ -263,6 +270,78 @@ static unsigned APS5_VABI TestPadGo(GuestPad* base) { return 42; } +static void RequireExit(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "CRT exit lifecycle: %s\n", message); + std::abort(); + } +} + +struct CrtExitState { + int code = 0; + int key = -1; + std::barrier<>* ready = nullptr; + void* parent = nullptr; + void* self = nullptr; + std::atomic destructors{0}; + std::atomic returned{false}; +}; + +static void APS5_VABI CrtExitDestructor(void* value) { + auto& state = *static_cast(value); + RequireExit(_Thrd_current_nid_postfix() == state.self, "identity during destructor"); + RequireExit(_Tss_get_nid_postfix(state.key) == nullptr, "value cleared before destructor"); + if (state.destructors.fetch_add(1) == 0) + RequireExit(_Tss_set_nid_postfix(state.key, &state) == 0, "reinstall value for another cleanup pass"); +} + +static void* APS5_VABI CrtExitEntry(void* value) { + auto& state = *static_cast(value); + state.self = _Thrd_current_nid_postfix(); + RequireExit(state.self && _Thrd_equal_nid_postfix(state.self, state.self) == 1, "current identity"); + RequireExit(_Thrd_equal_nid_postfix(state.self, state.parent) == 0, "different parent identity"); + RequireExit(_Tss_get_nid_postfix(state.key) == nullptr, "child starts with empty value"); + RequireExit(_Tss_set_nid_postfix(state.key, &state) == 0, "set child value"); + state.ready->arrive_and_wait(); + RequireExit(_Tss_get_nid_postfix(state.key) == &state, "child value isolation"); + _Thrd_exit_nid_postfix(state.code); + state.returned = true; + return nullptr; +} + +static void ThreadExitLifecycle() { + constexpr std::array codes{0, 42, -1, std::numeric_limits::min(), std::numeric_limits::max()}; + constexpr std::array expected{0, 42, 0xffffffffu, 0x80000000u, 0x7fffffffu}; + std::barrier ready(6); + std::array states; + std::array threads{}; + int key = -1, parentValue = 31; + void* parent = _Thrd_current_nid_postfix(); + RequireExit(_Tss_create_nid_postfix(&key, CrtExitDestructor) == 0, "create destructor key"); + RequireExit(_Tss_set_nid_postfix(key, &parentValue) == 0, "set parent value"); + for (unsigned i = 0; i < states.size(); ++i) { + states[i].code = codes[i]; + states[i].key = key; + states[i].ready = &ready; + states[i].parent = parent; + RequireExit(scePthreadCreate(&threads[i], nullptr, CrtExitEntry, &states[i], "CRT exit audit") == 0, "create child"); + } + RequireExit(_Tss_get_nid_postfix(key) == &parentValue, "parent isolated before release"); + ready.arrive_and_wait(); + for (unsigned i = 0; i < states.size(); ++i) { + void* result = nullptr; + RequireExit(scePthreadJoin(threads[i], &result) == 0, "join exiting child"); + RequireExit(reinterpret_cast(result) == expected[i], "full zero-extended exit result"); + RequireExit(!states[i].returned, "exit must not return"); + RequireExit(states[i].destructors == 2, "reinstalled destructor runs twice before join returns"); + RequireExit(states[i].self == threads[i], "created and current identity match"); + } + RequireExit(_Tss_get_nid_postfix(key) == &parentValue, "parent isolated after children exit"); + RequireExit(_Tss_delete_nid_postfix(key) == 0, "delete key"); + RequireExit(_Tss_get_nid_postfix(key) == nullptr, "deleted key value unavailable"); + std::puts("PASS CRT lifecycle: five concurrent exit codes, full return width, identity, parent/child isolation, two destructor passes, key deletion"); +} + static void PadLaunchAndJoin() { unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; TestPad unregPad{}; @@ -375,6 +454,7 @@ static void ForwardedCalls() { } int main() { + ThreadExitLifecycle(); KernelKeys(); ForwardedCalls(); MutexBasics(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 581f589299..dd5684d3b9 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -1,5 +1,9 @@ # libc technical debt +### Silent stubs + +- [_Mtx_current_owns](../../../core/libs/prx/libc/src/Threads.cpp) (libSceLibcInternal, implemented in libc, NID `VYQwFs4CC4Y`) returns 1 and ignores its argument. This follows the PS5 12.00 body reading reported in [PR8859](https://github.com/boykopovar/AnyPS5/pull/8859), rather than inspecting host mutex ownership. The firmware observation is attributed to that contributor; other firmware versions and the argument's console type remain unverified. + ### Unknown function info - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. From 408a656e7b2e507fbf2db7eda43033cdbc53ad9a Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:41:26 +0100 Subject: [PATCH 03/63] fix(agc): put a stencil replay shader's guest buffer offsets where its shader data is A pixel shader whose stencil export is replayed gets a push constant block that ends with the stencil reference word. When its shader data is not in push constants (registration modules give a shader with an image a shader data buffer, and data over the stage push range goes there too), the block holds only that word, and buildPrepare took any non-empty block to mean the guest buffer offsets are in push constants: the draw threw "guest buffer offset lies outside the shader's push constants". Only the stencilReferenceDword words before the reference now count as shader data in push constants; with none, the offsets go to the shader data buffer, where the shader reads them. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/ShaderResources.cpp | 5 ++-- .../tests/execution/StencilReplay.cpp | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d6d84cd4aa..d62578d410 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1986,9 +1986,10 @@ void ShaderResources::buildPrepare(std::span shaders, cons const bool classified = element < binding.bufferWritten.size() && element < binding.bufferRead.size() && element < binding.bufferAtomic.size(); allocations[index].writeOnly = classified && allocations[index].guest && allocations[index].written && binding.bufferWritten[element] && !read && !atomic; const auto& push = shader.program->pushConstants; - if (!push.empty()) { + const auto pushDataBytes = shader.program->stencilReferences.empty() ? push.size() : shader.program->stencilReferenceDword * sizeof(std::uint32_t); + if (pushDataBytes != 0u) { const auto position = shader.program->memoryOffsetDword * 4u + element; - Require(position < push.size(), "guest buffer offset lies outside the shader's push constants"); + Require(position < pushDataBytes, "guest buffer offset lies outside the shader's push constants"); allocations[index].pushByte = static_cast(shader.pushConstantOffset + position); } else { allocations[index].dataByte = shader.program->memoryOffsetDword * 4u + element; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp index 3612fae6ef..2f150274b0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp @@ -369,6 +369,29 @@ int main() { catch (const std::exception& error) { faultThrown = std::string(error.what()).find("BDA") != std::string::npos; } Require(faultThrown, "poisoned readonly SRT replay did not throw a host fault"); Require(std::ranges::all_of(Pixels, [](std::byte value) { return value == Kept; }), "poisoned replay published guest color results before its fault"); + alignas(256) std::array columnValues{}; + alignas(256) std::array columnTexels{}; + columnValues[0] = 0x2fu; + for (std::uint32_t x = 0; x < Width; ++x) { + columnValues[x + 1u] = 0x30u + x; + columnTexels[x] = 1.0f; + } + constexpr std::array bufferImageCode{ + 0x7e060f00u, 0x7e080280u, 0xf0001108u, 0x00010503u, 0xe0302000u, 0x80000603u, 0xe0302000u, 0x80030803u, + 0xbf8c3f70u, 0x7e0e02f2u, 0xf8000082u, 0x00000600u, 0xf800180fu, 0x07050805u, 0xbf810000u, + }; + const auto columnBuffer = BufferDescriptor(columnValues.data() + 1, 4u, Width); + const auto alignedBuffer = BufferDescriptor(columnValues.data(), 4u, Width + 1u); + const auto texelAddress = static_cast(reinterpret_cast(columnTexels.data())); + const std::array columnTexture{static_cast(texelAddress >> 8u), static_cast((texelAddress >> 40u) & 0xffu) | (22u << 20u) | (((Width - 1u) & 3u) << 30u), (Width - 1u) >> 2u, 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; + std::vector bufferImageData(columnBuffer.begin(), columnBuffer.end()); + bufferImageData.insert(bufferImageData.end(), columnTexture.begin(), columnTexture.end()); + bufferImageData.insert(bufferImageData.end(), alignedBuffer.begin(), alignedBuffer.end()); + Draw(*device, PlainPixelCode, plain, replace(0u)); + Draw(*device, bufferImageCode, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, bufferImageData); + Check(all, "a replayed export beside an image draws the image's texels"); + Draw(*device, PlainPixelCode, plain, equal(0x35u)); + Check([](std::uint32_t x, std::uint32_t) { return x == 5u; }, "a replayed export beside an image reads its guest buffer at a 4-byte offset"); std::puts("stencil replay tests passed"); return 0; } catch (const std::exception& error) { From 15803c7f1a3a2323459399bcaab0fe405931abbb Mon Sep 17 00:00:00 2001 From: battleblob1 Date: Thu, 22 Oct 2026 00:40:11 +0300 Subject: [PATCH 04/63] test(agc): replay a stencil export whose shader data is in a buffer Draw takes an optional fragment push range size. The new case sets it to the stencil reference dword, so the shader data of a pixel shader that reads its color with s_buffer_load_dword through a buffer descriptor in user data is in a buffer with or without the pipeline library, and the reference is the only push constant. The replayed draw must color every pixel from the buffer. Without the buildPrepare fix before it, both stencil replay registrations refuse that draw with "guest buffer offset lies outside the shader's push constants". Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/StencilReplay.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp index 2f150274b0..14d571cf48 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilReplay.cpp @@ -88,7 +88,7 @@ ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) { return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); } -void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function& alter = {}, const std::function& alterDraw = {}, std::span> vertexData = Vertices, std::span vertexCode = VertexCode, std::span pixelData = {}, std::span pixelSrt = {}, const std::function& alterVertex = {}) { +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function& alter = {}, const std::function& alterDraw = {}, std::span> vertexData = Vertices, std::span vertexCode = VertexCode, std::span pixelData = {}, std::span pixelSrt = {}, const std::function& alterVertex = {}, std::uint32_t pixelPushBytes = 0u) { Pixels.fill(Kept); auto target = device.Target(); std::vector capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()); @@ -113,11 +113,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::span pixelC auto pixelUserData = pixelData.empty() ? std::vector(8, 0u) : std::vector(pixelData.begin(), pixelData.end()); std::vector pixelMemory{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}; pixelMemory.insert(pixelMemory.end(), pixelSrt.begin(), pixelSrt.end()); + auto pixelLayout = PixelPushLayout(vertexPush, target); + if (pixelPushBytes != 0u) pixelLayout.pushConstantSizeBytes = pixelPushBytes; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {WaveSize, 0, pixelUserData, std::nullopt, PixelInfo(shaderControl), std::nullopt, pixelMemory}, target, - PixelPushLayout(vertexPush, target) + pixelLayout }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); @@ -350,6 +352,14 @@ int main() { const std::array colorData{0x3f800000u}; Draw(*device, userColor, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, colorData); Check(all, "dynamic reference preserves the adjacent shader-data push payload"); + auto bufferColor = std::vector(alternating.begin(), alternating.end()); + *std::find(bufferColor.begin(), bufferColor.end(), 0x7e0e02f2u) = 0x7e0e0208u; + bufferColor.insert(bufferColor.begin(), {0xf4200200u, 0xfa000000u, 0xbf8cc07fu}); + alignas(256) const std::array colorBuffer{0x3f800000u, 0u, 0u, 0u}; + const auto colorDescriptor = BufferDescriptor(colorBuffer.data(), 4u, static_cast(colorBuffer.size())); + const std::array colorMemory{{{reinterpret_cast(colorBuffer.data()), std::as_bytes(std::span(colorBuffer))}}}; + Draw(*device, bufferColor, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, colorDescriptor, colorMemory, {}, sizeof(std::uint32_t)); + Check(all, "dynamic reference keeps guest buffer offsets in shader data outside the push constants"); auto guarded = std::vector(alternating.begin(), alternating.end()); *std::find(guarded.begin(), guarded.end(), 0x7e0e02f2u) = 0x7e0e020cu; guarded.insert(guarded.begin(), {0xf4040280u, 0xfa000018u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu}); From 899306f8558bf50a97825befd6e46f63eeaefcce Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 14:47:21 +0100 Subject: [PATCH 05/63] test(libc): cover the remaining sizes of the Dinkumware typed atomics guest_dinkumware_atomics called one or two sizes of each typed _Atomic_ function. It now also runs the other sizes: store 2 and 4, load 1, exchange 1 and 2, strong compare-exchange 1, 4 and 8 and weak 1 and 2 (a failure writes the observed value back, then a success swaps), fetch_sub 1 and 8, fetch_and 1, 2 and 8, fetch_or 2, 4 and 8, fetch_xor 1, 2 and 4 (each returns the previous value, with wraparound), and is_lock_free 1, 2 and 8. --- core/libs/tests/GuestDinkumwareAtomics.cpp | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/core/libs/tests/GuestDinkumwareAtomics.cpp b/core/libs/tests/GuestDinkumwareAtomics.cpp index 8e45d76902..8adb814bad 100644 --- a/core/libs/tests/GuestDinkumwareAtomics.cpp +++ b/core/libs/tests/GuestDinkumwareAtomics.cpp @@ -5,6 +5,34 @@ #include #include +extern "C" { +void APS5_VABI _Atomic_store_2_nid_postfix(volatile std::uint16_t*, std::uint16_t, int); +void APS5_VABI _Atomic_store_4_nid_postfix(volatile std::uint32_t*, std::uint32_t, int); +std::uint8_t APS5_VABI _Atomic_load_1_nid_postfix(volatile std::uint8_t*, int); +std::uint8_t APS5_VABI _Atomic_exchange_1_nid_postfix(volatile std::uint8_t*, std::uint8_t, int); +std::uint16_t APS5_VABI _Atomic_exchange_2_nid_postfix(volatile std::uint16_t*, std::uint16_t, int); +int APS5_VABI _Atomic_compare_exchange_strong_1_nid_postfix(volatile std::uint8_t*, std::uint8_t*, std::uint8_t, int, int); +int APS5_VABI _Atomic_compare_exchange_strong_4_nid_postfix(volatile std::uint32_t*, std::uint32_t*, std::uint32_t, int, int); +int APS5_VABI _Atomic_compare_exchange_strong_8_nid_postfix(volatile std::uint64_t*, std::uint64_t*, std::uint64_t, int, int); +int APS5_VABI _Atomic_compare_exchange_weak_1_nid_postfix(volatile std::uint8_t*, std::uint8_t*, std::uint8_t, int, int); +int APS5_VABI _Atomic_compare_exchange_weak_2_nid_postfix(volatile std::uint16_t*, std::uint16_t*, std::uint16_t, int, int); +std::uint16_t APS5_VABI _Atomic_fetch_add_2_nid_postfix(volatile std::uint16_t*, std::uint16_t, int); +std::uint8_t APS5_VABI _Atomic_fetch_sub_1_nid_postfix(volatile std::uint8_t*, std::uint8_t, int); +std::uint64_t APS5_VABI _Atomic_fetch_sub_8_nid_postfix(volatile std::uint64_t*, std::uint64_t, int); +std::uint8_t APS5_VABI _Atomic_fetch_and_1_nid_postfix(volatile std::uint8_t*, std::uint8_t, int); +std::uint16_t APS5_VABI _Atomic_fetch_and_2_nid_postfix(volatile std::uint16_t*, std::uint16_t, int); +std::uint64_t APS5_VABI _Atomic_fetch_and_8_nid_postfix(volatile std::uint64_t*, std::uint64_t, int); +std::uint16_t APS5_VABI _Atomic_fetch_or_2_nid_postfix(volatile std::uint16_t*, std::uint16_t, int); +std::uint32_t APS5_VABI _Atomic_fetch_or_4_nid_postfix(volatile std::uint32_t*, std::uint32_t, int); +std::uint64_t APS5_VABI _Atomic_fetch_or_8_nid_postfix(volatile std::uint64_t*, std::uint64_t, int); +std::uint8_t APS5_VABI _Atomic_fetch_xor_1_nid_postfix(volatile std::uint8_t*, std::uint8_t, int); +std::uint16_t APS5_VABI _Atomic_fetch_xor_2_nid_postfix(volatile std::uint16_t*, std::uint16_t, int); +std::uint32_t APS5_VABI _Atomic_fetch_xor_4_nid_postfix(volatile std::uint32_t*, std::uint32_t, int); +int APS5_VABI _Atomic_is_lock_free_1_nid_postfix(); +int APS5_VABI _Atomic_is_lock_free_2_nid_postfix(); +int APS5_VABI _Atomic_is_lock_free_8_nid_postfix(); +} + extern "C" { void APS5_VABI _Atomic_store_1_nid_postfix(volatile std::uint8_t*, std::uint8_t, int); void APS5_VABI _Atomic_store_8_nid_postfix(volatile std::uint64_t*, std::uint64_t, int); @@ -35,6 +63,50 @@ static void Require(bool value) { if (!value) std::abort(); } constexpr int Relaxed = 0, Acquire = 2, Release = 3, AcqRel = 4, SeqCst = 5; +static void CheckRemainingTyped() { + std::uint8_t byte = 0; + Require(_Atomic_load_1_nid_postfix(&byte, SeqCst) == 0); + Require(_Atomic_exchange_1_nid_postfix(&byte, 0xa5, SeqCst) == 0 && byte == 0xa5); + std::uint8_t expectedByte = 0; + Require(_Atomic_compare_exchange_strong_1_nid_postfix(&byte, &expectedByte, 1, SeqCst, SeqCst) == 0 && expectedByte == 0xa5 && byte == 0xa5); + Require(_Atomic_compare_exchange_strong_1_nid_postfix(&byte, &expectedByte, 0x3c, SeqCst, SeqCst) == 1 && byte == 0x3c); + expectedByte = 0; + Require(_Atomic_compare_exchange_weak_1_nid_postfix(&byte, &expectedByte, 1, SeqCst, Relaxed) == 0 && expectedByte == 0x3c); + Require(_Atomic_compare_exchange_weak_1_nid_postfix(&byte, &expectedByte, 0x0f, SeqCst, Relaxed) == 1 && byte == 0x0f); + Require(_Atomic_fetch_sub_1_nid_postfix(&byte, 0x10, SeqCst) == 0x0f && byte == 0xff); + Require(_Atomic_fetch_and_1_nid_postfix(&byte, 0x3c, Relaxed) == 0xff && byte == 0x3c); + Require(_Atomic_fetch_xor_1_nid_postfix(&byte, 0xff, AcqRel) == 0x3c && byte == 0xc3); + + std::uint16_t half = 0; + _Atomic_store_2_nid_postfix(&half, 0x1234, Release); + Require(half == 0x1234 && _Atomic_exchange_2_nid_postfix(&half, 0xfff0, SeqCst) == 0x1234 && half == 0xfff0); + Require(_Atomic_fetch_add_2_nid_postfix(&half, 0x20, SeqCst) == 0xfff0 && half == 0x0010); + Require(_Atomic_fetch_and_2_nid_postfix(&half, 0x0018, SeqCst) == 0x0010 && half == 0x0010); + Require(_Atomic_fetch_or_2_nid_postfix(&half, 0x0101, SeqCst) == 0x0010 && half == 0x0111); + Require(_Atomic_fetch_xor_2_nid_postfix(&half, 0x0110, SeqCst) == 0x0111 && half == 0x0001); + std::uint16_t expectedHalf = 2; + Require(_Atomic_compare_exchange_weak_2_nid_postfix(&half, &expectedHalf, 9, SeqCst, Relaxed) == 0 && expectedHalf == 1); + Require(_Atomic_compare_exchange_weak_2_nid_postfix(&half, &expectedHalf, 9, SeqCst, Relaxed) == 1 && half == 9); + + std::uint32_t word = 0; + _Atomic_store_4_nid_postfix(&word, 0x00ff00ffu, SeqCst); + Require(word == 0x00ff00ffu && _Atomic_fetch_or_4_nid_postfix(&word, 0xff000000u, SeqCst) == 0x00ff00ffu && word == 0xffff00ffu); + Require(_Atomic_fetch_xor_4_nid_postfix(&word, 0xffffffffu, SeqCst) == 0xffff00ffu && word == 0x0000ff00u); + std::uint32_t expectedWord = 0; + Require(_Atomic_compare_exchange_strong_4_nid_postfix(&word, &expectedWord, 1, SeqCst, SeqCst) == 0 && expectedWord == 0x0000ff00u); + Require(_Atomic_compare_exchange_strong_4_nid_postfix(&word, &expectedWord, 1, SeqCst, SeqCst) == 1 && word == 1); + + std::uint64_t wide = 0x8000000000000001ull; + Require(_Atomic_fetch_sub_8_nid_postfix(&wide, 2, SeqCst) == 0x8000000000000001ull && wide == 0x7fffffffffffffffull); + Require(_Atomic_fetch_and_8_nid_postfix(&wide, 0xffffffff00000000ull, SeqCst) == 0x7fffffffffffffffull && wide == 0x7fffffff00000000ull); + Require(_Atomic_fetch_or_8_nid_postfix(&wide, 0xffull, SeqCst) == 0x7fffffff00000000ull && wide == 0x7fffffff000000ffull); + std::uint64_t expectedWide = 0; + Require(_Atomic_compare_exchange_strong_8_nid_postfix(&wide, &expectedWide, 5, SeqCst, SeqCst) == 0 && expectedWide == 0x7fffffff000000ffull); + Require(_Atomic_compare_exchange_strong_8_nid_postfix(&wide, &expectedWide, 5, SeqCst, SeqCst) == 1 && wide == 5); + + Require(_Atomic_is_lock_free_1_nid_postfix() == 1 && _Atomic_is_lock_free_2_nid_postfix() == 1 && _Atomic_is_lock_free_8_nid_postfix() == 1); +} + static void CheckTyped() { std::uint8_t byte = 0; _Atomic_store_1_nid_postfix(&byte, 7, SeqCst); @@ -112,6 +184,7 @@ static void CheckConcurrentGeneric() { } int main() { + CheckRemainingTyped(); CheckTyped(); CheckFlagsAndFences(); CheckGeneric(); From accae4b62dd305f636bd10670844fb181a8e955a Mon Sep 17 00:00:00 2001 From: m335671 <110462702+m335671@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:10:59 +0200 Subject: [PATCH 06/63] fix(agc): re-check a dispatch resolved ahead against memory before adopting it Group capture resolves the dispatches behind a gate ahead and parks the result per packet. A CPU store or a GPU write queued on another queue between the resolve and the adoption was not seen, so the dispatch ran with the descriptors and data it captured before. At adoption, reject the ahead result when a non-code region it captured has a pending write or no longer matches memory, and take the normal path instead. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/Driver.hpp | 2 + .../Execution/include/Driver/Driver.hpp | 6 +- .../src/Driver/Dispatch/Dispatch.cpp | 22 ++ .../src/Driver/Dispatch/DispatchAhead.cpp | 12 +- .../Execution/src/Driver/EntryPoints.cpp | 8 + .../execution/DispatchAheadAdoptCheck.cpp | 208 ++++++++++++++++++ 6 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DispatchAheadAdoptCheck.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index b599b88f95..5a63f230b6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -20,6 +20,8 @@ extern "C" void AgcDriverShutdown_nid_postfix(); extern "C" void AgcDriverRequestScreenshot_nid_postfix(); extern "C" void AgcDriverSuspendPoint_nid_postfix(); extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix(); +extern "C" std::uint64_t AgcDriverDispatchesRejectedAhead_nid_postfix(); +extern "C" void AgcDriverSetAheadAdoptHook_nid_postfix(void (*hook)()); extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader); extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive); extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 0e96bcc002..7c087b27c3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -51,6 +51,8 @@ public: void Shutdown(); void WaitIdle(); std::uint64_t DispatchesAdoptedAhead() const; + std::uint64_t DispatchesRejectedAhead() const; + void SetAheadAdoptHook(void (*hook)()); void CheckFailure(); void ReportFailure(std::exception_ptr error); void Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip = false); @@ -289,7 +291,7 @@ private: std::chrono::steady_clock::time_point resolvedAt{}; }; struct GroupCaptureStats { - std::uint64_t runs = 0, resolved = 0, resolvedHits = 0, adopted = 0, addressMismatches = 0, keyMismatches = 0, failures = 0, stale = 0, labelOverlaps = 0, writtenBefore = 0, deviceRejects = 0, argumentMismatches = 0; + std::uint64_t runs = 0, resolved = 0, resolvedHits = 0, adopted = 0, addressMismatches = 0, keyMismatches = 0, memoryMismatches = 0, failures = 0, stale = 0, labelOverlaps = 0, writtenBefore = 0, deviceRejects = 0, argumentMismatches = 0; double resolveMs = 0, resolveMaxMs = 0, ageMs = 0, ageMaxMs = 0; std::chrono::steady_clock::time_point reported = std::chrono::steady_clock::now(); }; @@ -438,6 +440,8 @@ private: std::atomic evidenceReads{0}; std::atomic evidenceValidations{0}; std::atomic dispatchesAdoptedAhead{0}; + std::atomic dispatchesRejectedAhead{0}; + std::atomic aheadAdoptHook{nullptr}; std::mutex writtenBuffersMutex; std::deque writtenBuffers; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 808000d470..428dd8d3a6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -16,6 +16,7 @@ #include "Optimization/ResourceProgram.hpp" #include #include +#include #include #include #include @@ -57,6 +58,19 @@ void ReportDispatchItems(bool profile) { std::fprintf(stderr, "[dispatch-item] %llu commit items (%llu recipes rebuilt on the committer); drained instead, cumulative:%s\n", static_cast(itemCommits.load(std::memory_order_relaxed)), static_cast(itemRebuilds.load(std::memory_order_relaxed)), drained.c_str()); } +bool CapturedCurrent(std::span captured, const ShaderSnapshot& code) { + for (const auto& region : captured) { + const auto address = region.guestAddress; + const auto bytes = region.bytes.size(); + if (bytes == 0 || (address >= code.codeAddress && address < code.codeAddress + code.code.size() * sizeof(std::uint32_t)) || (!code.header.empty() && address >= code.headerAddress && address < code.headerAddress + code.header.size())) continue; + if (Driver::QueryPendingSpan(address, bytes, {}) != ShaderMemory::PendingWrite::None || !GuestMemory::Accessible(reinterpret_cast(address), bytes)) return false; + GuestMemory::GuardRead(address, bytes); + GuestMemory::FlushGpuWrites(address, bytes); + if (std::memcmp(region.bytes.data(), reinterpret_cast(address), bytes) != 0) return false; + } + return true; +} + } bool Driver::pipelineDispatch() { @@ -281,6 +295,14 @@ void Driver::dispatch(QueueState& queue, std::span packet, fromAhead = false; ++groupStats.keyMismatches; } + if (fromAhead) { + if (const auto hook = aheadAdoptHook.load(std::memory_order_acquire)) hook(); + if (!CapturedCurrent(ahead.captured, snapshot)) { + fromAhead = false; + ++dispatchesRejectedAhead; + ++groupStats.memoryMismatches; + } + } phaseTiming.Phase(PhaseKey); timing.Mark("decode_key_device_setup"); bool readsClear = false; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index 0b6f1f0eb0..8bf29a55b6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -39,6 +39,14 @@ std::uint64_t Driver::DispatchesAdoptedAhead() const { return dispatchesAdoptedAhead.load(std::memory_order_relaxed); } +std::uint64_t Driver::DispatchesRejectedAhead() const { + return dispatchesRejectedAhead.load(std::memory_order_relaxed); +} + +void Driver::SetAheadAdoptHook(void (*hook)()) { + aheadAdoptHook.store(hook, std::memory_order_release); +} + void Driver::clearResolvedAhead() { auto& resolved = resolvedAhead(); if (resolved.empty()) return; @@ -207,9 +215,9 @@ void Driver::reportGroupCapture(std::uint32_t queue) { auto& stats = groupCaptureStats(); const auto now = std::chrono::steady_clock::now(); if (now - stats.reported < std::chrono::seconds(10)) return; - AgcDriver::ProfilePrint_nid_no_patch("[groupcap] queue 0x%x (10 s): %llu runs resolved %llu dispatches (%llu cache hits) in %.1f ms (max %.2f ms); %llu adopted %.2f ms after the resolve on average, max %.2f ms; mismatches: address %llu key %llu; %llu resolve failures, %llu stale, %llu label overlaps, %llu read an earlier dispatch's output, %llu rejected by the device, %llu dropped because the argument re-read differed\n", + AgcDriver::ProfilePrint_nid_no_patch("[groupcap] queue 0x%x (10 s): %llu runs resolved %llu dispatches (%llu cache hits) in %.1f ms (max %.2f ms); %llu adopted %.2f ms after the resolve on average, max %.2f ms; mismatches: address %llu key %llu memory %llu; %llu resolve failures, %llu stale, %llu label overlaps, %llu read an earlier dispatch's output, %llu rejected by the device, %llu dropped because the argument re-read differed\n", queue, static_cast(stats.runs), static_cast(stats.resolved), static_cast(stats.resolvedHits), stats.resolveMs, stats.resolveMaxMs, - static_cast(stats.adopted), stats.adopted != 0 ? stats.ageMs / static_cast(stats.adopted) : 0.0, stats.ageMaxMs, static_cast(stats.addressMismatches), static_cast(stats.keyMismatches), + static_cast(stats.adopted), stats.adopted != 0 ? stats.ageMs / static_cast(stats.adopted) : 0.0, stats.ageMaxMs, static_cast(stats.addressMismatches), static_cast(stats.keyMismatches), static_cast(stats.memoryMismatches), static_cast(stats.failures), static_cast(stats.stale), static_cast(stats.labelOverlaps), static_cast(stats.writtenBefore), static_cast(stats.deviceRejects), static_cast(stats.argumentMismatches)); stats = GroupCaptureStats{}; stats.reported = now; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 04a1f72a02..4a2e1aa502 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -116,6 +116,14 @@ extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix() { return AgcDriver::DriverDetail::Driver::Get().DispatchesAdoptedAhead(); } +extern "C" std::uint64_t AgcDriverDispatchesRejectedAhead_nid_postfix() { + return AgcDriver::DriverDetail::Driver::Get().DispatchesRejectedAhead(); +} + +extern "C" void AgcDriverSetAheadAdoptHook_nid_postfix(void (*hook)()) { + AgcDriver::DriverDetail::Driver::Get().SetAheadAdoptHook(hook); +} + extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output) { AgcDriver::RegisterVideoOutput(handle, output); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchAheadAdoptCheck.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchAheadAdoptCheck.cpp new file mode 100644 index 0000000000..e2453a66f0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchAheadAdoptCheck.cpp @@ -0,0 +1,208 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array StoreCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0xe0700000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array SpinCode{0xf4080200u, 0xfa000000u, 0xbe840381u, 0x9304ff04u, 0x0019660du, 0x8004ff04u, 0x3c6ef35fu, 0x80828102u, 0xbf078002u, 0xbf85fff9u, 0xbf8cc07fu, 0x7e000204u, 0xe0700000u, 0x80020000u, 0xbf810000u}; + +constexpr std::uint32_t Compute = 0x20u; +constexpr std::uint32_t OtherCompute = 0x28u; +constexpr std::uint32_t SpinIterations = 1u << 20u; + +alignas(256) std::array Stale{}; +alignas(256) std::array Fresh{}; +alignas(256) std::array Target{}; +alignas(256) std::array TargetDescriptor{}; +alignas(256) std::array Spun{}; +alignas(256) std::array SpunDescriptor{}; +alignas(256) volatile std::uint32_t Gate = 1; +alignas(256) volatile std::uint32_t Done = 0; +alignas(256) volatile std::uint32_t OtherDone = 0; + +enum class Window { None, CpuStore, QueuedGpuStore }; + +std::atomic Armed{Window::None}; +std::uint32_t SpinDispatches = 16; +std::vector OtherWords; +bool SawPending = false; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +void Dispatch(std::vector& words, const void* code, std::initializer_list userData) { + Program(words, code, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); +} + +void WriteData(std::vector& words, const volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(target), High(target), value}); +} + +void Submit(std::uint32_t queue, std::vector& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(queue, &packet) == 0, "compute submit failed"); +} + +void QueueStoreAhead() { + OtherWords.clear(); + for (std::uint32_t index = 0; index < SpinDispatches; ++index) Dispatch(OtherWords, SpinCode.data(), {Low(SpunDescriptor.data()), High(SpunDescriptor.data()), SpinIterations}); + Dispatch(OtherWords, StoreCode.data(), {Low(TargetDescriptor.data()), High(TargetDescriptor.data()), Low(Fresh.data())}); + WriteData(OtherWords, &OtherDone, 1); + Submit(OtherCompute, OtherWords); + const auto start = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() - start < std::chrono::seconds(5) && OtherDone == 0) { + if (AgcDriver::DriverDetail::Driver::QueryPendingSpan(reinterpret_cast(Target.data()), sizeof(Target), {}) != AgcDriver::ShaderMemory::PendingWrite::None) { + SawPending = true; + return; + } + std::this_thread::yield(); + } +} + +void BeforeAdoption() { + const auto window = Armed.exchange(Window::None); + if (window == Window::CpuStore) reinterpret_cast(Target[0]) = Low(Fresh.data()); + else if (window == Window::QueuedGpuStore) QueueStoreAhead(); +} + +void WaitIdle() { + AgcDriverWaitIdle_nid_postfix(); + for (const auto* buffer : {&Stale, &Fresh, &Target}) AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(buffer->data()), sizeof(*buffer)); +} + +void Reset() { + WaitIdle(); + Stale = {}; + Fresh = {}; + Target = Descriptor(Stale.data(), sizeof(Stale)); + Done = 0; + OtherDone = 0; + SawPending = false; +} + +void Gated(Window window) { + Reset(); + std::vector words; + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + Dispatch(words, CounterCode.data(), {Low(Target.data()), High(Target.data())}); + WriteData(words, &Done, 1); + Armed = window; + Submit(Compute, words); + WaitIdle(); + Armed = Window::None; + Require(Done == 1, "the gated submission did not finish"); +} + +std::string Counts() { + return "(stale " + std::to_string(Stale[0]) + ", fresh " + std::to_string(Fresh[0]) + ")"; +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + Require(High(Stale.data()) == High(Fresh.data()), "the stale and fresh buffers differ in their high address word"); + TargetDescriptor = Descriptor(Target.data(), sizeof(Target)); + SpunDescriptor = Descriptor(Spun.data(), sizeof(Spun)); + AgcDriverSetAheadAdoptHook_nid_postfix(&BeforeAdoption); + Reset(); + std::vector warmUp; + Dispatch(warmUp, CounterCode.data(), {Low(Target.data()), High(Target.data())}); + WriteData(warmUp, &Done, 1); + Submit(Compute, warmUp); + std::vector otherWarmUp; + Dispatch(otherWarmUp, SpinCode.data(), {Low(SpunDescriptor.data()), High(SpunDescriptor.data()), SpinIterations}); + Dispatch(otherWarmUp, StoreCode.data(), {Low(TargetDescriptor.data()), High(TargetDescriptor.data()), Low(Fresh.data())}); + Submit(OtherCompute, otherWarmUp); + WaitIdle(); + Require(Done == 1 && Target[0] == Low(Fresh.data()), "the warm-up dispatches did not run"); + + Gated(Window::None); + Require(Stale[0] == 1 && Fresh[0] == 0, "the gated dispatch without a store in the window did not count into the stale buffer " + Counts()); + const auto adopted = AgcDriverDispatchesAdoptedAhead_nid_postfix(); + Require(adopted != 0, "the gated dispatch was not resolved ahead and adopted"); + + auto rejected = AgcDriverDispatchesRejectedAhead_nid_postfix(); + Gated(Window::CpuStore); + Require(Fresh[0] == 1 && Stale[0] == 0, "a dispatch resolved ahead whose descriptor the CPU replaced before its adoption used the old descriptor " + Counts()); + Require(AgcDriverDispatchesRejectedAhead_nid_postfix() == rejected + 1, "the CPU store in the adoption window was not caught by the adoption check"); + std::cout << "cpu store before adoption: passed\n"; + + bool conclusive = false; + for (int attempt = 0; attempt < 4 && !conclusive; ++attempt, SpinDispatches *= 4) { + rejected = AgcDriverDispatchesRejectedAhead_nid_postfix(); + Gated(Window::QueuedGpuStore); + conclusive = SawPending; + if (!conclusive) { + std::cout << "queued gpu store before adoption: the store finished before the adoption with " << SpinDispatches << " spin dispatches, retrying\n"; + continue; + } + Require(Fresh[0] == 1 && Stale[0] == 0, "a dispatch resolved ahead whose descriptor a GPU store queued on another queue replaces before its adoption used the old descriptor " + Counts()); + Require(AgcDriverDispatchesRejectedAhead_nid_postfix() == rejected + 1, "the queued GPU store in the adoption window was not caught by the adoption check"); + } + AgcDriverSetAheadAdoptHook_nid_postfix(nullptr); + if (!conclusive) { + std::cout << "queued gpu store before adoption: never observed the store pending at the adoption\n"; + AgcDriverShutdown_nid_postfix(); + return VulkanTestSkipped; + } + std::cout << "queued gpu store before adoption: passed\n"; + std::cout << "dispatch ahead adopt check test passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} From 30707fb9c5d42ba747736f01a08764bd4ad04103 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:38:23 +0100 Subject: [PATCH 07/63] feat(libSceFreeTypeOt): declare 150 PS5 3.20 exports --- core/libs/prx/libSceFreeTypeOt/Export.cpp | 751 +++++++++++++++++++++- docs/dev/debt/libSceFreeTypeOt.md | 5 + 2 files changed, 755 insertions(+), 1 deletion(-) create mode 100644 docs/dev/debt/libSceFreeTypeOt.md diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index d81f4a5e32..1412320d5a 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -5,6 +5,755 @@ extern "C" { -APS5_DUMMY_FUN + +int APS5_VABI sceFreeTypeOtUnknown_UD7IFu7ML2U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("UD7IFu7ML2U", sceFreeTypeOtUnknown_UD7IFu7ML2U_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_2T7lbS_CSzA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2T7lbS+CSzA", sceFreeTypeOtUnknown_2T7lbS_CSzA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_MGEIQsujKaY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("MGEIQsujKaY", sceFreeTypeOtUnknown_MGEIQsujKaY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_mkmgcRnAJog_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mkmgcRnAJog", sceFreeTypeOtUnknown_mkmgcRnAJog_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_m18bTUNF_0g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("m18bTUNF-0g", sceFreeTypeOtUnknown_m18bTUNF_0g_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_vTZ34rw1sfg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("vTZ34rw1sfg", sceFreeTypeOtUnknown_vTZ34rw1sfg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_PvCNusrp9xQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("PvCNusrp9xQ", sceFreeTypeOtUnknown_PvCNusrp9xQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_dlM4ASZdcjk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dlM4ASZdcjk", sceFreeTypeOtUnknown_dlM4ASZdcjk_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Ev3BqJ_U1Fg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Ev3BqJ-U1Fg", sceFreeTypeOtUnknown_Ev3BqJ_U1Fg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_ZhtAXOoLNyo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ZhtAXOoLNyo", sceFreeTypeOtUnknown_ZhtAXOoLNyo_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_73vg_nRH68g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("73vg+nRH68g", sceFreeTypeOtUnknown_73vg_nRH68g_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_wBKAYO7yEXw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("wBKAYO7yEXw", sceFreeTypeOtUnknown_wBKAYO7yEXw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_nSIux4OQyJM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("nSIux4OQyJM", sceFreeTypeOtUnknown_nSIux4OQyJM_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Dr3F3qrSQ2c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Dr3F3qrSQ2c", sceFreeTypeOtUnknown_Dr3F3qrSQ2c_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_avLzN0tTxLI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("avLzN0tTxLI", sceFreeTypeOtUnknown_avLzN0tTxLI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_0rKmI1ZbMAc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0rKmI1ZbMAc", sceFreeTypeOtUnknown_0rKmI1ZbMAc_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_BBRhYfYBDgU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BBRhYfYBDgU", sceFreeTypeOtUnknown_BBRhYfYBDgU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_c1nbr8oSWlg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("c1nbr8oSWlg", sceFreeTypeOtUnknown_c1nbr8oSWlg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_AVEEb57HhUw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("AVEEb57HhUw", sceFreeTypeOtUnknown_AVEEb57HhUw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_x8y0qzLiYOU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("x8y0qzLiYOU", sceFreeTypeOtUnknown_x8y0qzLiYOU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_VW4GD_TaIUc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("VW4GD-TaIUc", sceFreeTypeOtUnknown_VW4GD_TaIUc_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_GiuNQjoj4fs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("GiuNQjoj4fs", sceFreeTypeOtUnknown_GiuNQjoj4fs_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_5qqbXOGlHw4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5qqbXOGlHw4", sceFreeTypeOtUnknown_5qqbXOGlHw4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4LY5pyZ5IQk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4LY5pyZ5IQk", sceFreeTypeOtUnknown_4LY5pyZ5IQk_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_zE0V_0aouZ4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zE0V+0aouZ4", sceFreeTypeOtUnknown_zE0V_0aouZ4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_LsyswTUNTRs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LsyswTUNTRs", sceFreeTypeOtUnknown_LsyswTUNTRs_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_AfL_nqNC5fY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("AfL-nqNC5fY", sceFreeTypeOtUnknown_AfL_nqNC5fY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_WIvTHYs7ZQQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("WIvTHYs7ZQQ", sceFreeTypeOtUnknown_WIvTHYs7ZQQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_yrtsLmTfa48_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yrtsLmTfa48", sceFreeTypeOtUnknown_yrtsLmTfa48_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_tCMhTh7h8D4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tCMhTh7h8D4", sceFreeTypeOtUnknown_tCMhTh7h8D4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_zyezaHweY3U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zyezaHweY3U", sceFreeTypeOtUnknown_zyezaHweY3U_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_C_PpdS5zris_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("C-PpdS5zris", sceFreeTypeOtUnknown_C_PpdS5zris_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_g8eLxL9VAhU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("g8eLxL9VAhU", sceFreeTypeOtUnknown_g8eLxL9VAhU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_OeNH41MA2Vs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("OeNH41MA2Vs", sceFreeTypeOtUnknown_OeNH41MA2Vs_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Bhm8Injl0KA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Bhm8Injl0KA", sceFreeTypeOtUnknown_Bhm8Injl0KA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown___lgV9pRsO4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("++lgV9pRsO4", sceFreeTypeOtUnknown___lgV9pRsO4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_BqGJsJcFYaM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BqGJsJcFYaM", sceFreeTypeOtUnknown_BqGJsJcFYaM_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_umIqupy8UU0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("umIqupy8UU0", sceFreeTypeOtUnknown_umIqupy8UU0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_l6Cf9rb9fZQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("l6Cf9rb9fZQ", sceFreeTypeOtUnknown_l6Cf9rb9fZQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_BgUMBGvhATU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BgUMBGvhATU", sceFreeTypeOtUnknown_BgUMBGvhATU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Pz262plj20w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Pz262plj20w", sceFreeTypeOtUnknown_Pz262plj20w_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_sKneJjUXWkQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("sKneJjUXWkQ", sceFreeTypeOtUnknown_sKneJjUXWkQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_0Aal_TIC4so_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Aal-TIC4so", sceFreeTypeOtUnknown_0Aal_TIC4so_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4RRjK_Wd1lU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4RRjK-Wd1lU", sceFreeTypeOtUnknown_4RRjK_Wd1lU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_weVsBCT_TTQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("weVsBCT+TTQ", sceFreeTypeOtUnknown_weVsBCT_TTQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_QH2emcgeFck_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("QH2emcgeFck", sceFreeTypeOtUnknown_QH2emcgeFck_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_NG7OgYsgcF8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("NG7OgYsgcF8", sceFreeTypeOtUnknown_NG7OgYsgcF8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_9km02mPqPJE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("9km02mPqPJE", sceFreeTypeOtUnknown_9km02mPqPJE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_0Lv6K1d47jg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0Lv6K1d47jg", sceFreeTypeOtUnknown_0Lv6K1d47jg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_8fAMdZCnewA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8fAMdZCnewA", sceFreeTypeOtUnknown_8fAMdZCnewA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_LsB538_IPrE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LsB538+IPrE", sceFreeTypeOtUnknown_LsB538_IPrE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_hz_J3hohXW8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hz-J3hohXW8", sceFreeTypeOtUnknown_hz_J3hohXW8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_KVbmKCGGowg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KVbmKCGGowg", sceFreeTypeOtUnknown_KVbmKCGGowg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_hF3stKUWbWg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("hF3stKUWbWg", sceFreeTypeOtUnknown_hF3stKUWbWg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_p5GNJaXO2yE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("p5GNJaXO2yE", sceFreeTypeOtUnknown_p5GNJaXO2yE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_8cnVx1yH9_I_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8cnVx1yH9-I", sceFreeTypeOtUnknown_8cnVx1yH9_I_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_HVSaZbxYi2s_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HVSaZbxYi2s", sceFreeTypeOtUnknown_HVSaZbxYi2s_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_qAPXM5DN62Q_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("qAPXM5DN62Q", sceFreeTypeOtUnknown_qAPXM5DN62Q_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_5ipBcLYVc2k_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("5ipBcLYVc2k", sceFreeTypeOtUnknown_5ipBcLYVc2k_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_8A0cAU4yBVY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8A0cAU4yBVY", sceFreeTypeOtUnknown_8A0cAU4yBVY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_z9Wsaf34JA4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("z9Wsaf34JA4", sceFreeTypeOtUnknown_z9Wsaf34JA4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_PaDk9bdmdhU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("PaDk9bdmdhU", sceFreeTypeOtUnknown_PaDk9bdmdhU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Z8KG6700sw4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Z8KG6700sw4", sceFreeTypeOtUnknown_Z8KG6700sw4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_uK7vGxAQVxM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uK7vGxAQVxM", sceFreeTypeOtUnknown_uK7vGxAQVxM_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_zetC8Tp1S8w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("zetC8Tp1S8w", sceFreeTypeOtUnknown_zetC8Tp1S8w_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_DAmwG6U7YIc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("DAmwG6U7YIc", sceFreeTypeOtUnknown_DAmwG6U7YIc_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Yu9oXRVmTTo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Yu9oXRVmTTo", sceFreeTypeOtUnknown_Yu9oXRVmTTo_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_ihIHD8BTNxI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ihIHD8BTNxI", sceFreeTypeOtUnknown_ihIHD8BTNxI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_yCEJIsjIOO8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("yCEJIsjIOO8", sceFreeTypeOtUnknown_yCEJIsjIOO8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_L1uHkWTazkI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("L1uHkWTazkI", sceFreeTypeOtUnknown_L1uHkWTazkI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_C7N2GuwqTA0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("C7N2GuwqTA0", sceFreeTypeOtUnknown_C7N2GuwqTA0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_JgtSc0g3G6E_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JgtSc0g3G6E", sceFreeTypeOtUnknown_JgtSc0g3G6E_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_gVf7xRLdWm8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("gVf7xRLdWm8", sceFreeTypeOtUnknown_gVf7xRLdWm8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_FHdeCFVf_f8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("FHdeCFVf-f8", sceFreeTypeOtUnknown_FHdeCFVf_f8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Qmgc0XACzRg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Qmgc0XACzRg", sceFreeTypeOtUnknown_Qmgc0XACzRg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_0rGx_oFMLyk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("0rGx-oFMLyk", sceFreeTypeOtUnknown_0rGx_oFMLyk_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_thA48N_0VwQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("thA48N-0VwQ", sceFreeTypeOtUnknown_thA48N_0VwQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_jl0VxYZb8_o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jl0VxYZb8-o", sceFreeTypeOtUnknown_jl0VxYZb8_o_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Er0Lin_u20o_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Er0Lin-u20o", sceFreeTypeOtUnknown_Er0Lin_u20o_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_KHj92a3WDn4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("KHj92a3WDn4", sceFreeTypeOtUnknown_KHj92a3WDn4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_tDssQCH2syY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("tDssQCH2syY", sceFreeTypeOtUnknown_tDssQCH2syY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_dXJK59vXRxs_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dXJK59vXRxs", sceFreeTypeOtUnknown_dXJK59vXRxs_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_BB4gl7SgLdk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("BB4gl7SgLdk", sceFreeTypeOtUnknown_BB4gl7SgLdk_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Fv8MWCUH8k8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Fv8MWCUH8k8", sceFreeTypeOtUnknown_Fv8MWCUH8k8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_RzgDJTjQst4_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RzgDJTjQst4", sceFreeTypeOtUnknown_RzgDJTjQst4_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4yt9cD1qcEI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4yt9cD1qcEI", sceFreeTypeOtUnknown_4yt9cD1qcEI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_u6Xquj1bFwc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("u6Xquj1bFwc", sceFreeTypeOtUnknown_u6Xquj1bFwc_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown__Xm1cLEpBec_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-Xm1cLEpBec", sceFreeTypeOtUnknown__Xm1cLEpBec_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_uDXTnGbAz44_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uDXTnGbAz44", sceFreeTypeOtUnknown_uDXTnGbAz44_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_iruPsrXCN7U_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("iruPsrXCN7U", sceFreeTypeOtUnknown_iruPsrXCN7U_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_a7RlARfexoo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("a7RlARfexoo", sceFreeTypeOtUnknown_a7RlARfexoo_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_jX1zq1i8KfI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jX1zq1i8KfI", sceFreeTypeOtUnknown_jX1zq1i8KfI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_U_0WKVAePww_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("U+0WKVAePww", sceFreeTypeOtUnknown_U_0WKVAePww_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_dlbnv6Wass8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dlbnv6Wass8", sceFreeTypeOtUnknown_dlbnv6Wass8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_cmd41395g7Y_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cmd41395g7Y", sceFreeTypeOtUnknown_cmd41395g7Y_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_SX5I_XzH8HI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("SX5I+XzH8HI", sceFreeTypeOtUnknown_SX5I_XzH8HI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Bmi8PdvsM3M_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Bmi8PdvsM3M", sceFreeTypeOtUnknown_Bmi8PdvsM3M_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_jJh4SMt8NFQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("jJh4SMt8NFQ", sceFreeTypeOtUnknown_jJh4SMt8NFQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_1z6bBg1qBfU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1z6bBg1qBfU", sceFreeTypeOtUnknown_1z6bBg1qBfU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_mF0B64O954g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mF0B64O954g", sceFreeTypeOtUnknown_mF0B64O954g_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_c002A4zTOgA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("c002A4zTOgA", sceFreeTypeOtUnknown_c002A4zTOgA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_mcT796Qgj_g_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("mcT796Qgj+g", sceFreeTypeOtUnknown_mcT796Qgj_g_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_b5gZp_r3WWI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("b5gZp-r3WWI", sceFreeTypeOtUnknown_b5gZp_r3WWI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_S_n0Md8Rjfw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("S+n0Md8Rjfw", sceFreeTypeOtUnknown_S_n0Md8Rjfw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_6a1p3QgXvo0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6a1p3QgXvo0", sceFreeTypeOtUnknown_6a1p3QgXvo0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_JFSCOXTEvfg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("JFSCOXTEvfg", sceFreeTypeOtUnknown_JFSCOXTEvfg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_xejRyEbsLcw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xejRyEbsLcw", sceFreeTypeOtUnknown_xejRyEbsLcw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_R1mZutnlTiM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("R1mZutnlTiM", sceFreeTypeOtUnknown_R1mZutnlTiM_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4J4Baf8ana8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4J4Baf8ana8", sceFreeTypeOtUnknown_4J4Baf8ana8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Az4H9FlfxCA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Az4H9FlfxCA", sceFreeTypeOtUnknown_Az4H9FlfxCA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_xj3tNJptfLI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("xj3tNJptfLI", sceFreeTypeOtUnknown_xj3tNJptfLI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_TPKhRt1QIeE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("TPKhRt1QIeE", sceFreeTypeOtUnknown_TPKhRt1QIeE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_6_yXRlVrjbc_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6+yXRlVrjbc", sceFreeTypeOtUnknown_6_yXRlVrjbc_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown__RLa_6fbshA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("+RLa-6fbshA", sceFreeTypeOtUnknown__RLa_6fbshA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_pBqh_Uh_gns_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("pBqh-Uh-gns", sceFreeTypeOtUnknown_pBqh_Uh_gns_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_2rcvKZfm5rI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2rcvKZfm5rI", sceFreeTypeOtUnknown_2rcvKZfm5rI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_7cIS2cFG3PE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("7cIS2cFG3PE", sceFreeTypeOtUnknown_7cIS2cFG3PE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_LfyUi_LkdCk_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("LfyUi-LkdCk", sceFreeTypeOtUnknown_LfyUi_LkdCk_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_rzIWh7FJnmU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("rzIWh7FJnmU", sceFreeTypeOtUnknown_rzIWh7FJnmU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_cKndiZnt0EA_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("cKndiZnt0EA", sceFreeTypeOtUnknown_cKndiZnt0EA_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_eqGCaeizvWM_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("eqGCaeizvWM", sceFreeTypeOtUnknown_eqGCaeizvWM_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_RvFx7dv9ThU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("RvFx7dv9ThU", sceFreeTypeOtUnknown_RvFx7dv9ThU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Po8kyd3DlIw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Po8kyd3DlIw", sceFreeTypeOtUnknown_Po8kyd3DlIw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_B3uU5aDZn94_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("B3uU5aDZn94", sceFreeTypeOtUnknown_B3uU5aDZn94_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_dojyaLKzKN0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("dojyaLKzKN0", sceFreeTypeOtUnknown_dojyaLKzKN0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_08_mhD_AAds_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("08-mhD-AAds", sceFreeTypeOtUnknown_08_mhD_AAds_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_64XMq6sDxUo_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("64XMq6sDxUo", sceFreeTypeOtUnknown_64XMq6sDxUo_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4xG87W9dTSg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4xG87W9dTSg", sceFreeTypeOtUnknown_4xG87W9dTSg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_4_SScYDT5Pg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("4+SScYDT5Pg", sceFreeTypeOtUnknown_4_SScYDT5Pg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_2rmvbvOCyjY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2rmvbvOCyjY", sceFreeTypeOtUnknown_2rmvbvOCyjY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_1jmC2QcpRl0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("1jmC2QcpRl0", sceFreeTypeOtUnknown_1jmC2QcpRl0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_HMcNe70D9TI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("HMcNe70D9TI", sceFreeTypeOtUnknown_HMcNe70D9TI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_6wJwUIVwAxg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("6wJwUIVwAxg", sceFreeTypeOtUnknown_6wJwUIVwAxg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_PrZtZyFwM6c_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("PrZtZyFwM6c", sceFreeTypeOtUnknown_PrZtZyFwM6c_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_3WpTQrvGUcU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("3WpTQrvGUcU", sceFreeTypeOtUnknown_3WpTQrvGUcU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_P6WOAeb0lJ8_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("P6WOAeb0lJ8", sceFreeTypeOtUnknown_P6WOAeb0lJ8_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_XGVABnpNksQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("XGVABnpNksQ", sceFreeTypeOtUnknown_XGVABnpNksQ_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_Ln9I6YfmYJU_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("Ln9I6YfmYJU", sceFreeTypeOtUnknown_Ln9I6YfmYJU_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_7cVMwHmN308_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("7cVMwHmN308", sceFreeTypeOtUnknown_7cVMwHmN308_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_AEU2tkjdZ2w_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("AEU2tkjdZ2w", sceFreeTypeOtUnknown_AEU2tkjdZ2w_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_2lSRg9FfIOY_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("2lSRg9FfIOY", sceFreeTypeOtUnknown_2lSRg9FfIOY_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_fPrLu1YGOaw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("fPrLu1YGOaw", sceFreeTypeOtUnknown_fPrLu1YGOaw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_t3vCgnT5YL0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("t3vCgnT5YL0", sceFreeTypeOtUnknown_t3vCgnT5YL0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_8bkQ_YRnpOE_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("8bkQ+YRnpOE", sceFreeTypeOtUnknown_8bkQ_YRnpOE_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_q4qhGebYuGI_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("q4qhGebYuGI", sceFreeTypeOtUnknown_q4qhGebYuGI_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_ni_K_leoQNg_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("ni-K-leoQNg", sceFreeTypeOtUnknown_ni_K_leoQNg_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_XfcgJB6V2a0_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("XfcgJB6V2a0", sceFreeTypeOtUnknown_XfcgJB6V2a0_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_TWzf5iykiGw_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("TWzf5iykiGw", sceFreeTypeOtUnknown_TWzf5iykiGw_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown__b4iVKArt94_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("-b4iVKArt94", sceFreeTypeOtUnknown__b4iVKArt94_nid_no_patch); + +int APS5_VABI sceFreeTypeOtUnknown_uSNqxrARpEQ_nid_no_patch() { + NotImplemented_nid_no_patch(__func__); +} +APS5_EXPORT("uSNqxrARpEQ", sceFreeTypeOtUnknown_uSNqxrARpEQ_nid_no_patch); } diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md new file mode 100644 index 0000000000..285157bd3c --- /dev/null +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -0,0 +1,5 @@ +# libSceFreeTypeOt technical debt + +### Unknown function info + +- The 150 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. From 718e1755ac6f1f6b3a17495da942a3d55e1ea41b Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:15:21 +0100 Subject: [PATCH 08/63] docs: distinguish known exports from unverified ABI --- docs/dev/debt/libSceFreeTypeOt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 285157bd3c..b3e085d085 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -1,5 +1,5 @@ # libSceFreeTypeOt technical debt -### Unknown function info +### Known export names; ABI prototypes unverified - The 150 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. From 2a7cd60c89fe3a7f3cdf3afdcd56d99bf626a480 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:43:33 +0100 Subject: [PATCH 09/63] feat(freetype): implement system outline border queries --- .../prx/libSceFreeType/src/OutlineBorders.cpp | 68 +++++ core/libs/prx/libSceFreeTypeOl/CMakeLists.txt | 37 +++ core/libs/prx/libSceFreeTypeOt/CMakeLists.txt | 3 + .../libs/tests/GuestFreeTypeOutlineBorder.cpp | 260 ++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 5 files changed, 370 insertions(+) create mode 100644 core/libs/prx/libSceFreeType/src/OutlineBorders.cpp create mode 100644 core/libs/prx/libSceFreeTypeOl/CMakeLists.txt create mode 100644 core/libs/prx/libSceFreeTypeOt/CMakeLists.txt create mode 100644 core/libs/tests/GuestFreeTypeOutlineBorder.cpp diff --git a/core/libs/prx/libSceFreeType/src/OutlineBorders.cpp b/core/libs/prx/libSceFreeType/src/OutlineBorders.cpp new file mode 100644 index 0000000000..0362e25ff0 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/OutlineBorders.cpp @@ -0,0 +1,68 @@ +#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include +#include +#include +#include + +namespace { + +bool Clockwise(const FreeType::GuestOutline* outline) { + if (!outline || outline->n_points == 0) return true; + if (!outline->points) throw std::invalid_argument("FreeType outline border: null points"); + auto minimum = outline->points[0]; + auto maximum = minimum; + for (unsigned i = 1; i < outline->n_points; ++i) { + const auto point = outline->points[i]; + minimum.x = std::min(minimum.x, point.x); + minimum.y = std::min(minimum.y, point.y); + maximum.x = std::max(maximum.x, point.x); + maximum.y = std::max(maximum.y, point.y); + } + if (minimum.x == maximum.x || minimum.y == maximum.y) return false; + constexpr std::int64_t limit = 0x1000000; + if (minimum.x < -limit || minimum.y < -limit || maximum.x > limit || maximum.y > limit) return false; + if (outline->n_contours == 0) return false; + if (!outline->contours) throw std::invalid_argument("FreeType outline border: null contours"); + unsigned first = 0; + for (unsigned contour = 0; contour < outline->n_contours; ++contour) { + const unsigned last = outline->contours[contour]; + if (last < first || last >= outline->n_points) + throw std::invalid_argument("FreeType outline border: invalid contour endpoint"); + first = last + 1; + } + const auto magnitude = [](std::int64_t value) { return static_cast(value < 0 ? -value : value); }; + const auto shift = [](std::uint32_t value) { const auto bits = std::bit_width(value); return bits > 15 ? bits - 15 : 0; }; + const auto xShift = shift(magnitude(maximum.x) | magnitude(minimum.x)); + const auto yShift = shift(static_cast(maximum.y - minimum.y)); + std::int64_t area = 0; + first = 0; + for (unsigned contour = 0; contour < outline->n_contours; ++contour) { + const unsigned last = outline->contours[contour]; + auto previous = outline->points[last]; + previous.x >>= xShift; + previous.y >>= yShift; + for (unsigned i = first; i <= last; ++i) { + auto current = outline->points[i]; + current.x >>= xShift; + current.y >>= yShift; + area += (current.y - previous.y) * (current.x + previous.x); + previous = current; + } + first = last + 1; + } + return area < 0; +} + +} + +extern "C" { + +std::int32_t APS5_VABI FT_Outline_GetInsideBorder_nid_postfix(const FreeType::GuestOutline* outline) { + return Clockwise(outline) ? 1 : 0; +} + +std::int32_t APS5_VABI FT_Outline_GetOutsideBorder_nid_postfix(const FreeType::GuestOutline* outline) { + return Clockwise(outline) ? 0 : 1; +} + +} diff --git a/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt b/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt new file mode 100644 index 0000000000..a9053faf5c --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt @@ -0,0 +1,37 @@ +add_library(libSceFreeTypeOl SHARED EXCLUDE_FROM_ALL Export.cpp ../libSceFreeType/src/OutlineBorders.cpp) +target_include_directories(libSceFreeTypeOl PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceFreeTypeOl) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_outline_border_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeOutlineBorder.cpp) + target_include_directories(guest_freetype_outline_border_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_outline_border_tests PRIVATE freetype ${CMAKE_DL_LIBS}) + if(WIN32) + target_link_libraries(guest_freetype_outline_border_tests PRIVATE libc) + endif() + configure_windows_unwind(guest_freetype_outline_border_tests) + add_custom_target(freetype_outline_border_patched DEPENDS + ${PATCHED_DIR}/libSceFreeTypeOl.prx ${PATCHED_DIR}/libSceFreeTypeOt.prx ${LIBC_PATCHED}) + add_dependencies(guest_freetype_outline_border_tests freetype_outline_border_patched) + add_test(NAME guest_freetype_outline_border_reference COMMAND guest_freetype_outline_border_tests reference) + foreach(module IN ITEMS Ol Ot) + foreach(border IN ITEMS inside outside) + add_test(NAME guest_freetype_outline_border_${module}_${border} + COMMAND guest_freetype_outline_border_tests $ ${border} raw) + add_test(NAME guest_freetype_outline_border_${module}_${border}_patched + COMMAND guest_freetype_outline_border_tests ${PATCHED_DIR}/libSceFreeType${module}.prx ${border} patched) + set_tests_properties(guest_freetype_outline_border_${module}_${border} + guest_freetype_outline_border_${module}_${border}_patched PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_outline_border_${module}_${border} + guest_freetype_outline_border_${module}_${border}_patched PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() + set_tests_properties(guest_freetype_outline_border_reference PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_outline_border_reference PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt b/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt new file mode 100644 index 0000000000..aca3d5c0df --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(libSceFreeTypeOt SHARED EXCLUDE_FROM_ALL Export.cpp ../libSceFreeType/src/OutlineBorders.cpp) +target_include_directories(libSceFreeTypeOt PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceFreeTypeOt) diff --git a/core/libs/tests/GuestFreeTypeOutlineBorder.cpp b/core/libs/tests/GuestFreeTypeOutlineBorder.cpp new file mode 100644 index 0000000000..e191a4c131 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeOutlineBorder.cpp @@ -0,0 +1,260 @@ +#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include +#include FT_FREETYPE_H +#include FT_STROKER_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +using FreeType::GuestOutline; +using FreeType::GuestVector; +using Query = int (APS5_VABI*)(const GuestOutline*); +static_assert(sizeof(GuestOutline) == 40 && alignof(GuestOutline) == 8); +static_assert(offsetof(GuestOutline, points) == 8 && offsetof(GuestOutline, contours) == 24); +static_assert(offsetof(GuestOutline, flags) == 32 && sizeof(GuestVector) == 16); +static_assert(sizeof(FT_StrokerBorder) == 4); + +namespace { + +bool nativeInside; + +void Require(bool result, const char* message) { + if (result) return; + std::fprintf(stderr, "FreeType outline border: %s\n", message); + std::exit(1); +} + +int APS5_VABI Native(const GuestOutline* input) { + if (!input) return nativeInside ? FT_Outline_GetInsideBorder(nullptr) : FT_Outline_GetOutsideBorder(nullptr); + FT_Outline outline{}; + outline.n_points = input->n_points; + outline.n_contours = input->n_contours; + outline.flags = input->flags; + std::vector points; + std::vector contours; + if (outline.n_points > 0) { + for (int i = 0; i < outline.n_points; ++i) { + Require(input->points[i].x >= std::numeric_limits::min() && input->points[i].x <= std::numeric_limits::max(), "reference x range"); + Require(input->points[i].y >= std::numeric_limits::min() && input->points[i].y <= std::numeric_limits::max(), "reference y range"); + points.push_back({static_cast(input->points[i].x), static_cast(input->points[i].y)}); + } + if (outline.n_contours > 0) + for (int i = 0; i < outline.n_contours; ++i) contours.push_back(input->contours[i]); + } + outline.points = points.data(); + outline.contours = contours.data(); + return nativeInside ? FT_Outline_GetInsideBorder(&outline) : FT_Outline_GetOutsideBorder(&outline); +} + +int Expected(bool inside, bool clockwise) { return inside == clockwise ? 1 : 0; } + +class ReadOnlyTail { + void* memory; + std::size_t page; + void* data; +public: + ReadOnlyTail(const void* source, std::size_t bytes) { +#ifdef _WIN32 + SYSTEM_INFO info; + GetSystemInfo(&info); + page = info.dwPageSize; + memory = VirtualAlloc(nullptr, page * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + Require(memory != nullptr, "allocate guard pages"); +#else + page = static_cast(sysconf(_SC_PAGESIZE)); + memory = mmap(nullptr, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(memory != MAP_FAILED, "allocate guard pages"); +#endif + Require(bytes <= page, "guard fixture size"); + data = static_cast(memory) + page - bytes; + std::memcpy(data, source, bytes); +#ifdef _WIN32 + DWORD previous; + Require(VirtualProtect(memory, page, PAGE_READONLY, &previous) != 0, "read-only input"); + Require(VirtualProtect(static_cast(memory) + page, page, PAGE_NOACCESS, &previous) != 0, "inaccessible neighbor"); +#else + Require(mprotect(memory, page, PROT_READ) == 0, "read-only input"); + Require(mprotect(static_cast(memory) + page, page, PROT_NONE) == 0, "inaccessible neighbor"); +#endif + } + ~ReadOnlyTail() { +#ifdef _WIN32 + VirtualFree(memory, 0, MEM_RELEASE); +#else + munmap(memory, page * 2); +#endif + } + template T* As() const { return static_cast(data); } + void* End() const { return static_cast(memory) + page; } +}; + +void Cases(Query query, bool inside, bool guest) { + Require(query(nullptr) == Expected(inside, true), "null outline"); + GuestOutline outline{}; + outline.points = reinterpret_cast(std::uintptr_t{1}); + outline.contours = reinterpret_cast(std::uintptr_t{1}); + Require(query(&outline) == Expected(inside, true), "empty outline does not read points or contours"); + std::array points{{{0, 0}, {0, 64}, {64, 64}, {64, 0}}}; + std::uint16_t end = 3; + outline = {1, 4, points.data(), nullptr, &end, 0}; + Require(query(&outline) == Expected(inside, true), "clockwise polygon"); + const auto original = points; + for (const int flags : {0, 1, 2, -1}) { + outline.flags = flags; + Require(query(&outline) == Expected(inside, true), "orientation ignores outline flags"); + } + Require(std::memcmp(points.data(), original.data(), sizeof(points)) == 0, "input points unchanged"); + std::reverse(points.begin(), points.end()); + Require(query(&outline) == Expected(inside, false), "counterclockwise polygon"); + for (auto& point : points) point.y = 0; + Require(query(&outline) == Expected(inside, false), "collapsed polygon uses no-orientation fallback"); + points = original; + outline.n_contours = 0; + Require(query(&outline) == Expected(inside, false), "no contours"); + outline.n_contours = 1; + for (const std::int64_t edge : {std::int64_t{0x1000000}, std::int64_t{0x1000001}}) { + points = {{{-edge, -edge}, {-edge, edge}, {edge, edge}, {edge, -edge}}}; + Require(query(&outline) == (edge == 0x1000000 ? Expected(inside, true) : Expected(inside, false)), "coordinate-limit boundary"); + } + std::array mixed{}; + std::copy(original.begin(), original.end(), mixed.begin()); + std::copy(original.rbegin(), original.rend(), mixed.begin() + 4); + std::array mixedEnds{{3, 7}}; + GuestOutline cancelled{2, 8, mixed.data(), nullptr, mixedEnds.data(), 0}; + Require(query(&cancelled) == Expected(inside, false), "non-collapsed opposite contours cancel area"); + std::vector manyPoints(65535); + std::copy(original.rbegin(), original.rend(), manyPoints.begin()); + std::uint16_t manyEnd = 65534; + GuestOutline many{1, 65535, manyPoints.data(), nullptr, &manyEnd, 0}; + Require(query(&many) == Expected(inside, false), "unsigned point count above 32767"); + std::fill(manyPoints.begin(), manyPoints.end(), GuestVector{}); + std::copy(original.begin(), original.end(), manyPoints.end() - 4); + std::vector manyEnds(65532); + for (unsigned i = 0; i < manyEnds.size() - 1; ++i) manyEnds[i] = static_cast(i); + manyEnds.back() = 65534; + many.n_contours = 65532; + many.contours = manyEnds.data(); + Require(query(&many) == Expected(inside, true), "unsigned contour count above 32767"); + if (guest) { + for (const auto edge : {std::numeric_limits::min(), std::numeric_limits::max()}) { + points = original; + points[0].x = edge; + Require(query(&outline) == Expected(inside, false), "64-bit out-of-range coordinate"); + } + points = original; + std::array repeated{}; + std::array ends{}; + const std::array large{{{0, 0}, {0, 16384}, {16384, 16384}, {16384, 0}}}; + for (unsigned i = 0; i < 5; ++i) { + std::copy(large.begin(), large.end(), repeated.begin() + i * 4); + ends[i] = static_cast(i * 4 + 3); + } + GuestOutline wide{5, 20, repeated.data(), nullptr, ends.data(), 0}; + Require(query(&wide) == Expected(inside, true), "LP64 area must not wrap at 32 bits"); + ReadOnlyTail guardedPoints(original.data(), sizeof(original)); + ReadOnlyTail guardedEnds(&end, sizeof(end)); + GuestOutline guarded{1, 4, guardedPoints.As(), static_cast(guardedPoints.End()), guardedEnds.As(), -1}; + ReadOnlyTail guardedOutline(&guarded, sizeof(guarded)); + Require(query(guardedOutline.As()) == Expected(inside, true), "read-only bounded inputs and unused inaccessible tags"); + outline = {1, 4, nullptr, nullptr, &end, 0}; + bool threw = false; + try { query(&outline); } catch (const std::invalid_argument&) { threw = true; } + Require(threw, "null positive-count points are explicitly unsupported"); + outline.points = points.data(); + outline.contours = nullptr; + threw = false; + try { query(&outline); } catch (const std::invalid_argument&) { threw = true; } + Require(threw, "null positive-count contours are explicitly unsupported"); + for (const std::uint16_t badEnd : {std::uint16_t{4}, std::uint16_t{0xffff}}) { + auto invalidEnd = badEnd; + outline.contours = &invalidEnd; + threw = false; + try { query(&outline); } catch (const std::invalid_argument&) { threw = true; } + Require(threw, "invalid contour endpoint is explicitly unsupported"); + } + for (const std::uint16_t nextEnd : {std::uint16_t{2}, std::uint16_t{3}}) { + std::array invalidEnds{{3, nextEnd}}; + outline.n_contours = 2; + outline.contours = invalidEnds.data(); + threw = false; + try { query(&outline); } catch (const std::invalid_argument&) { threw = true; } + Require(threw, "non-increasing contour endpoints are explicitly unsupported"); + } + } + std::uint32_t state = 7; + for (unsigned iteration = 0; iteration < 512; ++iteration) { + for (auto& point : points) { + state = state * 1664525u + 1013904223u; + point.x = static_cast((state >> 16) & 255) - 128; + state = state * 1664525u + 1013904223u; + point.y = static_cast((state >> 16) & 255) - 128; + } + outline = {1, 4, points.data(), nullptr, &end, 0}; + nativeInside = inside; + Require(query(&outline) == Native(&outline), "bounded polygon matches bundled FreeType"); + } + if (guest) { + points = original; + outline = {1, 4, points.data(), nullptr, &end, 0}; + std::array threads; + for (auto& thread : threads) thread = std::thread([&] { + for (unsigned i = 0; i < 100; ++i) Require(query(&outline) == Expected(inside, true), "concurrent independent query"); + }); + for (auto& thread : threads) thread.join(); + } +} + +} + +int main(int argc, char** argv) { +#ifdef _WIN32 + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); +#endif + if (argc == 2 && std::strcmp(argv[1], "reference") == 0) { + for (const bool inside : {false, true}) { + nativeInside = inside; + Cases(Native, inside, false); + } + std::puts("Bundled FreeType reference cases passed"); + return 0; + } + Require(argc == 4, "expected library, border and export mode"); + const bool inside = std::strcmp(argv[2], "inside") == 0; + const bool patched = std::strcmp(argv[3], "patched") == 0; + const char* symbol = patched ? (inside ? "Pz262plj20w" : "sKneJjUXWkQ") : + (inside ? "FT_Outline_GetInsideBorder_nid_postfix" : "FT_Outline_GetOutsideBorder_nid_postfix"); +#ifdef _WIN32 + const auto library = LoadLibraryA(argv[1]); + Require(library != nullptr, "load requested library"); + auto query = reinterpret_cast(GetProcAddress(library, symbol)); +#else + const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + Require(library != nullptr, "load requested library"); + auto query = reinterpret_cast(dlsym(library, symbol)); +#endif + if (!query) std::fprintf(stderr, "Missing %s\n", symbol); + Require(query != nullptr, "missing outline-border export"); + Cases(query, inside, true); + std::puts("Guest outline-border cases passed"); +#ifdef _WIN32 + FreeLibrary(library); +#else + dlclose(library); +#endif +} diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 6d4f98438d..5695ed22b3 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -4,6 +4,8 @@ - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char and FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Character iteration uses the low 32 bits of the input code, as that version does internally, and updates the guest's selected charmap pointer without refreshing unrelated glyph or outline data. Guest charmap fields are treated as read-only copies of the native records. +- [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeType/src/OutlineBorders.cpp) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. + - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions - [FT_New_Memory_Face, FT_Done_Face and FT_Done_FreeType](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures, errors and generic-finalizer ordering are unverified; they follow the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h) with LP64 guest arguments. Caller-owned font bytes must remain valid until the face is freed. From a3c6c8206e4cd2a548170345143c7fff68a8dfa5 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:47:12 +0100 Subject: [PATCH 10/63] docs(freetype): avoid adjacent debt entry conflicts --- docs/dev/debt/libSceFreeType.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 5695ed22b3..89ac75af9c 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -2,10 +2,10 @@ ### Unknown function info -- [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char and FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Character iteration uses the low 32 bits of the input code, as that version does internally, and updates the guest's selected charmap pointer without refreshing unrelated glyph or outline data. Guest charmap fields are treated as read-only copies of the native records. - - [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeType/src/OutlineBorders.cpp) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. +- [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char and FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Character iteration uses the low 32 bits of the input code, as that version does internally, and updates the guest's selected charmap pointer without refreshing unrelated glyph or outline data. Guest charmap fields are treated as read-only copies of the native records. + - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions - [FT_New_Memory_Face, FT_Done_Face and FT_Done_FreeType](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures, errors and generic-finalizer ordering are unverified; they follow the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h) with LP64 guest arguments. Caller-owned font bytes must remain valid until the face is freed. From 982350964ca784e111abd873524b81b4b7e37509 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:50:43 +0100 Subject: [PATCH 11/63] docs(freetype): keep provider debt in its module file --- docs/dev/debt/libSceFreeType.md | 2 -- docs/dev/debt/libSceFreeTypeOt.md | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 docs/dev/debt/libSceFreeTypeOt.md diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 89ac75af9c..6d4f98438d 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -2,8 +2,6 @@ ### Unknown function info -- [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeType/src/OutlineBorders.cpp) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. - - [FT_Select_Charmap, FT_Set_Charmap, FT_Get_First_Char and FT_Get_Next_Char](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console signatures and errors are unverified; the LP64 guest adapter follows the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Character iteration uses the low 32 bits of the input code, as that version does internally, and updates the guest's selected charmap pointer without refreshing unrelated glyph or outline data. Guest charmap fields are treated as read-only copies of the native records. - [FT_Init_FreeType, FT_New_Face, FT_Set_Pixel_Sizes, FT_Get_Char_Index, FT_Load_Glyph, FT_Render_Glyph](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the PS4 library is taken to be FreeType 2 built for LP64: the face, size, charmap, glyph slot, bitmap and outline layouts follow the public headers of the bundled FreeType 2.13.3 with a 64-bit `long`, which matches the offsets the OpenOrbis samples read (`face->glyph` at 0x98, `slot->advance` at 0x80, `slot->bitmap` at 0x98, `slot->bitmap_left` at 0xc0). Which FreeType version and options the console library is built with is unknown, so the behaviour and error codes are those of FreeType 2.13.3. The OpenOrbis font, keyboard, threading, system and SDL2 samples import these six functions diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md new file mode 100644 index 0000000000..64f195f445 --- /dev/null +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -0,0 +1,5 @@ +# libSceFreeTypeOt technical debt + +### Unknown function info + +- [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. From 96bccd4c9416d70ec106b569a5b6f748ef7370e1 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:48:10 +0000 Subject: [PATCH 12/63] perf(agc): wait for the display keys' writer before a present takes the device lock's time The presenter checks a display buffer's DCC keys under the device lock. When the batch of the kernel that writes those keys had not finished, the check waited for the GPU while holding the lock, and the queue worker recording the next frame waited behind the presenter. PresentDisplayBuffer now first syncs through the key range with the unlocked wait the flush hook uses: the presenter's acquisition is the outermost on its thread and nothing has been decided yet, so the lock is released for the GPU wait and retaken for the completions. The key check that follows finds the batch finished. Tested on Windows in a title's menu: the presenter's GPU waits under the lock went from 721 (1.38 s) to 1 (0 ms) per 10 s, and the queue worker's waits for the lock from 1.47 s to 0.18 s per 10 s. --- core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 4eb2724ed7..58683b2116 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3213,6 +3213,7 @@ FrameDumps& Dumps() { } bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { + if (buffer.dccAddress != 0 && buffer.tilingMode == 0) state->recorder->SyncThrough(buffer.dccAddress, DisplayBufferKeyBytes(buffer), true); static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP")); if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip"); else Graphics::StorageTexture::FlushDumpSelected("flip"); From a3af6b1149de514093ac0a3eef230d6b57c5eab6 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:33:39 +0000 Subject: [PATCH 13/63] perf(agc): count presents that waited for the display keys' writer unlocked With APS5_PROFILE_DRAW set, the [present] line now counts the presents whose sync through the display buffer's DCC keys waited for the GPU before the key check, outside the GPU mutex, and the milliseconds they waited: the waits the presenter took under the mutex before. --- .../libSceAgcDriver/Execution/include/VulkanDevice.hpp | 4 ++-- .../Execution/src/Driver/Presentation.cpp | 2 +- .../prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 10 +++++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 52303ede78..b30278117f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -252,8 +252,8 @@ public: // blit-submit times after the flip packet; noted in-place reads checked // (APS5_FLIP_READ_CHECK=1) and found overwritten before the blit's fence. struct PresentStatistics { - std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten, acquireDrops; - double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs; + std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten, acquireDrops, keyWriterWaits; + double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs, keyWriterWaitMs; }; static PresentStatistics PresentCounts(); struct PresyncStatistics { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp index d071d049fc..b0613c20c3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp @@ -177,7 +177,7 @@ void Driver::reportPresents(double waitedMs, std::size_t inFlight) { const auto flips = flipsCounted.load() - previousFlips; const double perFlip = flips != 0 ? 1.0 / static_cast(flips) : 0.0; const auto serial = flipSerial.load(); - AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%llu dropped: no free swapchain image; %zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per frame, presented or dropped: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast(presents), static_cast(dropped), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perFrame, (counts.gpuGapMs - previous.gpuGapMs) * perFrame, static_cast(counts.gpuUnread - previous.gpuUnread), static_cast(serial - windowSerial) * perFlip, static_cast(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast(counts.readsOverwritten - previous.readsOverwritten), static_cast(counts.readsChecked - previous.readsChecked)); + AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%llu dropped: no free swapchain image; %zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per frame, presented or dropped: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked; waits for the display keys' writer outside the GPU mutex: %llu, %.1f ms\n", static_cast(presents), static_cast(dropped), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perFrame, (counts.gpuGapMs - previous.gpuGapMs) * perFrame, static_cast(counts.gpuUnread - previous.gpuUnread), static_cast(serial - windowSerial) * perFlip, static_cast(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast(counts.readsOverwritten - previous.readsOverwritten), static_cast(counts.readsChecked - previous.readsChecked), static_cast(counts.keyWriterWaits - previous.keyWriterWaits), counts.keyWriterWaitMs - previous.keyWriterWaitMs); previous = counts; previousFlips = flipsCounted.load(); previousUnsignaled = flipBatchesUnsignaled.load(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 58683b2116..b0bac7aa90 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3213,7 +3213,15 @@ FrameDumps& Dumps() { } bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { - if (buffer.dccAddress != 0 && buffer.tilingMode == 0) state->recorder->SyncThrough(buffer.dccAddress, DisplayBufferKeyBytes(buffer), true); + if (buffer.dccAddress != 0 && buffer.tilingMode == 0) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + const auto before = profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0; + state->recorder->SyncThrough(buffer.dccAddress, DisplayBufferKeyBytes(buffer), true); + if (const auto waited = profile ? Graphics::Recorder::ThreadWaitedMs() - before : 0.0; waited > 0.0) { + ++presentCounters.keyWriterWaits; + presentCounters.keyWriterWaitMs += waited; + } + } static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP")); if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip"); else Graphics::StorageTexture::FlushDumpSelected("flip"); From 54196438fcc45d1f3fd611a0030cd14ddbdc0716 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:51:27 +0000 Subject: [PATCH 14/63] test(agc): present behind a pending writer of the display keys without the GPU mutex A headless device presents a DCC display buffer while a recorded kernel that copies over its keys is still pending, holding the GPU mutex once as Driver::Present does. The present must release the mutex for that wait (the hold serial advances), read the keys the kernel stored and reach the swapchain; with nothing pending it must keep the mutex. It stops at the missing swapchain, after the key check. --- .../tests/execution/PresentKeyWriterWait.cpp | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PresentKeyWriterWait.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PresentKeyWriterWait.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PresentKeyWriterWait.cpp new file mode 100644 index 0000000000..e85b6e55d0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PresentKeyWriterWait.cpp @@ -0,0 +1,202 @@ +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Side = 128; +constexpr std::uint64_t DisplayFormat = 0x8000000000000000ull; +constexpr std::size_t SurfaceBytes = 65536; +constexpr std::size_t KeysOffset = SurfaceBytes; +constexpr std::size_t KeysCapacity = 32768; +constexpr std::size_t CopiedKeys = 256; +constexpr std::size_t SourceKeysOffset = KeysOffset + KeysCapacity; +constexpr std::size_t BlockBytes = 2 * 65536; +constexpr std::uint8_t Uncompressed = 0xff; +constexpr std::uint8_t Cleared = 0x00; + +alignas(256) constexpr std::array CopyKeysCode{ + 0x34020083, 0xe0341000, 0x80000201, 0xbf8c3f70, 0xe0741000, 0x80010201, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(65536, BlockBytes)); +#endif + Require(block != nullptr, "present key writer wait: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, const std::vector& userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +struct Scenario { + std::uint8_t* surface; + std::uint8_t* keys; + std::uint8_t* source; + std::size_t keyBytes; + AgcDriver::DisplayBuffer buffer; + std::vector copyData; +}; + +Scenario Prepare(std::uint8_t* block) { + Scenario scenario{block, block + KeysOffset, block + SourceKeysOffset, 0, {AddressOf(block), DisplayFormat, Side, Side}, std::vector(16, 0u)}; + scenario.buffer.dccAddress = AddressOf(scenario.keys); + scenario.keyBytes = AgcDriver::DisplayBufferKeyBytes(scenario.buffer); + Require(AgcDriver::DisplayBufferSize(scenario.buffer) <= SurfaceBytes && scenario.keyBytes >= CopiedKeys && scenario.keyBytes <= KeysCapacity, "present key writer wait: the display buffer does not fit the guest block"); + const auto source = BufferDescriptor(scenario.source, CopiedKeys); + const auto destination = BufferDescriptor(scenario.keys, CopiedKeys); + std::copy(source.begin(), source.end(), scenario.copyData.begin()); + std::copy(destination.begin(), destination.end(), scenario.copyData.begin() + 4); + return scenario; +} + +void Reset(const Scenario& scenario) { + std::memset(scenario.surface, 0x55, SurfaceBytes); + std::memset(scenario.keys, Uncompressed, scenario.keyBytes); + std::memset(scenario.keys, Cleared, CopiedKeys); + std::memset(scenario.source, Uncompressed, CopiedKeys); + AgcDriver::GuestMemory::BumpCollectEpoch(); +} + +std::string PresentFailure(AgcDriver::VulkanDevice& device, const AgcDriver::DisplayBuffer& buffer) { + try { + device.PresentDisplayBuffer(buffer); + } catch (const std::exception& error) { + return error.what(); + } + return "the headless present succeeded"; +} + +const std::string NoSwapchain = "Vulkan presentation: device has no swapchain"; + +bool CanCopyKeys(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + const std::array pattern{0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const bool filled = device.FillBuffer(AddressOf(scenario.keys), CopiedKeys, pattern); + device.WaitIdle(); + return filled; +} + +void RunPendingWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + Reset(scenario); + const auto copy = Compile(device, CopyKeysCode, scenario.copyData); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(AgcDriver::GuestMemory::GpuMutex().DepthOnThisThread() == 1, "present key writer wait: the presenter's hold is not the outermost"); + device.Dispatch(copy, 1, 1, 1, {}, reinterpret_cast(CopyKeysCode.data())); + Require(AgcDriver::Graphics::DccKeyWritersPending(scenario.buffer.dccAddress, scenario.keyBytes), "a recorded kernel that copies data over the display keys is not seen as a pending key writer"); + const auto holds = AgcDriver::GuestMemory::GpuHoldSerial(); + const auto failure = PresentFailure(device, scenario.buffer); + const auto acquired = AgcDriver::GuestMemory::GpuHoldSerial() - holds; + Require(failure == NoSwapchain, "a present behind a pending writer of the display keys stopped before the swapchain: " + failure); + Require(acquired != 0, "the presenter synchronized with the display keys' writer without releasing the GPU mutex"); + Require(!AgcDriver::Graphics::DccKeyWritersPending(scenario.buffer.dccAddress, scenario.keyBytes), "the display keys' writer is still pending after the present"); + Require(scenario.keys[0] == Uncompressed && scenario.keys[CopiedKeys - 1] == Uncompressed, "the present read the display keys before their writer landed"); +} + +void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + std::memset(scenario.keys, Uncompressed, scenario.keyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.WaitIdle(); + Require(!AgcDriver::Graphics::DccKeyWritersPending(scenario.buffer.dccAddress, scenario.keyBytes), "the idle device still reports a pending writer of the display keys"); + const auto holds = AgcDriver::GuestMemory::GpuHoldSerial(); + const auto failure = PresentFailure(device, scenario.buffer); + const auto acquired = AgcDriver::GuestMemory::GpuHoldSerial() - holds; + Require(failure == NoSwapchain, "a present with no pending writer of the display keys stopped before the swapchain: " + failure); + Require(acquired == 0, "a present with no pending writer of the display keys released the GPU mutex"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!device->CanWaitUnlocked()) { + std::puts("skipped, the device has no timeline semaphores: the presenter cannot wait without the GPU mutex"); + return VulkanTestSkipped; + } + GuestBlock block; + const auto scenario = Prepare(block.Data()); + if (!CanCopyKeys(*device, scenario)) { + std::puts("skipped, host imports are unavailable: the key writer of the test is a copied writer"); + return VulkanTestSkipped; + } + RunPendingWriter(*device, scenario); + RunNoWriter(*device, scenario); + std::puts("present key writer wait tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From e284395f586dda6808250210cb225aef4df8021e Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Fri, 23 Oct 2026 13:24:29 +0200 Subject: [PATCH 15/63] fix(agc): keep hundreds of raw shader snapshots cached ReadRawShader evicted snapshots past 64 entries or 8 MiB. A snapshot owns its prepared artifacts, so an evicted unregistered shader was recompiled on its next use. PS4 titles do not register their shaders: Knowledge is Power cycles through 383 of them in its menus and recompiled them 341 136 times in 240 s. Raise the limit to 4096 entries or 64 MiB. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/Driver/Shaders/ShaderRegistry.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Submit.cpp | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 1065f7921f..5e1064aaa1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -136,7 +136,7 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { cache.erase(found); } const auto bytes = result->code.size() * sizeof(std::uint32_t); - while (!cache.empty() && (cache.size() >= 64 || cacheBytes + bytes > 8 * 1024 * 1024)) { + while (!cache.empty() && (cache.size() >= 4096 || cacheBytes + bytes > 64 * 1024 * 1024)) { cacheBytes -= cache.back()->code.size() * sizeof(std::uint32_t); cache.pop_back(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index d97862941b..ded031d574 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -955,7 +955,8 @@ int main(int argc, char** argv) { }); for (auto& reader : readers) reader.join(); for (const auto& snapshot : concurrent) check(snapshot == branched, "concurrent raw compute readers lost snapshot reuse"); - alignas(256) std::array, 65> programs{}; + struct alignas(256) RawProgram : std::array {}; + std::vector programs(4097); for (auto& program : programs) program[0] = 0xbf810000; const auto firstAddress = reinterpret_cast(programs.front().data()); readers.clear(); @@ -965,6 +966,9 @@ int main(int argc, char** argv) { for (auto& reader : readers) reader.join(); for (const auto& snapshot : concurrent) check(snapshot == concurrent.front(), "concurrent raw compute misses duplicated snapshots"); const auto evicted = AgcDriver::DriverDetail::ReadRawShader(firstAddress); + for (std::size_t i = 1; i <= 400; ++i) + AgcDriver::DriverDetail::ReadRawShader(reinterpret_cast(programs[i].data())); + check(AgcDriver::DriverDetail::ReadRawShader(firstAddress) == evicted, "raw shader cache evicted a program while a title used only 401"); for (std::size_t i = 1; i < programs.size(); ++i) AgcDriver::DriverDetail::ReadRawShader(reinterpret_cast(programs[i].data())); check(AgcDriver::DriverDetail::ReadRawShader(firstAddress) != evicted && evicted->code[0] == 0xbf810000, From f24320dc1c59344c13db1fefa042937b48939d02 Mon Sep 17 00:00:00 2001 From: shanu337 Date: Wed, 21 Oct 2026 19:16:32 +0530 Subject: [PATCH 16/63] refactor(libSceVideoOut): consolidate color space wrappers and add Flip tests --- .../libs/prx/libSceVideoOut/Unimplemented.cpp | 27 ------------------- .../prx/libSceVideoOut/include/Output.hpp | 2 ++ core/libs/prx/libSceVideoOut/src/Output.cpp | 25 +++++++++++++++++ core/libs/prx/libSceVideoOut/tests/Flip.cpp | 16 +++++++++++ docs/dev/debt/libSceVideoOut.md | 1 + 5 files changed, 44 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index 91b75fbb31..45a83a04c0 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -37,35 +37,8 @@ int APS5_VABI sceVideoOutCursorSetPosition(int32_t, int32_t, uint32_t, uint32_t) return 0; } -int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size) { - if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutAdjustColor_: VIDEO_OUT_ERROR_INVALID_VALUE"); - return sceVideoOutAdjustColor(handle, settings); -} - -int APS5_VABI sceVideoOutColorSettingsSetGamma_(VideoOutColorSettings* settings, float gamma, uint32_t settings_size) { - if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutColorSettingsSetGamma_: VIDEO_OUT_ERROR_INVALID_VALUE"); - return sceVideoOutColorSettingsSetGamma(settings, gamma); -} - -int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { - if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); - return sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(settings); -} - -int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option, uint32_t settings_size) { - if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); - return sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(settings, lumaScale, option); -} - -int APS5_VABI sceVideoOutAdjustColorSpaceConversion_(int handle, const VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { - if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutAdjustColorSpaceConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); - return sceVideoOutAdjustColorSpaceConversion(handle, settings); -} - - int APS5_VABI sceDbgVideoOutAddOutputModeVr60Privilege() { NotImplemented_nid_no_patch(__func__); return 0; } - } diff --git a/core/libs/prx/libSceVideoOut/include/Output.hpp b/core/libs/prx/libSceVideoOut/include/Output.hpp index 569e97ee77..8da7cc2561 100644 --- a/core/libs/prx/libSceVideoOut/include/Output.hpp +++ b/core/libs/prx/libSceVideoOut/include/Output.hpp @@ -28,6 +28,8 @@ int APS5_VABI sceVideoOutLatencyControlWaitBeforeInput(int handle); int APS5_VABI sceVideoOutLatencyMeasureSetStartPoint(int handle, uint32_t point); int APS5_VABI sceVideoOutColorSettingsSetGamma(VideoOutColorSettings* settings, float gamma); int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* settings); +int APS5_VABI sceVideoOutColorSettingsSetGamma_(VideoOutColorSettings* settings, float gamma, uint32_t settings_size); +int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size); int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings); int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option); int APS5_VABI sceVideoOutAdjustColorSpaceConversion(int handle, const VideoOutColorSpaceConversionSettings* settings); diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 24f9b6f15c..33a8ecdaac 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -370,6 +370,31 @@ int APS5_VABI sceVideoOutAdjustColorSpaceConversion(int handle, const VideoOutCo LibcAwaitExit_nid_postfix(); } +int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutAdjustColor_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutAdjustColor(handle, settings); +} + +int APS5_VABI sceVideoOutColorSettingsSetGamma_(VideoOutColorSettings* settings, float gamma, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutColorSettingsSetGamma_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutColorSettingsSetGamma(settings, gamma); +} + +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(settings); +} + +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(settings, lumaScale, option); +} + +int APS5_VABI sceVideoOutAdjustColorSpaceConversion_(int handle, const VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutAdjustColorSpaceConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutAdjustColorSpaceConversion(handle, settings); +} + int APS5_VABI sceVideoOutSetGlobalBlendSpace(int handle, uint32_t space, uint32_t option) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 365eddf412..962682815e 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -449,6 +449,22 @@ void testControls() { } VideoOutColorSettings settings{}; expectFailure([&] { sceVideoOutColorSettingsSetGamma(&settings, std::numeric_limits::quiet_NaN()); }); + expectFailure([&] { sceVideoOutColorSettingsSetGamma_(&settings, 1.0f, sizeof(VideoOutColorSettings) - 1); }); + check(sceVideoOutColorSettingsSetGamma_(&settings, 1.2f, sizeof(VideoOutColorSettings)) == 0, "gamma wrapper rejected valid size"); + check(settings.gamma == 1.2f, "gamma wrapper failed to forward value"); + + expectFailure([&] { sceVideoOutAdjustColor_(handle, &settings, sizeof(VideoOutColorSettings) - 1); }); + check(sceVideoOutAdjustColor_(handle, &settings, sizeof(VideoOutColorSettings)) == 0, "adjust color wrapper rejected valid size"); + + VideoOutColorSpaceConversionSettings cscSettings{}; + expectFailure([&] { sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(&cscSettings, sizeof(VideoOutColorSpaceConversionSettings) - 1); }); + check(sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(&cscSettings, sizeof(VideoOutColorSpaceConversionSettings)) == 0, "default SDR to HDR wrapper rejected valid size"); + + expectFailure([&] { sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(&cscSettings, 1.0f, 0, sizeof(VideoOutColorSpaceConversionSettings) - 1); }); + check(sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(&cscSettings, 1.0f, 0, sizeof(VideoOutColorSpaceConversionSettings)) == 0, "SDR to HDR conversion wrapper rejected valid size"); + + expectFailure([&] { sceVideoOutAdjustColorSpaceConversion_(handle, &cscSettings, sizeof(VideoOutColorSpaceConversionSettings) - 1); }); + check(sceVideoOutAdjustColorSpaceConversion_(handle, &cscSettings, sizeof(VideoOutColorSpaceConversionSettings)) == 0, "adjust color space conversion wrapper rejected valid size"); KernelEqueue queue = 0; check(sceKernelCreateEqueue(&queue, "VideoOut test") == 0, "event queue creation failed"); auto owner = EqueuePin_nid_postfix(queue); diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 5fdf3bd52f..42a02e8a22 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -9,6 +9,7 @@ ### Unknown function info +- PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) - [sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_, sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_, sceVideoOutAdjustColorSpaceConversion_, sceVideoOutSetGlobalBlendSpace](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA30630 imports these to configure color space conversion and global blend space. Color space conversion settings are accepted with size validation (32 bytes) but conversion blocks and blend space parameters are not applied to the presentation swapchain - [sceVideoOutDeleteVrrActiveStatusEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut, NID `-q-vbO859Tw`) - the name hashes to the NID PPSA02530 imports and is listed in [sce_symbols](https://github.com/zecoxao/sce_symbols) `aerolib.csv` at `fe2cc2948e`. The signature `(eq, handle)` and the behaviour, removing the handle's VRR status registration from the queue as the other `Delete*Event` calls do, follow sceVideoOutAddVrrActiveStatusEvent; no title has been observed calling it - [sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress, sceVideoOutCursorSetPosition](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NIDs `UPZWCH8qTM4`, `fEPEwROdCwM`, `u-9bhWQApq8`, `N17AK88NdD0`) - the names hash to the NIDs Prospero Win (PPSA99995) imports, and the parameter lists `(handle, index, address)`, `(handle, index)`, `(handle, index, address)` and `(handle, index, x, y)` follow its [declarations](https://github.com/mpereiraesaa/prospero-win/blob/644e32e400c555e340d507383398c5d21b39b0b0/wine/ps5/pw_vulkan_radv.c#L65-L68). The cursor image's format and size and the console's return values are not known; the functions report that they are not implemented. From e437f33787df84b3033c82d991c120d89236bb1f Mon Sep 17 00:00:00 2001 From: orezraey Date: Fri, 23 Oct 2026 12:13:07 -0300 Subject: [PATCH 17/63] fix(libkernel): answer the thread atexit references as measured on a PS5 Increment returns -1 for an address outside every module and Decrement never fails, instead of throwing. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Rtld.cpp | 11 +++++------ core/libs/tests/GuestDynamicLoader.cpp | 7 +++++-- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index 90472da544..bd730181e3 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -35,15 +35,13 @@ const void* imageContaining(const void* address, bool reference, void*& handle, #ifdef _WIN32 const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | (reference ? 0 : GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT); HMODULE module = nullptr; - if (!GetModuleHandleExW(flags, reinterpret_cast(address), &module)) - throw std::runtime_error(std::string(caller) + ": address outside every loaded image"); + if (!GetModuleHandleExW(flags, reinterpret_cast(address), &module)) return nullptr; if (reference) handle = module; return module; #else Dl_info info{}; link_map* image = nullptr; - if (!dladdr1(address, &info, reinterpret_cast(&image), RTLD_DL_LINKMAP) || !image) - throw std::runtime_error(std::string(caller) + ": address outside every loaded image"); + if (!dladdr1(address, &info, reinterpret_cast(&image), RTLD_DL_LINKMAP) || !image) return nullptr; if (reference && image->l_name && *image->l_name) { handle = dlopen(image->l_name, RTLD_LAZY | RTLD_NOLOAD); if (!handle) throw std::runtime_error(std::string(caller) + ": cannot reference " + image->l_name); @@ -55,6 +53,7 @@ const void* imageContaining(const void* address, bool reference, void*& handle, int referenceThreadAtexitImage(const void* address, const char* caller) { void* handle = nullptr; const void* image = imageContaining(address, true, handle, caller); + if (!image) return -1; std::lock_guard lock(threadAtexitMutex); threadAtexitReferences.emplace(image, handle); return 0; @@ -63,11 +62,11 @@ int referenceThreadAtexitImage(const void* address, const char* caller) { int releaseThreadAtexitImage(const void* address, const char* caller) { void* handle = nullptr; const void* image = imageContaining(address, false, handle, caller); + if (!image) return 0; { std::lock_guard lock(threadAtexitMutex); const auto found = threadAtexitReferences.find(image); - if (found == threadAtexitReferences.end()) - throw std::runtime_error(std::string(caller) + ": image holds no thread atexit reference"); + if (found == threadAtexitReferences.end()) return 0; handle = found->second; threadAtexitReferences.erase(found); } diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 1307264c64..6013ae1f19 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -229,9 +229,12 @@ int main(int argc, char** argv) { Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(add)) == 0); Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); - Require(ThrowsRuntimeError([] { _sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&Require)); })); + Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&Require)) == 0); int local = 0; - Require(ThrowsRuntimeError([&] { _sceKernelRtldThreadAtexitIncrement_nid_postfix(&local); })); + Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(&local) == -1); + Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(nullptr) == -1); + Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(&local) == 0); + Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(nullptr) == 0); #ifndef _WIN32 Require(dlsym_nid_postfix(nullptr, "malloc") == nullptr); Require(dlsym_nid_postfix(nullptr, "dlopen") == nullptr); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 515c8d8c2b..b8804e2c64 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -77,7 +77,7 @@ - [Map hints inside a reservation](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMapDirectMemory` and the flexible and anonymous maps place a mapping without `MAP_FIXED` at its hint address when the whole range lies in a range still reserved by `sceKernelReserveVirtualRange`, as if `MAP_FIXED` were set. Taken from PPSA01474, which reserves 64 GiB and then maps its heaps inside it with hints and no `MAP_FIXED`, and uses the hint addresses afterwards; not measured on the console. Hints outside a reservation, or over a range that is partly mapped, stay hints - [Fixed anonymous mmap](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `MAP_PRIVATE | MAP_ANON | MAP_FIXED` replaces the requested range through the existing guest mapping machinery and zeroes the new memory. `MAP_EXCL` is checked under the allocation-registry mutation lock, including virtual reservations, and returns `ENOMEM` without changing an occupied range; without `MAP_FIXED` it is `EINVAL`. Lengths are rounded to 16 KiB and fixed addresses must be aligned. The complete rounded range must lie inside the guest arena or, for POSIX fixed anonymous `mmap` on Windows, one current or registered native thread stack; other mapping APIs do not opt into stack replacement. Native stack replacement keeps the reservation, decommits and recommits fresh pages, and rejects foreign allocations, ranges spanning reservations and host `PAGE_GUARD` pages before mutation. Current-stack ranges must end at least 64 KiB below the active host frame; callers replacing another thread's stack must coordinate with that thread. If recommitting fails, the call returns `ENOMEM` with the range still reserved; thread cleanup attempts to recommit it before restoring write access. This follows FreeBSD 11.4 [argument validation and rounding](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L226-L299), [fixed replacement](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1455-L1464), and [collision errno](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1604-L1613). The console behaviour has not been measured. AnyPS5 starts the guest arena at 0x200000000, so the fixed path rejects the low 0x400000 address requested by PICO-8 PPSA99808. The console address-space layout is unmeasured; executable placement outside the host arena is tracked in #5217. Static catalog disassembly finds `0x1012` in PPSA98001/PPSA99808 and `0x5012` in PPSA99808; these titles were not run. - [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start -- [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match +- [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in a payload process on a PS5 (firmware 13.60), Increment returns -1 for an address outside every module (null, stack, heap) and 0 otherwise, and Decrement never fails, also without a matching Increment; the console's Decrement returns nothing, here 0. The module's `ref_count` did not change on the console. As in [fpPS4](https://github.com/red-prig/fpPS4), Increment here takes a reference on the image and Decrement releases one. The variants without the leading underscore are not exported by firmware 13.60's `libkernel_sys`; here they match - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown - [cfsetspeed](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel, NID `C1MQ6OaXRjo`) - console behavior and module/library versions are unverified. The prototype, uint32 speed and 44-byte termios layout (input speed at 36, output speed at 40) follow the public PS5 payload SDK [termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/termios.h) and [sys/_termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/sys/_termios.h). The module association follows its libkernel stub table and the NID rehashes to the aerolib name. Both speed fields accept every uint32 value and no flags or control characters change, as [FreeBSD 11.4 termios.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/gen/termios.c) does. No terminal I/O is performed. Null pointers throw where the reference dereferences them. - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled. A kqueue is readable while one of its events is triggered and is never writable or in the exception set, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers, on every host. Other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket From 4acca6020c26583deda0334309de8d6b25814f13 Mon Sep 17 00:00:00 2001 From: orezraey Date: Fri, 23 Oct 2026 12:14:27 -0300 Subject: [PATCH 18/63] fix(libkernel): log a thread atexit Decrement without a reference Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Rtld.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index bd730181e3..4a11c90be5 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -66,7 +66,10 @@ int releaseThreadAtexitImage(const void* address, const char* caller) { { std::lock_guard lock(threadAtexitMutex); const auto found = threadAtexitReferences.find(image); - if (found == threadAtexitReferences.end()) return 0; + if (found == threadAtexitReferences.end()) { + APS5_LOG_OUT("%s: image holds no thread atexit reference", caller); + return 0; + } handle = found->second; threadAtexitReferences.erase(found); } From 7618d0335fb22651722c2bd0fc41c6da0f423538 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:30:51 -0500 Subject: [PATCH 19/63] test(libScePlayGo): cover install speed, eta, and prefetch handle validation --- core/libs/tests/GuestPlayGo.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index c03f4761ca..30a1375975 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -26,6 +26,9 @@ int APS5_VABI scePlayGoSetToDoList(int, const PlayGoToDo*, std::uint32_t); int APS5_VABI scePlayGoGetToDoList(int, PlayGoToDo*, std::uint32_t, std::uint32_t*); int APS5_VABI scePlayGoGetLocus(int, const std::uint16_t*, std::uint32_t, std::int8_t*); int APS5_VABI scePlayGoGetChunkId(int, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI scePlayGoGetEta(int, const std::uint16_t*, std::uint32_t, std::int64_t*); +int APS5_VABI scePlayGoGetInstallSpeed(int, std::int32_t*); +int APS5_VABI scePlayGoPrefetch(int, const std::uint16_t*, std::uint32_t, std::int8_t); } static void Require(bool value, const std::source_location location = std::source_location::current()) { @@ -271,6 +274,14 @@ int main(int argc, char** argv) { RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}}); RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile("plgo")}}); RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); + std::int64_t eta = -1; + std::int32_t speed = -1; + Require(scePlayGoGetEta(-1, nullptr, 0, &eta) == static_cast(0x80CF0002)); + Require(scePlayGoGetEta(0x506C476F, nullptr, 0, nullptr) == static_cast(0x80CF0004)); + Require(scePlayGoGetInstallSpeed(-1, &speed) == static_cast(0x80CF0002)); + Require(scePlayGoGetInstallSpeed(0x506C476F, nullptr) == static_cast(0x80CF0004)); + Require(scePlayGoGetInstallSpeed(0x506C476F, &speed) == 0 && speed == 2); + Require(scePlayGoPrefetch(-1, nullptr, 0, 0) == static_cast(0x80CF0002)); RunLayout(argv[0], "--scenario-only", {{"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--ficm-only", {{"playgo-ficm.dat", "x"}}); RunLayout(argv[0], "--hash-table-only", {{"playgo-hash-table.dat", "x"}}); From 07198e1e6a3a7bd48e1e486bfd67dac23941ea26 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:31:30 -0500 Subject: [PATCH 20/63] test(libSceSaveData.native): cover null parameter validation in sceSaveDataSyncSaveDataMemory --- core/libs/tests/GuestSaveDataMountStatus.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 33fd69861e..b6330cb98e 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -20,6 +20,7 @@ int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, cons int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint*, SaveDataIcon*); int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint*, const SaveDataIcon*); int APS5_VABI sceSaveDataSaveIconByPath(const SaveDataMountPoint*, const char*); +int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* param); } namespace { @@ -232,6 +233,7 @@ int main() { const auto previous = std::filesystem::current_path(); std::filesystem::current_path(root); CheckUnmountedCalls(); + Check(sceSaveDataSyncSaveDataMemory(nullptr) == static_cast(0x809F0001), "null param rejected in memory sync"); Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); MountAndCheck("fresh", MountReadWrite | MountCreate2, 1u, "CREATE2 of a missing save"); MountAndCheck("fresh", MountReadWrite | MountCreate2, 0u, "CREATE2 of an existing save"); From 08d7d3fb897669568dcd2a531ae24c188f2191bc Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:31:56 -0500 Subject: [PATCH 21/63] test(libSceNpTrophy): cover icon size pointer checks and invalid handle validation --- core/libs/tests/GuestNpTrophy.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestNpTrophy.cpp b/core/libs/tests/GuestNpTrophy.cpp index e8a9b8749f..a8fb67b032 100644 --- a/core/libs/tests/GuestNpTrophy.cpp +++ b/core/libs/tests/GuestNpTrophy.cpp @@ -13,6 +13,8 @@ extern "C" { int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t serviceLabel, std::uint64_t options); int APS5_VABI sceNpTrophyDestroyContext(int context); int APS5_VABI sceNpTrophyCreateHandle(int* handle); +int APS5_VABI sceNpTrophyGetGameIcon(int contextId, int handle, void* buffer, std::uint64_t* size); +int APS5_VABI sceNpTrophyGetGroupIcon(int contextId, int handle, int groupId, void* buffer, std::uint64_t* size); int APS5_VABI sceNpTrophyDestroyHandle(int handle); int APS5_VABI sceNpTrophyRegisterContext(int context, int handle, std::uint64_t options); int APS5_VABI sceNpTrophyGetTrophyUnlockState(int context, int handle, std::uint32_t* flags, std::uint32_t* count); @@ -108,6 +110,12 @@ int main() { Require(sceNpTrophyCreateContext(&missing, 1, 1, 0) == 0, "context for a label without a package"); Require(sceNpTrophyRegisterContext(missing, handle, 0) == TitleConfNotInstalled, "register without a trophy package"); + Require(sceNpTrophyCreateHandle(nullptr) == InvalidArgument, "null handle creation"); + Require(sceNpTrophyCreateContext(nullptr, 1, 0, 0) == InvalidArgument, "null context creation"); + Require(sceNpTrophyGetGameIcon(context, handle, icon, nullptr) == InvalidArgument, "null game icon size pointer"); + Require(sceNpTrophyGetGroupIcon(context, handle, 0, icon, nullptr) == InvalidArgument, "null group icon size pointer"); + Require(sceNpTrophyGetTrophyIcon(context, handle, 0, icon, nullptr) == InvalidArgument, "null trophy icon size pointer"); + Require(sceNpTrophyDestroyHandle(-1) == InvalidHandle, "invalid handle destruction"); Require(sceNpTrophyDestroyContext(missing) == 0, "destroy the second context"); Require(sceNpTrophyDestroyHandle(handle) == 0 && sceNpTrophyDestroyContext(context) == 0, "destroy the handle and context"); Require(sceNpTrophyDestroyContext(context) == InvalidContext, "destroy the context twice"); From c6b0f8ac9d79d7960659267c93676325e5574b78 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:34:28 -0500 Subject: [PATCH 22/63] test(libSceVideoOut): cover null status buffer validation in sceVideoOutGetResolutionStatus --- core/libs/tests/GuestVideoOutResolutionStatus.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestVideoOutResolutionStatus.cpp b/core/libs/tests/GuestVideoOutResolutionStatus.cpp index e401ad3023..fb4d14f108 100644 --- a/core/libs/tests/GuestVideoOutResolutionStatus.cpp +++ b/core/libs/tests/GuestVideoOutResolutionStatus.cpp @@ -68,6 +68,8 @@ int main() { for (const auto byte : vrr) Require(byte == std::byte{0xff}); Require(Rejects(handle, nullptr)); + Require(sceVideoOutGetResolutionStatus(handle, nullptr) == static_cast(0x80290002u)); + Require(sceVideoOutGetResolutionStatus(-1, nullptr) == static_cast(0x80290002u)); Require(Rejects(0, &status)); Require(Rejects(-1, &status)); Require(Rejects(NEVER_OPENED_HANDLE, &status)); From aa276cd7457508f0a479b83bc76a6bda18372950 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:34:40 -0500 Subject: [PATCH 23/63] test(libSceConvertKeycode): cover null address and invalid keyboard type rejection --- core/libs/tests/GuestConvertKeycodeTitle.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestConvertKeycodeTitle.cpp b/core/libs/tests/GuestConvertKeycodeTitle.cpp index 6fd7337573..20bb8d6656 100644 --- a/core/libs/tests/GuestConvertKeycodeTitle.cpp +++ b/core/libs/tests/GuestConvertKeycodeTitle.cpp @@ -8,6 +8,8 @@ extern "C" { int APS5_VABI sceConvertKeycodeGetImeKeyboardType(std::int32_t, std::uint32_t*); +int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint16_t, std::uint32_t, std::uint16_t*); +int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t, std::uint32_t, std::uint32_t, char16_t*); } int main() { @@ -21,5 +23,11 @@ int main() { std::memcpy(process.data() + 16, &sdkVersion, sizeof(sdkVersion)); if (!ProcessParametersFixture::Install(process.data(), process.size())) return 2; std::uint32_t type = 0xFFFFFFFFu; + std::uint16_t virtualKey = 0; + char16_t character = 0; + if (sceConvertKeycodeGetVirtualKeycode(0, 0, nullptr) != static_cast(0x80BC0031u)) return 1; + if (sceConvertKeycodeGetVirtualKeycode(0, 0, &virtualKey) != static_cast(0x80BC0034u)) return 1; + if (sceConvertKeycodeGetCharacter(0, 0, 0, nullptr) != static_cast(0x80BC0031u)) return 1; + if (sceConvertKeycodeGetCharacter(0, 0, 0, &character) != static_cast(0x80BC0034u)) return 1; return sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4 ? 0 : 1; } From 7753a92d3249b935d403d01a2639760e6dc4900e Mon Sep 17 00:00:00 2001 From: Filip Michalski Date: Wed, 21 Oct 2026 14:27:12 +0200 Subject: [PATCH 24/63] perf(shader): avoid scanning the final IR operand use --- .../src/IrValue.cpp | 8 +++++--- .../tests/UseRewrite.cpp | 20 ++++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index a7cc1db50d..6d1e69deb7 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -39,9 +39,11 @@ void IrValue::ReplaceArgument(std::size_t index, IrValue* argument) { } IrValue* previous = arguments[index]; if (previous != nullptr) { - const auto operandIt = std::find_if(previous->operandUses.begin(), previous->operandUses.end(), [this, index](const IrUse& use) { - return use.user == this && use.operand == index; - }); + const auto operandIt = !previous->operandUses.empty() && previous->operandUses.back() == IrUse{this, index} + ? previous->operandUses.end() - 1 + : std::find_if(previous->operandUses.begin(), previous->operandUses.end(), [this, index](const IrUse& use) { + return use.user == this && use.operand == index; + }); if (operandIt != previous->operandUses.end()) { previous->operandUses.erase(operandIt); } diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp index 0849790845..f5a711b969 100644 --- a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp @@ -101,6 +101,23 @@ void CheckSimpleReplacement() { Require(source->Opcode() == IrOpcode::IAdd32 && source->Parent() == &graph.block); } +void CheckTailArgumentRemoval() { + Graph graph(4); + auto* source = graph.Value(0); + auto* first = graph.Value(1); + auto* last = graph.Value(2); + first->AddArgument(source); + last->AddArgument(source); + last->AddArgument(source); + last->ReplaceArgument(0, nullptr); + Require(source->OperandUses() == std::vector({{first, 0}, {last, 1}})); + last->ReplaceArgument(1, graph.Value(3)); + Require(source->OperandUses() == std::vector({{first, 0}})); + Require(graph.Value(3)->OperandUses() == std::vector({{last, 1}})); + first->Invalidate(); + Require(!source->HasUses() && first->Argument(0) == nullptr); +} + void CheckSelfAndEmpty() { Graph graph(3); graph.Value(2)->AddArgument(graph.Value(0)); @@ -223,6 +240,7 @@ int main() { try { const std::pair cases[] = { {"ordered duplicate operands", CheckSimpleReplacement}, + {"tail argument removal", CheckTailArgumentRemoval}, {"self and empty replacement", CheckSelfAndEmpty}, {"phi and identity rewriting", CheckPhiAndIdentity}, {"allocation failure integrity", CheckAllocationFailure}, @@ -234,7 +252,7 @@ int main() { current = name; test(); } - std::puts("7 IR rewrite suites passed; 2000 differential use graphs and 48000 mutation comparisons"); + std::puts("8 IR rewrite suites passed; 2000 differential use graphs and 48000 mutation comparisons"); return 0; } catch (const std::exception& error) { failureAfter = -1; From b10424a47bf3efe4a07972d031879579999a687e Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:43:37 +0000 Subject: [PATCH 25/63] fix(relinker): require the Linux entry, relocation offsets and DT_PLTGOT in PT_LOAD memory The Windows and macOS patchers reject an entry point outside executable PT_LOAD memory and a relocation offset or DT_PLTGOT outside PT_LOAD memory through WindowsLoadImage::GetRva. The Linux patcher copied these values unchecked and reported success; with a bad entry or relocation offset the output crashed with SIGSEGV at load. LinuxElfPatcher::Patch now requires e_entry in an executable load and every non-NONE RELA and JMPREL r_offset and DT_PLTGOT to fit inside one PT_LOAD's MemorySize, in the fixed-base and --lazy-binding paths. A relocation needs the bytes ld.so writes: 4 for PC32 and 32, the output symbol's st_size (at least 1) for COPY, and 8 otherwise. DT_PLTGOT needs 8 bytes with eager binding and 24 with --lazy-binding, where ld.so writes GOT[1] and GOT[2]. Two test fixtures that set e_entry to a file offset outside their only PT_LOAD now use the matching virtual address. --- core/relinker/CMakeLists.txt | 2 + .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 34 ++++++ .../tests/test_linux_eh_frame_header.py | 2 +- .../tests/test_linux_load_alignment.py | 2 +- .../relinker/tests/test_linux_load_bounds.py | 114 ++++++++++++++++++ 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 core/relinker/relinker/tests/test_linux_load_bounds.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 5c4b89dfa8..7c091a9c5a 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -527,6 +527,8 @@ if(BUILD_TESTING) set_tests_properties(macos_tls_alu PROPERTIES TIMEOUT 60) add_test(NAME linux_load_alignment COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_load_alignment.py $) + add_test(NAME linux_load_bounds COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_load_bounds.py $) add_test(NAME linux_eh_frame_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_eh_frame_header.py $) add_test(NAME linux_dynamic_strings COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 7453d82aca..883abd7e46 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -22,6 +22,8 @@ namespace Elfpatcher::Linux { namespace { constexpr std::uint64_t kLinuxUserAddressEnd = 0x800000000000; +constexpr std::uint64_t kLazyPltGotBytes = 24; +constexpr std::uint64_t kPltGotEntryBytes = 8; std::string Hex(const std::uint64_t value) { std::ostringstream text; @@ -103,6 +105,31 @@ void RequireFileRange(const std::vector& buf, const Domain::Progra throw Domain::RelinkerException("PT_LOAD at " + Hex(load.MappedAddress) + " has file bytes outside the input"); } +void RequireLoaded(const std::vector& headers, const std::uint64_t address, const std::uint64_t size, const std::uint32_t flags, const std::string& what) { + for (const auto& header : headers) + if (header.Type == PT_LOAD && (header.Flags & flags) == flags && address >= header.MappedAddress && address - header.MappedAddress < header.MemorySize && size <= header.MemorySize - (address - header.MappedAddress)) + return; + const auto range = size > 1 ? " (" + std::to_string(size) + " bytes) does not fit inside " : std::string(" is not inside "); + throw Domain::RelinkerException(what + " " + Hex(address) + range + ((flags & PF_X) != 0 ? "an executable" : "a") + " PT_LOAD segment"); +} + +std::uint64_t RelocationWidth(const std::vector& relocations, const std::size_t entry, const std::vector& symbols) { + const auto info = Io::ReadU64(relocations, entry + 8); + switch (static_cast(info)) { + case R_X86_64_PC32: + case R_X86_64_32: + return 4; + case R_X86_64_COPY: { + const auto symbol = info >> 32; + if (symbol >= symbols.size() / kSymEntrySize) + throw Domain::RelinkerException("COPY relocation symbol index " + std::to_string(symbol) + " is outside the dynamic symbol table"); + return std::max(Io::ReadU64(symbols, static_cast(symbol * kSymEntrySize + 16)), 1); + } + default: + return 8; + } +} + void SeparateSharedLoadPages(std::vector& buf, std::vector& headers) { std::vector loads; for (std::size_t index = 0; index < headers.size(); ++index) { @@ -358,6 +385,13 @@ std::vector LinuxElfPatcher::Patch( throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); if (_fixedBase) RequireOnlyDependencies(dynSection.DynamicSegmentData); + RequireLoaded(originalHeaders, Io::ReadU64(sourceElf, kEhdrEntryOffset), 1, PF_X, "e_entry"); + for (const auto& [relocationTable, name] : {std::pair{&dynSection.RelaData, "RELA r_offset"}, std::pair{&dynSection.RelaPltData, "JMPREL r_offset"}}) + for (std::size_t entry = 0; entry < relocationTable->size(); entry += kRelaEntrySize) + if (static_cast(Io::ReadU64(*relocationTable, entry + 8)) != R_X86_64_NONE) + RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry, dynSection.DynSymData), 0, name); + if (!dynSection.RelaPltData.empty()) + RequireLoaded(originalHeaders, originalPltGotVaddr, lazyBinding ? kLazyPltGotBytes : kPltGotEntryBytes, 0, "DT_PLTGOT"); const auto traced = _traceImports ? TracedImports(dynSection) : std::vector{}; const std::uint64_t base = _fixedBase.value_or(0); const auto address = [this, base](const std::uint64_t vaddr) { diff --git a/core/relinker/relinker/tests/test_linux_eh_frame_header.py b/core/relinker/relinker/tests/test_linux_eh_frame_header.py index a80a1eb1ed..88d77abe53 100644 --- a/core/relinker/relinker/tests/test_linux_eh_frame_header.py +++ b/core/relinker/relinker/tests/test_linux_eh_frame_header.py @@ -18,7 +18,7 @@ def fixture(header_count, with_frame=True): image = bytearray(0x8000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) struct.pack_into(" Date: Fri, 23 Oct 2026 20:45:22 -0400 Subject: [PATCH 26/63] fix(libkernel): return EINTR from pselect when its mask unblocks a pending signal FreeBSD's kern_pselect installs the mask and kern_select sleeps with PCATCH, so a pending signal that the new mask unblocks makes the call return EINTR once the descriptors have been scanned. Here the handler ran inside the mask swap and select then waited the full timeout, or forever with no timeout. The swap now reports whether a handler ran, and pselect then scans the descriptors once without waiting: a ready descriptor, a zero timeout or an invalid argument keep select's answer, and otherwise the call fails with EINTR and leaves the descriptor sets unchanged. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libkernel/Socket/src/Pselect.cpp | 68 ++++++++++++++---- .../libs/prx/libkernel/System/src/Signals.cpp | 70 +++++++++++-------- core/libs/tests/GuestKernelSysPselect.cpp | 69 ++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 4 files changed, 166 insertions(+), 43 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Pselect.cpp b/core/libs/prx/libkernel/Socket/src/Pselect.cpp index 931cc83a54..66996aadfa 100644 --- a/core/libs/prx/libkernel/Socket/src/Pselect.cpp +++ b/core/libs/prx/libkernel/Socket/src/Pselect.cpp @@ -1,32 +1,76 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include +#include extern "C" { int* APS5_VABI __error_nid_postfix(); int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); +int APS5_VABI SetSignalMaskCatching_nid_no_patch(const void* set, void* previousSet); + +} + +namespace { + +constexpr int SetMask = 3; +constexpr int Eintr = 4; +constexpr int DescriptorLimit = 1024; + +struct Timeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +using SavedSet = std::array; + +void Save(SavedSet& saved, const void* set, std::size_t bytes) { + if (set != nullptr) std::memcpy(saved.data(), set, bytes); +} + +void Restore(void* set, const SavedSet& saved, std::size_t bytes) { + if (set != nullptr) std::memcpy(set, saved.data(), bytes); +} + +int ScanAfterCaughtSignal(int nfds, void* readfds, void* writefds, void* exceptfds, const Timeval* wait) { + const bool validWait = wait == nullptr || (wait->seconds >= 0 && wait->microseconds >= 0 && wait->microseconds < 1000000); + if (nfds < 0 || nfds > DescriptorLimit || !validWait) return select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); + const std::size_t bytes = static_cast((nfds + 63) / 64) * sizeof(std::uint64_t); + SavedSet read{}, write{}, except{}; + Save(read, readfds, bytes); + Save(write, writefds, bytes); + Save(except, exceptfds, bytes); + const Timeval zero{}; + const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, &zero); + if (result != 0 || (wait != nullptr && wait->seconds == 0 && wait->microseconds == 0)) return result; + Restore(readfds, read, bytes); + Restore(writefds, write, bytes); + Restore(exceptfds, except, bytes); + *__error_nid_postfix() = Eintr; + return -1; +} + +} + +extern "C" { int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask) { - constexpr int SetMask = 3; - struct Timeval { - std::int64_t seconds; - std::int64_t microseconds; - } limit{}; + Timeval limit{}; const Timeval* wait = nullptr; if (timeout != nullptr) { const auto* spec = static_cast(timeout); limit = {spec[0], spec[1] / 1000}; wait = &limit; } + if (sigmask == nullptr) return select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); std::uint32_t previous[4]{}; - if (sigmask != nullptr && sigprocmask_nid_postfix(SetMask, sigmask, previous) != 0) return -1; - const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); - if (sigmask != nullptr) { - const int error = *__error_nid_postfix(); - sigprocmask_nid_postfix(SetMask, previous, nullptr); - *__error_nid_postfix() = error; - } + const bool caught = SetSignalMaskCatching_nid_no_patch(sigmask, previous) != 0; + const int result = caught ? ScanAfterCaughtSignal(nfds, readfds, writefds, exceptfds, wait) + : select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); + const int error = *__error_nid_postfix(); + sigprocmask_nid_postfix(SetMask, previous, nullptr); + *__error_nid_postfix() = error; return result; } diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 0137030675..2a5b941662 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -478,6 +478,45 @@ namespace { constexpr int SsDisable = 4; constexpr std::size_t MinSignalStackSize = 2048; thread_local GuestStack alternateStack{nullptr, 0, SsDisable}; + +int ChangeSignalMask(int how, const GuestSignalSet* set, GuestSignalSet* previousSet, bool* caught) { + if (set != nullptr && (how < 1 || how > 3)) { *__error_nid_postfix() = 22; return -1; } + { + std::lock_guard lock(registration); + if (previousSet != nullptr) { + previousSet->bits[0] = blockedMask.load(); + for (int word = 1; word < 4; ++word) previousSet->bits[word] = blockedMaskExtra[word - 1].load(); + } + if (set != nullptr) { + switch (how) { + case 1: + blockedMask.fetch_or(set->bits[0] & ~UnblockableSignals); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); + break; + case 2: + blockedMask.fetch_and(~set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); + break; + case 3: + blockedMask.store(set->bits[0] & ~UnblockableSignals); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); + break; + } + } + } + std::atomic_thread_fence(std::memory_order_seq_cst); + for (int guest = 1; guest <= MaxSignal; ++guest) { + if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; + if (DeliverPendingSignal(guest) && caught != nullptr) *caught = true; + } + return 0; +} +} + +extern "C" int APS5_VABI SetSignalMaskCatching_nid_no_patch(const GuestSignalSet* set, GuestSignalSet* previousSet) { + bool caught = false; + ChangeSignalMask(GuestSigsetmask, set, previousSet, &caught); + return caught ? 1 : 0; } extern "C" { @@ -560,36 +599,7 @@ int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previ return 0; } int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { - if (set != nullptr && (how < 1 || how > 3)) { *__error_nid_postfix() = 22; return -1; } - { - std::lock_guard lock(registration); - if (previousSet != nullptr) { - previousSet->bits[0] = blockedMask.load(); - for (int word = 1; word < 4; ++word) previousSet->bits[word] = blockedMaskExtra[word - 1].load(); - } - if (set != nullptr) { - switch (how) { - case 1: - blockedMask.fetch_or(set->bits[0] & ~UnblockableSignals); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); - break; - case 2: - blockedMask.fetch_and(~set->bits[0]); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); - break; - case 3: - blockedMask.store(set->bits[0] & ~UnblockableSignals); - for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); - break; - } - } - } - std::atomic_thread_fence(std::memory_order_seq_cst); - for (int guest = 1; guest <= MaxSignal; ++guest) { - if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; - DeliverPendingSignal(guest); - } - return 0; + return ChangeSignalMask(how, set, previousSet, nullptr); } int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet) { diff --git a/core/libs/tests/GuestKernelSysPselect.cpp b/core/libs/tests/GuestKernelSysPselect.cpp index e3924a7f43..e3fbee46ed 100644 --- a/core/libs/tests/GuestKernelSysPselect.cpp +++ b/core/libs/tests/GuestKernelSysPselect.cpp @@ -15,10 +15,14 @@ int APS5_VABI close_nid_postfix(int descriptor); std::int64_t APS5_VABI write_nid_postfix(int descriptor, const char* buffer, std::int64_t size); int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask); +using Handler = void (APS5_VABI *)(int); +Handler APS5_VABI signal_nid_postfix(int guest, Handler handler); +int APS5_VABI raise_nid_postfix(int guest); } namespace { +constexpr int Eintr = 4; constexpr int Einval = 22; constexpr int SigBlock = 1; constexpr int SigSetmask = 3; @@ -59,6 +63,17 @@ bool Same(const SignalSet& left, const SignalSet& right) { return std::memcmp(left.bits, right.bits, sizeof(left.bits)) == 0; } +int caught = 0; + +void APS5_VABI Count(int) { + ++caught; +} + +void RaiseBlocked(const SignalSet& blocked) { + Require(sigprocmask_nid_postfix(SigBlock, &blocked, nullptr) == 0); + Require(raise_nid_postfix(SigUsr1) == 0 && caught == 0); +} + } int main() { @@ -101,6 +116,60 @@ int main() { const Timespec zero{0, 0}; Require(pselect_nid_postfix(-1, nullptr, nullptr, nullptr, &zero, nullptr) == -1 && Error() == Einval); } + Require(signal_nid_postfix(SigUsr1, Count) != reinterpret_cast(static_cast(-1))); + const SignalSet empty{}; + { + RaiseBlocked(blocked); + const SignalSet withUsr1 = CurrentMask(); + DescriptorSet read{}; + read.Add(writer); + const DescriptorSet original = read; + const Timespec limit{0, 300000000}; + const auto start = std::chrono::steady_clock::now(); + Require(pselect_nid_postfix(writer + 1, read.words.data(), nullptr, nullptr, &limit, &empty) == -1 && Error() == Eintr); + Require(std::chrono::steady_clock::now() - start < std::chrono::milliseconds(150)); + Require(caught == 1); + Require(read.words == original.words); + Require(Same(CurrentMask(), withUsr1)); + caught = 0; + } + { + RaiseBlocked(blocked); + Require(pselect_nid_postfix(0, nullptr, nullptr, nullptr, nullptr, &empty) == -1 && Error() == Eintr && caught == 1); + caught = 0; + } + { + RaiseBlocked(blocked); + DescriptorSet read{}; + read.Add(reader); + const Timespec limit{0, 300000000}; + Require(pselect_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit, &empty) == 1 && caught == 1); + Require(read.Has(reader)); + caught = 0; + } + { + RaiseBlocked(blocked); + const Timespec zero{0, 0}; + DescriptorSet read{}; + read.Add(writer); + Require(pselect_nid_postfix(writer + 1, read.words.data(), nullptr, nullptr, &zero, &empty) == 0 && caught == 1); + caught = 0; + } + { + RaiseBlocked(blocked); + const Timespec invalid{0, 1000000000}; + Require(pselect_nid_postfix(0, nullptr, nullptr, nullptr, &invalid, &empty) == -1 && Error() == Einval && caught == 1); + caught = 0; + } + { + RaiseBlocked(blocked); + const Timespec limit{0, 200000000}; + const auto start = std::chrono::steady_clock::now(); + Require(pselect_nid_postfix(0, nullptr, nullptr, nullptr, &limit, &blocked) == 0 && caught == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(150)); + Require(sigprocmask_nid_postfix(SigSetmask, &empty, nullptr) == 0 && caught == 1); + caught = 0; + } Require(sigprocmask_nid_postfix(SigSetmask, &before, nullptr) == 0); Require(close_nid_postfix(reader) == 0 && close_nid_postfix(writer) == 0); std::puts("pselect passed"); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 63512e62d1..b3200f639e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -158,7 +158,7 @@ - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store - [ktimer_create, ktimer_settime, ktimer_gettime, ktimer_getoverrun, ktimer_delete](../../../core/libs/prx/libkernel/Time/Ktimer.cpp) (libkernel) - a `SIGEV_SIGNAL` or `SIGEV_THREAD_ID` timer, and one created with a null `sigevent` (FreeBSD defaults it to `SIGEV_SIGNAL`), throws, because delivering a guest signal from a timer is not modelled. A non-zero value under one tick rounds up to 1 ms, as FreeBSD 11 does at hz 1000; the console's hz is not measured (`kern.clockrate` is unanswered). `SIGEV_NONE` timers on `CLOCK_REALTIME` and `CLOCK_MONOTONIC` follow `kern_time.c`: ids 3 to 31 (`EAGAIN` when full), `EINVAL` for another clock, a bad notify type or signal, an id that is not live or a bad `itimerspec` field, and `TIMER_ABSTIME` (1). Expiry is worked out from the guest clock when the timer is read, and `ktimer_getoverrun` returns 0, as FreeBSD only updates it when a signal is accepted - [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified -- [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask with `sigprocmask`, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does, but the swap is not atomic with the wait: a signal that becomes deliverable between the swap and the wait is not guaranteed to interrupt it +- [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does. A pending signal that the new mask unblocks has its handler run during the swap, and `pselect` then returns `EINTR` unless a descriptor is already ready, the timeout is zero or an argument is invalid, as FreeBSD's `kern_select` decides before it sleeps. A signal raised during the wait, including one sent by another thread, does not interrupt it, because `select` has no `EINTR` path; and the mask is the process-wide one, so it applies to every thread for the duration of the call - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only From 698697cbc74ffe79f62f8c72c3a273063827c774 Mon Sep 17 00:00:00 2001 From: orezraey Date: Sat, 24 Oct 2026 02:53:35 -0300 Subject: [PATCH 27/63] fix(libkernel): throw for an unmeasured unbalanced thread atexit decrement on a module Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Rtld.cpp | 6 ++---- core/libs/tests/GuestDynamicLoader.cpp | 2 +- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index 4a11c90be5..044ae1ef1c 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -66,10 +66,8 @@ int releaseThreadAtexitImage(const void* address, const char* caller) { { std::lock_guard lock(threadAtexitMutex); const auto found = threadAtexitReferences.find(image); - if (found == threadAtexitReferences.end()) { - APS5_LOG_OUT("%s: image holds no thread atexit reference", caller); - return 0; - } + if (found == threadAtexitReferences.end()) + throw std::runtime_error(std::string(caller) + ": image holds no thread atexit reference"); handle = found->second; threadAtexitReferences.erase(found); } diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 6013ae1f19..afcc355f26 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -229,7 +229,7 @@ int main(int argc, char** argv) { Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(add)) == 0); Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); - Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&Require)) == 0); + Require(ThrowsRuntimeError([] { _sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&Require)); })); int local = 0; Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(&local) == -1); Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(nullptr) == -1); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b8804e2c64..3db94d8388 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -77,7 +77,7 @@ - [Map hints inside a reservation](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMapDirectMemory` and the flexible and anonymous maps place a mapping without `MAP_FIXED` at its hint address when the whole range lies in a range still reserved by `sceKernelReserveVirtualRange`, as if `MAP_FIXED` were set. Taken from PPSA01474, which reserves 64 GiB and then maps its heaps inside it with hints and no `MAP_FIXED`, and uses the hint addresses afterwards; not measured on the console. Hints outside a reservation, or over a range that is partly mapped, stay hints - [Fixed anonymous mmap](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `MAP_PRIVATE | MAP_ANON | MAP_FIXED` replaces the requested range through the existing guest mapping machinery and zeroes the new memory. `MAP_EXCL` is checked under the allocation-registry mutation lock, including virtual reservations, and returns `ENOMEM` without changing an occupied range; without `MAP_FIXED` it is `EINVAL`. Lengths are rounded to 16 KiB and fixed addresses must be aligned. The complete rounded range must lie inside the guest arena or, for POSIX fixed anonymous `mmap` on Windows, one current or registered native thread stack; other mapping APIs do not opt into stack replacement. Native stack replacement keeps the reservation, decommits and recommits fresh pages, and rejects foreign allocations, ranges spanning reservations and host `PAGE_GUARD` pages before mutation. Current-stack ranges must end at least 64 KiB below the active host frame; callers replacing another thread's stack must coordinate with that thread. If recommitting fails, the call returns `ENOMEM` with the range still reserved; thread cleanup attempts to recommit it before restoring write access. This follows FreeBSD 11.4 [argument validation and rounding](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L226-L299), [fixed replacement](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1455-L1464), and [collision errno](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1604-L1613). The console behaviour has not been measured. AnyPS5 starts the guest arena at 0x200000000, so the fixed path rejects the low 0x400000 address requested by PICO-8 PPSA99808. The console address-space layout is unmeasured; executable placement outside the host arena is tracked in #5217. Static catalog disassembly finds `0x1012` in PPSA98001/PPSA99808 and `0x5012` in PPSA99808; these titles were not run. - [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start -- [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in a payload process on a PS5 (firmware 13.60), Increment returns -1 for an address outside every module (null, stack, heap) and 0 otherwise, and Decrement never fails, also without a matching Increment; the console's Decrement returns nothing, here 0. The module's `ref_count` did not change on the console. As in [fpPS4](https://github.com/red-prig/fpPS4), Increment here takes a reference on the image and Decrement releases one. The variants without the leading underscore are not exported by firmware 13.60's `libkernel_sys`; here they match +- [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in a payload process on a PS5 (firmware 13.60), Increment returns -1 for an address outside every module (null, stack, heap) and 0 otherwise, and Decrement never fails on an address outside every module or after a matching Increment; the console's Decrement returns nothing, here 0. A Decrement on a module's address without a matching Increment was not measured and throws here. The module's `ref_count` did not change on the console. As in [fpPS4](https://github.com/red-prig/fpPS4), Increment here takes a reference on the image and Decrement releases one. The variants without the leading underscore are not exported by firmware 13.60's `libkernel_sys`; here they match - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown - [cfsetspeed](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel, NID `C1MQ6OaXRjo`) - console behavior and module/library versions are unverified. The prototype, uint32 speed and 44-byte termios layout (input speed at 36, output speed at 40) follow the public PS5 payload SDK [termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/termios.h) and [sys/_termios.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/sys/_termios.h). The module association follows its libkernel stub table and the NID rehashes to the aerolib name. Both speed fields accept every uint32 value and no flags or control characters change, as [FreeBSD 11.4 termios.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/gen/termios.c) does. No terminal I/O is performed. Null pointers throw where the reference dereferences them. - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled. A kqueue is readable while one of its events is triggered and is never writable or in the exception set, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers, on every host. Other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket From 138bde1cd60be574bf79782d161ec4a0cf956ff3 Mon Sep 17 00:00:00 2001 From: orezraey Date: Sat, 24 Oct 2026 03:08:51 -0300 Subject: [PATCH 28/63] test(libkernel): drop the assertion on the unsupported thread atexit decrement Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestDynamicLoader.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index afcc355f26..d9de8873df 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -229,7 +229,6 @@ int main(int argc, char** argv) { Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(add)) == 0); Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); - Require(ThrowsRuntimeError([] { _sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&Require)); })); int local = 0; Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(&local) == -1); Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(nullptr) == -1); From 676a0e007250a92f8f768e95cb4cbf713abc57cc Mon Sep 17 00:00:00 2001 From: atharvjairath <54663702+atharvjairath@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:40:33 +0530 Subject: [PATCH 29/63] feat(fiber): execute contextless fibers on the caller stack --- core/libs/prx/libSceFiber/CMakeLists.txt | 4 +- core/libs/prx/libSceFiber/Export.cpp | 46 ++++-- core/libs/tests/Fiber.cpp | 198 ++++++++++++++++++++++- docs/dev/debt/libSceFiber.md | 2 +- 4 files changed, 230 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceFiber/CMakeLists.txt b/core/libs/prx/libSceFiber/CMakeLists.txt index 883b7fc154..ef08d950c5 100644 --- a/core/libs/prx/libSceFiber/CMakeLists.txt +++ b/core/libs/prx/libSceFiber/CMakeLists.txt @@ -8,9 +8,11 @@ if(BUILD_TESTING) target_include_directories(fiber_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(fiber_tests PRIVATE libSceFiber libc) add_test(NAME fiber COMMAND fiber_tests) + add_test(NAME fiber_contextless COMMAND fiber_tests --contextless) add_asan_test_options(fiber detect_stack_use_after_return=0) + add_asan_test_options(fiber_contextless detect_stack_use_after_return=0) if(WIN32) - set_tests_properties(fiber PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(fiber fiber_contextless PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index e214387ae3..535bf1d7e9 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -107,14 +107,17 @@ static bool TraceFibers() { static void CompletePendingSuspend() { auto& thread = ThreadState(); if (thread.pendingSuspend) { - thread.pendingSuspend->state.store(FiberState::Suspended, std::memory_order_release); + auto* fiber = thread.pendingSuspend; + const auto state = fiber->context ? FiberState::Suspended : FiberState::Idle; + if (!fiber->context) fiber->savedStack = nullptr; + fiber->state.store(state, std::memory_order_release); thread.pendingSuspend = nullptr; } } #ifdef _WIN32 -extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void* load); +extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void** load, Fiber* start); extern "C" void Aps5FiberTrampoline_nid_no_patch(); asm(R"( @@ -144,7 +147,12 @@ Aps5FiberSwitchStack_nid_no_patch: stmxcsr 0xa0(%rsp) fnstcw 0xa4(%rsp) mov %rsp, (%rcx) - mov %rdx, %rsp + mov (%rdx), %rsp + test %r8, %r8 + jz 1f + mov %r8, %r12 + jmp Aps5FiberTrampoline_nid_no_patch +1: movaps 0x00(%rsp), %xmm6 movaps 0x10(%rsp), %xmm7 movaps 0x20(%rsp), %xmm8 @@ -215,7 +223,7 @@ static void UnpinStack(const void* context, std::uint64_t bytes) { #else -extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void* load); +extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void** load, Fiber* start); extern "C" void Aps5FiberTrampoline_nid_no_patch(); asm(".text\n" @@ -230,7 +238,12 @@ asm(".text\n" stmxcsr 0(%rsp) fnstcw 4(%rsp) mov %rsp, (%rdi) - mov %rsi, %rsp + mov (%rsi), %rsp + test %rdx, %rdx + jz 1f + mov %rdx, %r12 + jmp )" APS5_ASM_SYMBOL("Aps5FiberTrampoline_nid_no_patch") R"( +1: ldmxcsr 0(%rsp) fldcw 4(%rsp) add $8, %rsp @@ -338,6 +351,7 @@ static std::uint8_t* StackTop(const Fiber* fiber) { } static std::uint64_t* SizedWindow(const Fiber* fiber) { + if (!fiber->context) return nullptr; return reinterpret_cast(StackTop(fiber) - fiber->contextSize); } @@ -363,6 +377,10 @@ static Fiber* AsFiber(FiberObject* object) { extern "C" [[noreturn]] void Aps5FiberMain_nid_no_patch(Fiber* fiber) { CompletePendingSuspend(); + if (!fiber->context && fiber->setFpuRegisters) { + _mm_setcsr(FIBER_DEFAULT_MXCSR); + asm volatile("fldcw %0" : : "m"(FIBER_DEFAULT_FPU_CONTROL)); + } fiber->entry(fiber->argOnInitialize, ThreadState().transfer); throw std::runtime_error(std::string("sceFiber: entry function of fiber '") + fiber->name + "' returned"); } @@ -393,13 +411,7 @@ static bool AcquireForResume(Fiber* target) { } if (state != FiberState::Idle && state != FiberState::Suspended) return false; if (target->state.compare_exchange_weak(state, FiberState::Running, std::memory_order_acq_rel)) { - if (state == FiberState::Idle) { - if (!target->context) { - target->state.store(FiberState::Idle, std::memory_order_release); - throw std::runtime_error(std::string("sceFiber: fiber '") + target->name + "' has no context; running a fiber on the stack of its caller is not supported"); - } - PrepareInitialStack(target); - } + if (state == FiberState::Idle && target->context) PrepareInitialStack(target); return true; } } @@ -447,7 +459,7 @@ static int32_t AcquireWithContext(Fiber* target, void* addr_context, uint64_t si static void Resume(Fiber* target, void** save, std::uint64_t argOnRun) { const auto* frame = static_cast(target->savedStack); - if (frame->returnAddress == 0) { + if (target->context && frame->returnAddress == 0) { std::fprintf(stderr, "[fiber] resuming '%s' with a wiped context: saved=%p context=%p size=0x%llx\n", target->name, target->savedStack, static_cast(target->context), static_cast(target->contextSize)); const auto* words = static_cast(target->savedStack); @@ -457,8 +469,8 @@ static void Resume(Fiber* target, void** save, std::uint64_t argOnRun) { } ThreadState().current = target; ThreadState().transfer = argOnRun; - SetBounds(FiberBounds(target)); - Aps5FiberSwitchStack_nid_no_patch(save, target->savedStack); + SetBounds(target->context ? FiberBounds(target) : ThreadState().threadBounds); + Aps5FiberSwitchStack_nid_no_patch(save, target->context ? &target->savedStack : &ThreadState().threadStack, target->context ? nullptr : target); } static int32_t InitializeFiber(FiberObject* object, const char* name, GuestFiberEntry entry, uint64_t arg_on_initialize, void* addr_context, uint64_t size_context, uint32_t flags) { @@ -559,7 +571,7 @@ int32_t APS5_VABI sceFiberReturnToThread(uint64_t arg_on_return, uint64_t* arg_o ThreadState().current = nullptr; ThreadState().transfer = arg_on_return; SetBounds(ThreadState().threadBounds); - Aps5FiberSwitchStack_nid_no_patch(&self->savedStack, ThreadState().threadStack); + Aps5FiberSwitchStack_nid_no_patch(&self->savedStack, &ThreadState().threadStack, nullptr); CompletePendingSuspend(); if (arg_on_run) *arg_on_run = ThreadState().transfer; return SCE_OK; @@ -582,7 +594,7 @@ int32_t APS5_VABI sceFiberGetInfo(FiberObject* object, FiberInfo* fiber_info) { fiber_info->size_context = fiber->contextSize; std::memcpy(fiber_info->name, fiber->name, sizeof(fiber_info->name)); fiber_info->size_context_margin = static_cast(-1); - if (fiber->contextSizeCheck) { + if (fiber->contextSizeCheck && fiber->context) { const auto* words = SizedWindow(fiber); const auto* end = words + fiber->contextSize / sizeof(std::uint64_t); fiber_info->size_context_margin = static_cast(std::find_if(words, end, [](std::uint64_t word) { return word != FIBER_CONTEXT_FILL; }) - words) * sizeof(std::uint64_t); diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index f6b532900b..d8a6880d7c 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #ifdef _WIN32 @@ -325,6 +326,195 @@ void RunInternalOptions() { Require(sceFiberFinalize(fiber) == 0, "finalize fpu defaults fiber"); } +std::array g_contextlessStorage; +std::array g_contextlessEntries{}; +alignas(16) std::array g_transitionContext; +FiberStorage g_transitionStorage; +auto* const g_transition = reinterpret_cast(&g_transitionStorage); +std::uintptr_t g_callerStack = 0; +std::uint64_t g_contextlessFramePointer = 0; +unsigned g_contextlessHops = 0; +#ifdef _WIN32 +void* g_callerBase = nullptr; +void* g_callerLimit = nullptr; +#endif + +FiberObject* Contextless(unsigned index) { + return reinterpret_cast(&g_contextlessStorage[index]); +} + +void APS5_VABI ContextlessEntry(std::uint64_t index, std::uint64_t run) { + ++g_contextlessEntries[index]; + auto* self = Contextless(index); + FiberObject* current = nullptr; + Require(sceFiberGetSelf(¤t) == 0 && current == self, "contextless entry self"); + FiberInfo info{}; + info.size = sizeof(info); + Require(sceFiberGetInfo(self, &info) == 0, "contextless entry info"); + if (info.addr_context) { + Require(run == 90, "attaching after contextless execution restarts entry"); + for (;;) sceFiberReturnToThread(run + 1, &run); + } + Require(info.size_context == 0 && info.size_context_margin == ~0ull, "contextless info has no stack or margin"); + std::uint64_t framePointer = 0; + Require(sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_contextlessFramePointer, "contextless entry preserves the caller frame pointer"); + Require(sceFiberRun_nid_postfix(self, 0, nullptr) == FiberErrorPermission, "contextless entry cannot run a fiber"); + Require(sceFiberSwitch(self, 0, nullptr) == FiberErrorState, "contextless entry cannot switch to itself"); + Require(sceFiberFinalize(self) == FiberErrorState, "contextless entry cannot finalize itself"); + volatile std::uint64_t marker = 0; + const auto address = reinterpret_cast(&marker); + Require(address < g_callerStack && g_callerStack - address < 32768, "contextless execution uses calling thread stack"); + _mm_setcsr(_mm_getcsr() ^ 0x8000u); +#ifdef _WIN32 + const auto* tib = reinterpret_cast(NtCurrentTeb()); + Require(tib->StackBase == g_callerBase && tib->StackLimit == g_callerLimit, "contextless execution restores thread stack bounds"); +#endif + if (run == 2) { + sceFiberSwitch(Contextless(1), 1, nullptr); + } else if (run == 3) { + _sceFiberAttachContextAndSwitch(g_transition, g_transitionContext.data(), g_transitionContext.size(), 50, nullptr); + } else if (run == 4) { + sceFiberSwitch(g_transition, 70, nullptr); + } else if (run == 5) { + if (++g_contextlessHops < 1024) sceFiberSwitch(Contextless(1 - index), 5, nullptr); + sceFiberReturnToThread(999, nullptr); + } else { + Require(run == 1, "contextless run argument"); + sceFiberReturnToThread(200 + index, nullptr); + } + Require(false, "contextless fiber must restart rather than resume after yielding"); +} + +void APS5_VABI TransitionEntry(std::uint64_t, std::uint64_t run) { + Require(run == 50, "contextless source attaches target"); + Require(sceFiberReturnToThread(777, &run) == 0 && run == 55, "attached target resumes after contextless source"); + Require(_sceFiberAttachContextAndSwitch(Contextless(1), nullptr, 0, 1, &run) == 0 && run == 60, "attached source resumes after contextless target"); + Require(sceFiberSwitch(Contextless(0), 4, &run) == 0 && run == 70, "contextless target switches back to suspended attached source"); + for (;;) sceFiberReturnToThread(778, nullptr); +} + +[[gnu::noinline]] std::int32_t RunContextless(FiberObject* fiber, std::uint64_t run, std::uint64_t* returned, bool attach = false) { + register std::uint64_t savedRbx asm("rbx") = 0x123456789abcdef0ull; + register std::uint64_t savedR12 asm("r12") = 0x23456789abcdef01ull; + register std::uint64_t savedR13 asm("r13") = 0x3456789abcdef012ull; + register std::uint64_t savedR14 asm("r14") = 0x456789abcdef0123ull; + register std::uint64_t savedR15 asm("r15") = 0x56789abcdef01234ull; + asm volatile("" : "+r"(savedRbx), "+r"(savedR12), "+r"(savedR13), "+r"(savedR14), "+r"(savedR15)); + g_contextlessFramePointer = reinterpret_cast(__builtin_frame_address(0)); + const auto result = attach ? _sceFiberAttachContextAndRun(fiber, nullptr, 0, run, returned) : sceFiberRun_nid_postfix(fiber, run, returned); + asm volatile("" : "+r"(savedRbx), "+r"(savedR12), "+r"(savedR13), "+r"(savedR14), "+r"(savedR15) : : "memory"); + Require(savedRbx == 0x123456789abcdef0ull && savedR12 == 0x23456789abcdef01ull && savedR13 == 0x3456789abcdef012ull && savedR14 == 0x456789abcdef0123ull && savedR15 == 0x56789abcdef01234ull, "contextless return restores caller nonvolatile registers"); + return result; +} + +void RunContextlessTransitions() { + volatile std::uint64_t marker = 0; + g_callerStack = reinterpret_cast(&marker); +#ifdef _WIN32 + const auto* tib = reinterpret_cast(NtCurrentTeb()); + g_callerBase = tib->StackBase; + g_callerLimit = tib->StackLimit; +#endif + for (unsigned index = 0; index < 2; ++index) { + Require(_sceFiberInitializeWithInternalOptionImpl(Contextless(index), "contextless", ContextlessEntry, index, nullptr, 0, nullptr, 0x10, 0) == 0, "initialize contextless transition fiber with size checking"); + } + Require(_sceFiberInitializeImpl_nid_postfix(g_transition, "transition", TransitionEntry, 0, nullptr, 0, nullptr, 0) == 0, "initialize transition target"); + std::uint64_t returned = 0; + const auto csr = _mm_getcsr(); + Require(RunContextless(Contextless(0), 1, &returned) == 0 && returned == 200, "first contextless run"); + Require(_mm_getcsr() == csr, "contextless return restores thread MXCSR"); + Require(RunContextless(Contextless(0), 1, &returned) == 0 && returned == 200 && g_contextlessEntries[0] == 2, "contextless return discards continuation"); + Require(RunContextless(Contextless(0), 2, &returned, true) == 0 && returned == 201, "contextless to contextless switch"); + Require(RunContextless(Contextless(1), 1, &returned) == 0 && returned == 201 && g_contextlessEntries[1] == 2, "contextless switch target restarts"); + Require(RunContextless(Contextless(0), 3, &returned) == 0 && returned == 777, "contextless to attached switch"); + Require(RunContextless(g_transition, 55, &returned) == 0 && returned == 201, "attached to contextless switch"); + Require(RunContextless(g_transition, 60, &returned) == 0 && returned == 778, "switch through contextless entry back to attached fiber"); + Require(_sceFiberAttachContextAndRun(Contextless(0), g_firstContext.data(), g_firstContext.size(), 90, &returned) == 0 && returned == 91, "attach after abandoning contextless stack"); + Require(sceFiberRun_nid_postfix(Contextless(0), 100, &returned) == 0 && returned == 101 && g_contextlessEntries[0] == 6, "attached continuation persists after contextless runs"); + std::thread([&] { + volatile std::uint64_t workerMarker = 0; + g_callerStack = reinterpret_cast(&workerMarker); +#ifdef _WIN32 + const auto* workerTib = reinterpret_cast(NtCurrentTeb()); + g_callerBase = workerTib->StackBase; + g_callerLimit = workerTib->StackLimit; +#endif + Require(RunContextless(Contextless(1), 1, &returned) == 0 && returned == 201 && g_contextlessEntries[1] == 4, "contextless fiber restarts on another thread's stack"); + }).join(); + Require(sceFiberFinalize(Contextless(0)) == 0 && sceFiberFinalize(Contextless(1)) == 0 && sceFiberFinalize(g_transition) == 0, "finalize contextless transition fibers"); + g_callerStack = reinterpret_cast(&marker); +#ifdef _WIN32 + g_callerBase = tib->StackBase; + g_callerLimit = tib->StackLimit; +#endif + for (unsigned index = 0; index < 2; ++index) { + Require(_sceFiberInitializeImpl_nid_postfix(Contextless(index), "restart loop", ContextlessEntry, index, nullptr, 0, nullptr, 0) == 0, "initialize contextless restart loop"); + } + Require(RunContextless(Contextless(0), 5, &returned) == 0 && returned == 999 && g_contextlessHops == 1024, "contextless switch loop reuses the caller stack"); + Require(RunContextless(Contextless(1), 1, nullptr, true) == 0, "contextless run permits a null return output"); + Require(sceFiberFinalize(Contextless(0)) == 0 && sceFiberFinalize(Contextless(1)) == 0, "finalize contextless restart loop"); +} + +std::uint16_t ReadFpuControl() { + std::uint16_t control = 0; + asm volatile("fnstcw %0" : "=m"(control)); + return control; +} + +void WriteFpuControl(std::uint16_t control) { + asm volatile("fldcw %0" : : "m"(control)); +} + +constexpr std::uint64_t ContextlessInitializeArgument = 0xfedcba9876543210ull; +constexpr std::uint64_t ContextlessRunArgument = 0x8123456789abcdefull; +constexpr std::uint32_t ContextlessCsr = 0x7f80; +constexpr std::uint16_t ContextlessControl = 0x0b7f; +unsigned g_contextlessOptionEntries = 0; + +void APS5_VABI ContextlessOptionsEntry(std::uint64_t initialized, std::uint64_t run) { + Require(initialized == ContextlessInitializeArgument && run == ContextlessRunArgument, "contextless entry keeps 64-bit arguments"); + ++g_contextlessOptionEntries; + const auto result = (static_cast(ReadFpuControl()) << 32) | _mm_getcsr(); + _mm_setcsr(0x1f80); + WriteFpuControl(0x037f); + sceFiberReturnToThread(result, nullptr); + Require(false, "contextless options entry cannot resume after returning"); +} + +void APS5_VABI ContextlessFpuSource(std::uint64_t, std::uint64_t target) { + _mm_setcsr(ContextlessCsr); + WriteFpuControl(ContextlessControl); + sceFiberSwitch(reinterpret_cast(target), ContextlessRunArgument, nullptr); + Require(false, "contextless FPU source is not resumed"); +} + +void RunContextlessOptions() { + FiberStorage storage{}; + FiberStorage sourceStorage{}; + auto* fiber = reinterpret_cast(&storage); + auto* source = reinterpret_cast(&sourceStorage); + const auto callerCsr = _mm_getcsr(); + const auto callerControl = ReadFpuControl(); + for (const auto flags : {0u, 0x100u}) { + Require(_sceFiberInitializeWithInternalOptionImpl(fiber, "contextless options", ContextlessOptionsEntry, ContextlessInitializeArgument, nullptr, 0, nullptr, flags, 0) == 0, "initialize contextless FPU options"); + _mm_setcsr(ContextlessCsr); + WriteFpuControl(ContextlessControl); + const auto expected = flags ? (0x037full << 32) | 0x9fc0u : (static_cast(ContextlessControl) << 32) | ContextlessCsr; + std::uint64_t returned = 0; + for (unsigned restart = 0; restart < 2; ++restart) { + Require(sceFiberRun_nid_postfix(fiber, ContextlessRunArgument, &returned) == 0 && returned == expected, "contextless FPU policy applies on each run"); + Require(_mm_getcsr() == ContextlessCsr && ReadFpuControl() == ContextlessControl, "contextless return restores caller FPU registers"); + } + _mm_setcsr(callerCsr); + WriteFpuControl(callerControl); + Require(_sceFiberInitializeImpl_nid_postfix(source, "FPU source", ContextlessFpuSource, 0, g_transitionContext.data(), g_transitionContext.size(), nullptr, 0) == 0, "initialize attached FPU source"); + Require(sceFiberRun_nid_postfix(source, reinterpret_cast(fiber), &returned) == 0 && returned == expected, "contextless switch inherits or resets the source FPU registers"); + Require(_mm_getcsr() == callerCsr && ReadFpuControl() == callerControl, "contextless switch restores the original caller FPU registers"); + Require(sceFiberFinalize(fiber) == 0 && sceFiberFinalize(source) == 0, "finalize contextless FPU options"); + } + Require(g_contextlessOptionEntries == 6, "contextless FPU entry restarts on runs and switches"); +} + [[gnu::noinline]] std::int32_t RunFirst(std::uint64_t* returned) { g_threadFramePointer = reinterpret_cast(__builtin_frame_address(0)); const std::int32_t result = sceFiberRun_nid_postfix(g_first, 100, returned); @@ -334,7 +524,13 @@ void RunInternalOptions() { } -int main() { +int main(int argc, char** argv) { + if (argc == 2 && std::string_view(argv[1]) == "--contextless") { + RunContextlessTransitions(); + RunContextlessOptions(); + return 0; + } + Require(argc == 1, "known test arguments"); RunSmallContextFirst(); FiberObject* self = nullptr; Require(sceFiberGetSelf(&self) == FiberErrorPermission, "no fiber on the thread"); diff --git a/docs/dev/debt/libSceFiber.md b/docs/dev/debt/libSceFiber.md index f1c33e85c3..da142ae02b 100644 --- a/docs/dev/debt/libSceFiber.md +++ b/docs/dev/debt/libSceFiber.md @@ -8,6 +8,6 @@ - [_sceFiberInitializeWithInternalOptionImpl](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - its signature and the flag values `0x1` (no ulobjmgr registration), `0x10` (context size check) and `0x100` (start with MXCSR `0x9fc0` and x87 control word `0x037f`) follow shadPS4's [initialize implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L150-L153) and [flags](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.h#L27-L32). Not measured on a console. `0x1` has no effect because no ulobjmgr exists; other flag bits throw. shadPS4 also sets `0x100` for `build_version` 3.50 and later in every initialize; `opt_param` and `build_version` are ignored. - [_sceFiberAttachContextAndRun, _sceFiberAttachContextAndSwitch](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - signatures and behaviour follow shadPS4's [run](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L260-L261), [switch](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L336-L337) and [attach](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L46-L79) implementations, not measured on a console. A context is attached only to an idle fiber initialized without one; a fiber that already has a context, or is not idle, returns `SCE_FIBER_ERROR_INVALID`. A size that is not a multiple of 16 returns `SCE_FIBER_ERROR_ALIGNMENT` as in initialize, where shadPS4 returns `SCE_FIBER_ERROR_INVALID`. -- [Fibers without a context](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - running or switching to a fiber initialized without a context, before one is attached, throws. On the console such a fiber runs on the stack of its caller, per shadPS4's [run implementation](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L297-L313). +- [Fibers without a context](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - caller-stack execution and restart after yielding follow shadPS4 at commit `f68923fe807f657348e6719ce658bcbd0a2b1c72` ([run](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L238-L307), [switch](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L309-L360), [return](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/fiber/fiber.cpp#L376-L401)), not measurements on a PS5. The original calling thread stack is reused even when switching from an attached fiber; contextless continuations are discarded. - [_sceFiberGetThreadFramePointerAddress](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured - [Fiber stacks](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - a fiber whose context is smaller than 64 KiB runs on an mmap'd host stack of its context size plus 1 MiB, so host calls made from it (21 KiB deep when PPSA01341 reaches SPIRV-Tools from a 16 KiB context) do not overwrite the contexts packed next to it; a larger context is the stack, as on the console. PPSA05684 needs the second: its job fibers have 64 KiB and 512 KiB contexts, the 512 KiB ones at 1 MiB-aligned addresses, and find their job record by masking the stack pointer (`~0xfffff` for the 512 KiB ones), which a host stack breaks (the game waits forever in GameInit, or faults later in a 64 KiB fiber). The 64 KiB limit is chosen between these two titles, not measured: a title with small contexts that also derives state from the stack pointer, or one whose host calls exceed what is left of a large context, is not covered From 20311903c3a1eb112619396ac2bf0984f3d72af8 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:42:44 +0200 Subject: [PATCH 30/63] fix(shader): read a buffer V#'s presence at run time instead of specializing it A buffer V#'s presence came from the dispatch's V# snapshot as the specialization constant BufferBase + 2, which buildSpecializedModule turns into a literal. One guest program then compiled to two SPIR-V modules, one with the buffer bound and one with it still null, each a separate pipeline. descriptor() now reads presence from the V# words at run time, as registration modules already do, and the formatted-buffer helper takes it from its valid parameter. AI-assisted. Tested by me on my own machine. --- .../libSceAgcDriver/tests/GuardedMemoryLoads.cpp | 15 +++++++-------- .../Optimization/src/DescriptorBindingBuilder.cpp | 1 - .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 7 ++++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp index e20b23d8d5..452bac82c4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp @@ -32,7 +32,7 @@ struct LoadCase { std::uint32_t components = 1; }; -std::vector emitLoad(const LoadCase& test, bool active, std::uint32_t offset, std::uint32_t records, bool robust) { +std::vector emitLoad(const LoadCase& test, bool active, std::uint32_t offset, std::uint32_t records, bool robust, bool valid = true) { IrProgram program; program.SetWaveSize(32); program.Resources().stage = IrShaderStage::Compute; @@ -76,7 +76,8 @@ std::vector emitLoad(const LoadCase& test, bool active, std::uint program.Metadata().bindings.pushDataStartDword = 0; program.Metadata().bindings.memoryOffsetCount = 1; auto& size = constant(records); - auto& handle = emit(IrOpcode::GetBufferResource, IrType::BufferResource, {&zero, &zero, &size, &zero}); + auto& base = constant(valid ? 0x100u : 0u); + auto& handle = emit(IrOpcode::GetBufferResource, IrType::BufferResource, {&base, &zero, &size, &zero}); emit(test.opcode, test.type, {&handle, &zero, &address, &zero, &enabled}); } else { if (test.kind == ResourceKind::Gds) program.Metadata().bindings.descriptors.push_back({DescriptorBindingKind::Gds, {0}}); @@ -104,8 +105,8 @@ std::vector emitLoad(const LoadCase& test, bool active, std::uint class AccessTrace { public: - AccessTrace(const std::vector& module, std::uint32_t misalignment = 0, bool valid = true) - : words(module), misalignment(misalignment), valid(valid) { + AccessTrace(const std::vector& module, std::uint32_t misalignment = 0) + : words(module), misalignment(misalignment) { require(words.size() >= 5, "SPIR-V module header is missing"); for (std::size_t offset = 5; offset < words.size();) { const auto count = words[offset] >> 16; @@ -206,7 +207,6 @@ private: const auto spec = specializations.at(id); if (spec == PipelineSpecialization::BufferBase) return 0; if (spec == PipelineSpecialization::BufferBase + 1) return 0x30001000; - if (spec == PipelineSpecialization::BufferBase + 2) return valid; if (spec == PipelineSpecialization::BufferBase + 3) return misalignment; if (spec == PipelineSpecialization::PushDataOffset) return 0; throw std::runtime_error("unexpected specialization in access fixture"); @@ -250,7 +250,6 @@ private: const std::vector& words; std::uint32_t misalignment; - bool valid; std::uint32_t entryFunction = 0; std::map> definitions; std::map names; @@ -281,8 +280,8 @@ std::uint32_t ldsLength(const std::vector& words) { } void expectReads(const LoadCase& test, bool robust, bool active, std::uint32_t offset, std::vector expected, std::uint32_t records = 64, std::uint32_t misalignment = 0, bool valid = true) { - const auto module = emitLoad(test, active, offset, records, robust); - const auto actual = AccessTrace(module, misalignment, valid).Run(); + const auto module = emitLoad(test, active, offset, records, robust, valid); + const auto actual = AccessTrace(module, misalignment).Run(); if (actual != expected) { std::string detail = std::string(robust ? "robust" : "plain") + " guest reads:"; for (const auto index : actual) detail += " " + std::to_string(index); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index b3c679dc2e..1119918ed0 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -343,7 +343,6 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords; plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u}); plan.specialization.push_back({first + 1u, descriptor.dwords[3]}); - plan.specialization.push_back({first + 2u, present ? 1u : 0u}); plan.specialization.push_back({first + 3u, misalignment}); } if (buffer.formattedReadMask != 0u) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index f3736dc698..01fe275817 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -107,7 +107,9 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst } if (memory.gpuDescriptor) faultIf(context, instruction, invalid, base, 0u, BdaAbi::FaultReason::InvalidDescriptor); const auto stride = field(state, word1, 16u, 14u); - const auto present = runtime ? both(state, nonzero(state, context.Arg(*handle, 2u)), Binary(state, spv::OpLogicalOr, TypeBool(state), nonzero(state, context.Arg(*handle, 0u)), nonzero(state, field(state, word1, 0u, 16u)))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); + auto baseHigh = runtime ? word1 : context.Arg(*handle, 1u); + if (!runtime && GcnGuestShaders()) baseHigh = Binary(state, spv::OpBitwiseAnd, u32, baseHigh, ConstantU32(state, GcnBufferWord1Mask)); + const auto present = both(state, nonzero(state, context.Arg(*handle, 2u)), Binary(state, spv::OpLogicalOr, TypeBool(state), nonzero(state, context.Arg(*handle, 0u)), nonzero(state, field(state, baseHigh, 0u, 16u)))); const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : present; return {base, context.Arg(*handle, 2u), stride, word3, both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))), index, valid}; } @@ -668,8 +670,7 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr const auto word1 = state.module.SpecializationConstant(u32, first, 0u); const auto stride = field(state, word1, 16u, 14u); const auto swizzle = both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))); - const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u)); - shared = {ConstantU32(state, 0u), parameters[3], stride, state.module.SpecializationConstant(u32, first + 1u, 0u), swizzle, parameters[0], valid, parameters[1], parameters[2]}; + shared = {ConstantU32(state, 0u), parameters[3], stride, state.module.SpecializationConstant(u32, first + 1u, 0u), swizzle, parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]}; } const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[9] : 0u); if (store) state.module.AddFunction(spv::OpReturn); From 9bf8860f8757ce0a0543190d15e4f895ac2a133f Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:44:53 +0200 Subject: [PATCH 31/63] fix(relinker): drop the COPY width from the Linux load bounds check RelocationWidth took the width of a COPY relocation from the output symbol table, where every import has st_size 0, so the width was always one byte and the two copy-* boundary cases passed for that reason alone. #10658 rejects R_X86_64_COPY in validation, so the type no longer reaches this check; until it merges, COPY takes the 8-byte default like every other type. The three copy-* cases go with it. --- .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 20 ++++--------------- .../relinker/tests/test_linux_load_bounds.py | 13 ------------ 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 883abd7e46..3e9f0c0d27 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -113,21 +113,9 @@ void RequireLoaded(const std::vector& headers, const std: throw Domain::RelinkerException(what + " " + Hex(address) + range + ((flags & PF_X) != 0 ? "an executable" : "a") + " PT_LOAD segment"); } -std::uint64_t RelocationWidth(const std::vector& relocations, const std::size_t entry, const std::vector& symbols) { - const auto info = Io::ReadU64(relocations, entry + 8); - switch (static_cast(info)) { - case R_X86_64_PC32: - case R_X86_64_32: - return 4; - case R_X86_64_COPY: { - const auto symbol = info >> 32; - if (symbol >= symbols.size() / kSymEntrySize) - throw Domain::RelinkerException("COPY relocation symbol index " + std::to_string(symbol) + " is outside the dynamic symbol table"); - return std::max(Io::ReadU64(symbols, static_cast(symbol * kSymEntrySize + 16)), 1); - } - default: - return 8; - } +std::uint64_t RelocationWidth(const std::vector& relocations, const std::size_t entry) { + const auto type = static_cast(Io::ReadU64(relocations, entry + 8)); + return type == R_X86_64_PC32 || type == R_X86_64_32 ? 4 : 8; } void SeparateSharedLoadPages(std::vector& buf, std::vector& headers) { @@ -389,7 +377,7 @@ std::vector LinuxElfPatcher::Patch( for (const auto& [relocationTable, name] : {std::pair{&dynSection.RelaData, "RELA r_offset"}, std::pair{&dynSection.RelaPltData, "JMPREL r_offset"}}) for (std::size_t entry = 0; entry < relocationTable->size(); entry += kRelaEntrySize) if (static_cast(Io::ReadU64(*relocationTable, entry + 8)) != R_X86_64_NONE) - RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry, dynSection.DynSymData), 0, name); + RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry), 0, name); if (!dynSection.RelaPltData.empty()) RequireLoaded(originalHeaders, originalPltGotVaddr, lazyBinding ? kLazyPltGotBytes : kPltGotEntryBytes, 0, "DT_PLTGOT"); const auto traced = _traceImports ? TracedImports(dynSection) : std::vector{}; diff --git a/core/relinker/relinker/tests/test_linux_load_bounds.py b/core/relinker/relinker/tests/test_linux_load_bounds.py index d75d394cdf..49e9a994c4 100644 --- a/core/relinker/relinker/tests/test_linux_load_bounds.py +++ b/core/relinker/relinker/tests/test_linux_load_bounds.py @@ -14,12 +14,10 @@ PT_SCE_RELRO = 0x61000010 PF_W = 2 PF_R = 4 R_X86_64_PC32 = 2 -R_X86_64_COPY = 5 R_X86_64_32 = 10 E_ENTRY = 24 SPARE_HEADER = 64 + 3 * 56 PLTGOT_VALUE = 0x400 + 9 * 16 + 8 -SYMBOL_SIZE = 0x620 + 24 + 16 OUTSIDE = 0x2000 @@ -43,11 +41,6 @@ def with_pltgot(image, value): return image -def with_symbol_size(image, size): - struct.pack_into(" Date: Sat, 24 Oct 2026 18:09:58 +0200 Subject: [PATCH 32/63] test(relinker): expect the load bounds error for the wrapped GOT slot The got-slot-address-size-underflow case puts its GOT slot at the 32-bit wrapped address 0xfffff008, outside every PT_LOAD. The unused filter still keeps the import, which the case keeps checking through the CFG/GOT filtering line, and the Linux patcher now rejects the relocation, so the case expects exit 2 with that message instead of an output file. --- .../relinker/relinker/tests/test_unused_filter_indirect.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/relinker/relinker/tests/test_unused_filter_indirect.py b/core/relinker/relinker/tests/test_unused_filter_indirect.py index 7b0197ee6e..69e6e2f864 100644 --- a/core/relinker/relinker/tests/test_unused_filter_indirect.py +++ b/core/relinker/relinker/tests/test_unused_filter_indirect.py @@ -11,6 +11,8 @@ POINTER = 0x810 TABLE = 0x820 FUNCTION = CODE + 0x40 SECOND_FUNCTION = CODE + 0x60 +REJECTED = {"got-slot-address-size-underflow": + "RELA r_offset 0xfffff008 (8 bytes) does not fit inside a PT_LOAD segment"} def rel(next_address, target): @@ -62,6 +64,11 @@ def run(relinker, work, name, image, level=1): source.write_bytes(image) result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", f"unused-filter={level}", str(source), str(output)], capture_output=True, text=True, timeout=20) + if name in REJECTED: + assert result.returncode == 2 and REJECTED[name] in result.stderr and not output.exists(), ( + name, result.returncode, result.stdout, result.stderr) + assert "CFG/GOT filtering: 1 -> 1;" in result.stdout, (name, result.stdout) + return True assert result.returncode == 0 and output.exists(), (name, result.returncode, result.stdout, result.stderr) registry = json.loads(output.with_suffix(".registry.json").read_text()) kept = len(registry) From 1cb7e2e3e08a2a19617baf7dddfe3361f3c13068 Mon Sep 17 00:00:00 2001 From: louise-patra Date: Sat, 24 Oct 2026 19:42:23 +0530 Subject: [PATCH 33/63] feat(libSceNpUtility): implement lookup async requests, waits and timeouts Adds sceNpLookupAbortRequest, sceNpLookupCreateAsyncRequest, sceNpLookupPollAsync, sceNpLookupWaitAsync, sceNpLookupSetTimeout, sceNpWordFilterSetTimeout and sceNpWordFilterWaitAsync on the offline request model. Waits block on a condition variable that request completion, abort and deletion signal. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNpUtility/Export.cpp | 137 ++++++++++++------ core/libs/prx/libSceNpUtility/include/Api.hpp | 14 +- core/libs/tests/GuestNpUtilityOffline.cpp | 87 +++++++++++ docs/dev/debt/libSceNpUtility.md | 10 +- 4 files changed, 190 insertions(+), 58 deletions(-) diff --git a/core/libs/prx/libSceNpUtility/Export.cpp b/core/libs/prx/libSceNpUtility/Export.cpp index 7f5e3bbf94..bd009d549a 100644 --- a/core/libs/prx/libSceNpUtility/Export.cpp +++ b/core/libs/prx/libSceNpUtility/Export.cpp @@ -1,6 +1,7 @@ #include "prx/libSceNpUtility/include/Api.hpp" #include #include +#include #include #include #include @@ -23,7 +24,9 @@ static constexpr std::size_t MAX_REQUESTS = 32; static constexpr int WORD_FILTER_TITLE_CTX_ID_OFFSET = 0x01000000; static constexpr int WORD_FILTER_REQUEST_ID_OFFSET = 0x02000000; static constexpr std::size_t WORD_FILTER_COMMENT_MAX_LENGTH = 0x400; -static constexpr std::uint64_t WORD_FILTER_ASYNC_PARAMETER_SIZE = 0x18; +static constexpr std::uint64_t ASYNC_REQUEST_PARAMETER_SIZE = 0x18; +static constexpr std::uint32_t RESOLVE_TIMEOUT_MIN = 1000000; +static constexpr std::uint32_t TRANSFER_TIMEOUT_MIN = 10000000; namespace { struct Request { @@ -38,6 +41,7 @@ struct Request { struct Service { Service(int firstTitleContext, int firstRequest) : nextTitleContext(firstTitleContext), nextRequest(firstRequest) {} std::mutex mutex; + std::condition_variable finished; std::unordered_set titleContexts; std::unordered_map requests; int nextTitleContext; @@ -60,6 +64,7 @@ int DeleteTitleContext(Service& service, int titleCtxId) { std::lock_guard lock(service.mutex); if (!service.titleContexts.erase(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; std::erase_if(service.requests, [titleCtxId](const auto& entry) { return entry.second.titleCtxId == titleCtxId; }); + service.finished.notify_all(); return 0; } @@ -74,7 +79,9 @@ int CreateRequest(Service& service, int titleCtxId, bool async) { int DeleteRequest(Service& service, int reqId) { std::lock_guard lock(service.mutex); - return service.requests.erase(reqId) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!service.requests.erase(reqId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + service.finished.notify_all(); + return 0; } int StartOffline(Service& service, int reqId) { @@ -86,9 +93,58 @@ int StartOffline(Service& service, int reqId) { request->second.used = true; request->second.finished = true; request->second.result = SCE_NP_ERROR_SIGNED_OUT; + service.finished.notify_all(); return request->second.async ? 0 : SCE_NP_ERROR_SIGNED_OUT; } +int AbortRequest(Service& service, int reqId) { + std::lock_guard lock(service.mutex); + const auto request = service.requests.find(reqId); + if (request == service.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + request->second.aborted = true; + if (!request->second.finished) { + request->second.finished = true; + request->second.result = SCE_NP_COMMUNITY_ERROR_ABORTED; + service.finished.notify_all(); + } + return 0; +} + +int PollAsync(Service& service, int reqId, int* result, bool asyncOnly) { + std::lock_guard lock(service.mutex); + const auto request = service.requests.find(reqId); + if (request == service.requests.end() || (asyncOnly && !request->second.async)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.finished) return POLL_RUNNING; + if (result) *result = request->second.result; + return POLL_FINISHED; +} + +int WaitAsync(Service& service, int reqId, int* result, bool asyncOnly) { + std::unique_lock lock(service.mutex); + auto request = service.requests.find(reqId); + if (request == service.requests.end() || (asyncOnly && !request->second.async)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + service.finished.wait(lock, [&] { + request = service.requests.find(reqId); + return request == service.requests.end() || request->second.finished; + }); + if (request == service.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (result) *result = request->second.result; + return 0; +} + +bool ValidTimeout(std::uint32_t timeout, std::uint32_t minimum) { + return timeout == 0 || timeout >= minimum; +} + +int SetTimeout(Service& service, int id, int resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout) { + if (resolveRetry < 0 || !ValidTimeout(resolveTimeout, RESOLVE_TIMEOUT_MIN) || !ValidTimeout(connTimeout, TRANSFER_TIMEOUT_MIN) || + !ValidTimeout(sendTimeout, TRANSFER_TIMEOUT_MIN) || !ValidTimeout(recvTimeout, TRANSFER_TIMEOUT_MIN)) + return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (resolveRetry == 0 && resolveTimeout == 0 && connTimeout == 0 && sendTimeout == 0 && recvTimeout == 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(service.mutex); + return service.titleContexts.contains(id) || service.requests.contains(id) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; +} + int CheckComment(const char* comment) { return strnlen(comment, WORD_FILTER_COMMENT_MAX_LENGTH + 1) > WORD_FILTER_COMMENT_MAX_LENGTH ? SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT : 0; } @@ -237,14 +293,6 @@ int APS5_VABI sceNpBandwidthTestShutdownWithDetailedInfo() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpLookupAbortRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpLookupCreateAsyncRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceNpLookupCreateTitleCtxA() { NotImplemented_nid_no_patch(__func__); return 0; @@ -313,18 +361,6 @@ int APS5_VABI sceNpLookupNetTerm() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpLookupPollAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpLookupSetTimeout() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpLookupWaitAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceNpServiceChecker2IntAbortRequest() { NotImplemented_nid_no_patch(__func__); return 0; @@ -421,14 +457,6 @@ int APS5_VABI sceNpWordFilterCreateTitleCtxA() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpWordFilterSetTimeout() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceNpWordFilterWaitAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} } extern "C" { @@ -438,6 +466,15 @@ int APS5_VABI sceNpBandwidthTestInitStart() { return 0; } +int APS5_VABI sceNpLookupAbortRequest(int reqId) { + return AbortRequest(lookup, reqId); +} + +int APS5_VABI sceNpLookupCreateAsyncRequest(int titleCtxId, const std::uint64_t* param) { + if (!param || *param != ASYNC_REQUEST_PARAMETER_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return CreateRequest(lookup, titleCtxId, true); +} + int APS5_VABI sceNpLookupCreateRequest(int titleCtxId) { return CreateRequest(lookup, titleCtxId, false); } @@ -460,16 +497,20 @@ int APS5_VABI sceNpLookupNpId(int reqId, const NpOnlineId* onlineId, void* npId, return StartOffline(lookup, reqId); } +int APS5_VABI sceNpLookupPollAsync(int reqId, int* result) { + return PollAsync(lookup, reqId, result, false); +} + +int APS5_VABI sceNpLookupSetTimeout(int id, int resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout) { + return SetTimeout(lookup, id, resolveRetry, resolveTimeout, connTimeout, sendTimeout, recvTimeout); +} + +int APS5_VABI sceNpLookupWaitAsync(int reqId, int* result) { + return WaitAsync(lookup, reqId, result, false); +} + int APS5_VABI sceNpWordFilterAbortRequest(int reqId) { - std::lock_guard lock(wordFilter.mutex); - const auto request = wordFilter.requests.find(reqId); - if (request == wordFilter.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; - request->second.aborted = true; - if (!request->second.finished) { - request->second.finished = true; - request->second.result = SCE_NP_COMMUNITY_ERROR_ABORTED; - } - return 0; + return AbortRequest(wordFilter, reqId); } int APS5_VABI sceNpWordFilterCensorComment(int reqId, const char* comment, void* option) { @@ -480,7 +521,7 @@ int APS5_VABI sceNpWordFilterCensorComment(int reqId, const char* comment, void* } int APS5_VABI sceNpWordFilterCreateAsyncRequest(int titleCtxId, const std::uint64_t* param) { - if (!param || *param != WORD_FILTER_ASYNC_PARAMETER_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!param || *param != ASYNC_REQUEST_PARAMETER_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; return CreateRequest(wordFilter, titleCtxId, true); } @@ -502,12 +543,7 @@ int APS5_VABI sceNpWordFilterDeleteTitleCtx(int titleCtxId) { int APS5_VABI sceNpWordFilterPollAsync(int reqId, int* result) { if (!result) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - std::lock_guard lock(wordFilter.mutex); - const auto request = wordFilter.requests.find(reqId); - if (request == wordFilter.requests.end() || !request->second.async) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; - if (!request->second.finished) return POLL_RUNNING; - *result = request->second.result; - return POLL_FINISHED; + return PollAsync(wordFilter, reqId, result, true); } int APS5_VABI sceNpWordFilterSanitizeComment(int reqId, const char* comment, char* sanitizedComment, void* option) { @@ -517,4 +553,13 @@ int APS5_VABI sceNpWordFilterSanitizeComment(int reqId, const char* comment, cha return StartOffline(wordFilter, reqId); } +int APS5_VABI sceNpWordFilterSetTimeout(int id, int resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout) { + return SetTimeout(wordFilter, id, resolveRetry, resolveTimeout, connTimeout, sendTimeout, recvTimeout); +} + +int APS5_VABI sceNpWordFilterWaitAsync(int reqId, int* result) { + if (!result) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return WaitAsync(wordFilter, reqId, result, true); +} + } diff --git a/core/libs/prx/libSceNpUtility/include/Api.hpp b/core/libs/prx/libSceNpUtility/include/Api.hpp index 73d099655b..0fe054556f 100644 --- a/core/libs/prx/libSceNpUtility/include/Api.hpp +++ b/core/libs/prx/libSceNpUtility/include/Api.hpp @@ -23,6 +23,13 @@ int APS5_VABI sceNpWordFilterCreateTitleCtx(const void* selfNpId); int APS5_VABI sceNpWordFilterDeleteRequest(int reqId); int APS5_VABI sceNpWordFilterDeleteTitleCtx(int titleCtxId); int APS5_VABI sceNpWordFilterPollAsync(int reqId, int* result); +int APS5_VABI sceNpLookupAbortRequest(int reqId); +int APS5_VABI sceNpLookupCreateAsyncRequest(int titleCtxId, const std::uint64_t* param); +int APS5_VABI sceNpLookupPollAsync(int reqId, int* result); +int APS5_VABI sceNpLookupSetTimeout(int id, int resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout); +int APS5_VABI sceNpLookupWaitAsync(int reqId, int* result); +int APS5_VABI sceNpWordFilterSetTimeout(int id, int resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout); +int APS5_VABI sceNpWordFilterWaitAsync(int reqId, int* result); int APS5_VABI sceNpWordFilterSanitizeComment(int reqId, const char* comment, char* sanitizedComment, void* option); int APS5_VABI sceNpAppInfoIntAbortRequest(); @@ -53,8 +60,6 @@ int APS5_VABI sceNpAppLaunchLinkIntGetCompatibleTitleIdList(); int APS5_VABI sceNpAppLaunchLinkIntGetCompatibleTitleIdNum(); int APS5_VABI sceNpAppLaunchLinkIntInitialize(); int APS5_VABI sceNpBandwidthTestShutdownWithDetailedInfo(); -int APS5_VABI sceNpLookupAbortRequest(); -int APS5_VABI sceNpLookupCreateAsyncRequest(); int APS5_VABI sceNpLookupCreateTitleCtxA(); int APS5_VABI sceNpLookupNetAbortRequest(); int APS5_VABI sceNpLookupNetCensorComment(); @@ -72,9 +77,6 @@ int APS5_VABI sceNpLookupNetNpId(); int APS5_VABI sceNpLookupNetSanitizeComment(); int APS5_VABI sceNpLookupNetSetTimeout(); int APS5_VABI sceNpLookupNetTerm(); -int APS5_VABI sceNpLookupPollAsync(); -int APS5_VABI sceNpLookupSetTimeout(); -int APS5_VABI sceNpLookupWaitAsync(); int APS5_VABI sceNpServiceChecker2IntAbortRequest(); int APS5_VABI sceNpServiceChecker2IntCreateRequest(); int APS5_VABI sceNpServiceChecker2IntDestroyRequest(); @@ -99,7 +101,5 @@ int APS5_VABI sceNpTitleMetadataIntGetNpTitleId(); int APS5_VABI sceNpUtilityInit(); int APS5_VABI sceNpUtilityTerm(); int APS5_VABI sceNpWordFilterCreateTitleCtxA(); -int APS5_VABI sceNpWordFilterSetTimeout(); -int APS5_VABI sceNpWordFilterWaitAsync(); } diff --git a/core/libs/tests/GuestNpUtilityOffline.cpp b/core/libs/tests/GuestNpUtilityOffline.cpp index 42c76ec96c..290bbe7b94 100644 --- a/core/libs/tests/GuestNpUtilityOffline.cpp +++ b/core/libs/tests/GuestNpUtilityOffline.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace { @@ -118,11 +120,96 @@ void CheckWordFilter() { } +int WaitInThread(int (*wait)(int, int*), int reqId, int* result, void (*finish)(int)) { + int status = 7; + std::thread waiter([&] { status = wait(reqId, result); }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + finish(reqId); + waiter.join(); + return status; +} + +void StartLookup(int reqId) { + NpOnlineId onlineId{}; + std::strcpy(onlineId.data, "player_2"); + sceNpLookupNpId(reqId, &onlineId, npId, nullptr); +} + +void AbortLookup(int reqId) { + sceNpLookupAbortRequest(reqId); +} + +void DeleteLookup(int reqId) { + sceNpLookupDeleteRequest(reqId); +} + +void CheckLookupAsync() { + const int context = sceNpLookupCreateTitleCtx(selfNpId); + Require(sceNpLookupCreateAsyncRequest(context, nullptr) == InvalidArgument, "sceNpLookupCreateAsyncRequest", "a null parameter was taken"); + Require(sceNpLookupCreateAsyncRequest(context, wrongAsyncParam) == InvalidArgument, "sceNpLookupCreateAsyncRequest", "a parameter size other than 0x18 was taken"); + Require(sceNpLookupCreateAsyncRequest(context + 100, asyncParam) == InvalidId, "sceNpLookupCreateAsyncRequest", "an unknown context was taken"); + + Require(sceNpLookupSetTimeout(context, -1, 0, 0, 0, 0) == InvalidArgument, "sceNpLookupSetTimeout", "a negative retry count was taken"); + Require(sceNpLookupSetTimeout(context, 0, 0, 0, 0, 0) == InvalidArgument, "sceNpLookupSetTimeout", "settings without any effect were taken"); + Require(sceNpLookupSetTimeout(context, 0, 999999, 0, 0, 0) == InvalidArgument, "sceNpLookupSetTimeout", "a resolve timeout under 1 s was taken"); + Require(sceNpLookupSetTimeout(context, 0, 0, 9999999, 0, 0) == InvalidArgument, "sceNpLookupSetTimeout", "a connect timeout under 10 s was taken"); + Require(sceNpLookupSetTimeout(context, 0, 0, 0, 9999999, 0) == InvalidArgument, "sceNpLookupSetTimeout", "a send timeout under 10 s was taken"); + Require(sceNpLookupSetTimeout(context, 0, 0, 0, 0, 9999999) == InvalidArgument, "sceNpLookupSetTimeout", "a receive timeout under 10 s was taken"); + Require(sceNpLookupSetTimeout(context + 100, 1, 0, 0, 0, 0) == InvalidId, "sceNpLookupSetTimeout", "an unknown id was taken"); + Require(sceNpLookupSetTimeout(context, 1, 0, 0, 0, 0) == 0, "sceNpLookupSetTimeout", "a context timeout was refused"); + + const int async = sceNpLookupCreateAsyncRequest(context, asyncParam); + Require(async > 0, "sceNpLookupCreateAsyncRequest", "creating an asynchronous request failed"); + Require(sceNpLookupSetTimeout(async, 0, 1000000, 10000000, 10000000, 10000000) == 0, "sceNpLookupSetTimeout", "a request timeout was refused"); + int result = 7; + Require(sceNpLookupPollAsync(async + 100, &result) == InvalidId, "sceNpLookupPollAsync", "an unknown request was taken"); + Require(sceNpLookupPollAsync(async, nullptr) == PollRunning, "sceNpLookupPollAsync", "an unstarted request was not running"); + Require(WaitInThread(sceNpLookupWaitAsync, async, &result, StartLookup) == 0 && result == SignedOut, "sceNpLookupWaitAsync", "waiting did not end with SIGNED_OUT when the lookup started"); + Require(sceNpLookupPollAsync(async, nullptr) == PollFinished, "sceNpLookupPollAsync", "a null result was refused on a finished request"); + result = 7; + Require(sceNpLookupPollAsync(async, &result) == PollFinished && result == SignedOut, "sceNpLookupPollAsync", "the request did not finish with SIGNED_OUT"); + + const int sync = sceNpLookupCreateRequest(context); + StartLookup(sync); + result = 7; + Require(sceNpLookupWaitAsync(sync, &result) == 0 && result == SignedOut, "sceNpLookupWaitAsync", "a finished synchronous request was not returned"); + + const int aborted = sceNpLookupCreateAsyncRequest(context, asyncParam); + Require(sceNpLookupAbortRequest(aborted + 100) == InvalidId, "sceNpLookupAbortRequest", "an unknown request was taken"); + Require(WaitInThread(sceNpLookupWaitAsync, aborted, &result, AbortLookup) == 0 && result == Aborted, "sceNpLookupAbortRequest", "waiting did not end with ABORTED"); + Require(sceNpLookupNpId(aborted, nullptr, npId, nullptr) == InsufficientArgument, "sceNpLookupNpId", "arguments were not checked before the abort"); + + const int deleted = sceNpLookupCreateAsyncRequest(context, asyncParam); + Require(WaitInThread(sceNpLookupWaitAsync, deleted, &result, DeleteLookup) == InvalidId, "sceNpLookupWaitAsync", "waiting on a deleted request did not return INVALID_ID"); + Require(sceNpLookupWaitAsync(deleted, &result) == InvalidId, "sceNpLookupWaitAsync", "an unknown request was taken"); + sceNpLookupDeleteTitleCtx(context); +} + +void CheckWordFilterAsync() { + const int context = sceNpWordFilterCreateTitleCtx(selfNpId); + Require(sceNpWordFilterSetTimeout(context, 0, 0, 0, 0, 0) == InvalidArgument, "sceNpWordFilterSetTimeout", "settings without any effect were taken"); + Require(sceNpWordFilterSetTimeout(context, 0, 0, 0, 0, 9999999) == InvalidArgument, "sceNpWordFilterSetTimeout", "a receive timeout under 10 s was taken"); + Require(sceNpWordFilterSetTimeout(context, 1, 0, 0, 0, 0) == 0, "sceNpWordFilterSetTimeout", "a context timeout was refused"); + Require(sceNpWordFilterSetTimeout(1, 1, 0, 0, 0, 0) == InvalidId, "sceNpWordFilterSetTimeout", "a lookup id was taken"); + const int sync = sceNpWordFilterCreateRequest(context); + const int async = sceNpWordFilterCreateAsyncRequest(context, asyncParam); + Require(sceNpWordFilterSetTimeout(async, 2, 1000000, 0, 0, 0) == 0, "sceNpWordFilterSetTimeout", "a request timeout was refused"); + int result = 7; + Require(sceNpWordFilterWaitAsync(async, nullptr) == InvalidArgument, "sceNpWordFilterWaitAsync", "a null result was taken"); + Require(sceNpWordFilterWaitAsync(sync, &result) == InvalidId, "sceNpWordFilterWaitAsync", "a synchronous request was taken"); + Require(sceNpWordFilterWaitAsync(async + 100, &result) == InvalidId, "sceNpWordFilterWaitAsync", "an unknown request was taken"); + Require(sceNpWordFilterCensorComment(async, "word", nullptr) == 0, "sceNpWordFilterCensorComment", "the asynchronous request did not start"); + Require(sceNpWordFilterWaitAsync(async, &result) == 0 && result == SignedOut, "sceNpWordFilterWaitAsync", "the request did not finish with SIGNED_OUT"); + sceNpWordFilterDeleteTitleCtx(context); +} + int main() { std::memset(selfNpId, 0, sizeof(selfNpId)); std::strcpy(reinterpret_cast(selfNpId), "player_1"); CheckLookup(); CheckWordFilter(); + CheckLookupAsync(); + CheckWordFilterAsync(); if (failures != 0) return 1; std::puts("NpUtility offline tests passed"); return 0; diff --git a/docs/dev/debt/libSceNpUtility.md b/docs/dev/debt/libSceNpUtility.md index 642d6ab2bd..d83539b646 100644 --- a/docs/dev/debt/libSceNpUtility.md +++ b/docs/dev/debt/libSceNpUtility.md @@ -6,17 +6,17 @@ - [sceNpAppInfoIntCheckServiceAvailabilityA, sceNpAppInfoIntCheckServiceAvailabilityAll, sceNpAppInfoIntCheckServiceAvailabilityAllA, sceNpAppInfoIntCreateRequest, sceNpAppInfoIntDestroyRequest and sceNpAppInfoIntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpAppInfoIntInitialize, sceNpAppLaunchLink2IntAbortRequest, sceNpAppLaunchLink2IntCreateRequest, sceNpAppLaunchLink2IntDestroyRequest, sceNpAppLaunchLink2IntFinalize and sceNpAppLaunchLink2IntGetCompatibleTitleIdList](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpAppLaunchLink2IntGetCompatibleTitleIdNum, sceNpAppLaunchLink2IntInitialize, sceNpAppLaunchLinkIntAbortRequest, sceNpAppLaunchLinkIntCreateRequest, sceNpAppLaunchLinkIntDestroyRequest and sceNpAppLaunchLinkIntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpAppLaunchLinkIntGetCompatibleTitleIdList, sceNpAppLaunchLinkIntGetCompatibleTitleIdNum, sceNpAppLaunchLinkIntInitialize, sceNpBandwidthTestShutdownWithDetailedInfo and sceNpLookupAbortRequest](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpLookupCreateAsyncRequest, sceNpLookupCreateTitleCtxA, sceNpLookupNetAbortRequest and sceNpLookupNetCensorComment](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpAppLaunchLinkIntGetCompatibleTitleIdList, sceNpAppLaunchLinkIntGetCompatibleTitleIdNum, sceNpAppLaunchLinkIntInitialize and sceNpBandwidthTestShutdownWithDetailedInfo](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpLookupCreateTitleCtxA, sceNpLookupNetAbortRequest and sceNpLookupNetCensorComment](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpLookupNetConvertJidToNpId, sceNpLookupNetConvertNpIdToJid, sceNpLookupNetCreateRequest, sceNpLookupNetCreateTitleCtx, sceNpLookupNetDeleteRequest, sceNpLookupNetDeleteTitleCtx and sceNpLookupNetInit](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpLookupNetInitWithFunctionPointer, sceNpLookupNetInitWithMemoryPool, sceNpLookupNetIsInit, sceNpLookupNetNpId, sceNpLookupNetSanitizeComment, sceNpLookupNetSetTimeout, sceNpLookupNetTerm and sceNpLookupPollAsync](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpLookupSetTimeout, sceNpLookupWaitAsync, sceNpServiceChecker2IntAbortRequest, sceNpServiceChecker2IntCreateRequest, sceNpServiceChecker2IntDestroyRequest and sceNpServiceChecker2IntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpLookupNetInitWithFunctionPointer, sceNpLookupNetInitWithMemoryPool, sceNpLookupNetIsInit, sceNpLookupNetNpId, sceNpLookupNetSanitizeComment, sceNpLookupNetSetTimeout and sceNpLookupNetTerm](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. +- [sceNpServiceChecker2IntAbortRequest, sceNpServiceChecker2IntCreateRequest, sceNpServiceChecker2IntDestroyRequest and sceNpServiceChecker2IntFinalize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpServiceChecker2IntGetServiceAvailability, sceNpServiceChecker2IntGetServiceAvailabilityA, sceNpServiceChecker2IntInitialize, sceNpServiceChecker2IntIsSetServiceType and sceNpServiceCheckerIntAbortRequest](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpServiceCheckerIntCreateRequest, sceNpServiceCheckerIntDestroyRequest, sceNpServiceCheckerIntFinalize, sceNpServiceCheckerIntGetAvailability, sceNpServiceCheckerIntGetAvailabilityList and sceNpServiceCheckerIntInitialize](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpServiceCheckerIntIsCached, sceNpTitleMetadataIntAbortRequest, sceNpTitleMetadataIntCreateRequest, sceNpTitleMetadataIntDeleteRequest, sceNpTitleMetadataIntGetInfo, sceNpTitleMetadataIntGetNpTitleId and sceNpUtilityInit](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. - [sceNpUtilityTerm and sceNpWordFilterCreateTitleCtxA](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. -- [sceNpWordFilterSetTimeout and sceNpWordFilterWaitAsync](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - The list is the intersection of the export table of the console's libSceNpUtility (function symbols only), the pinned aerolib names and the functions not yet defined anywhere in the repo; names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpUtility.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. ### Functional - [NpLookup and NpWordFilter](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - offline only, not measured on a PS5: the argument checks and their order, the limit of 32 title contexts and 32 requests, the word filter id bases `0x01000001` and `0x02000001`, the 0x400-byte comment limit and single-use requests (`INVALID_TYPE`) follow shadPS4's [lookup](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_utility/np_utility.cpp#L41-L222) and [word filter](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_utility/np_utility.cpp#L705-L909) calls. `sceNpLookupNpId`, `sceNpWordFilterCensorComment` and `sceNpWordFilterSanitizeComment` then finish with `SCE_NP_ERROR_SIGNED_OUT` and write no NpId or comment; shadPS4's word filter accepts every comment as a stand-in for the server, so its result is not used +- [NpLookup and NpWordFilter waits and timeouts](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - offline, not measured on a PS5: the timeouts of `sceNpLookupSetTimeout` and `sceNpWordFilterSetTimeout` are checked and then have no effect, since no request reaches a server. A wait on a request deleted meanwhile returns `INVALID_ID`. The checks and results follow shadPS4's [lookup](https://github.com/shadps4-emu/shadPS4/blob/cc4dbd45bc10049ea8a445eb0e697de268ef4723/src/core/libraries/np/np_utility/np_utility.cpp#L120-L321) and [word filter](https://github.com/shadps4-emu/shadPS4/blob/cc4dbd45bc10049ea8a445eb0e697de268ef4723/src/core/libraries/np/np_utility/np_utility.cpp#L911-L951) calls: the lookup poll and wait take a null result and a synchronous request, the word filter ones refuse both From ed2d79d962a931e6ab0e2a74244196cd5699543b Mon Sep 17 00:00:00 2001 From: louise-patra Date: Sat, 24 Oct 2026 19:56:53 +0530 Subject: [PATCH 34/63] test(libSceNpUtility): fail a wait that returns before its request finishes The waiter sets a flag right before the wait; the test waits for it, then requires the wait not to have returned before it finishes, aborts or deletes the request. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestNpUtilityOffline.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestNpUtilityOffline.cpp b/core/libs/tests/GuestNpUtilityOffline.cpp index 290bbe7b94..21c4fa0481 100644 --- a/core/libs/tests/GuestNpUtilityOffline.cpp +++ b/core/libs/tests/GuestNpUtilityOffline.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -120,10 +121,18 @@ void CheckWordFilter() { } -int WaitInThread(int (*wait)(int, int*), int reqId, int* result, void (*finish)(int)) { +int WaitInThread(const char* name, int (*wait)(int, int*), int reqId, int* result, void (*finish)(int)) { int status = 7; - std::thread waiter([&] { status = wait(reqId, result); }); + std::atomic entered{false}; + std::atomic returned{false}; + std::thread waiter([&] { + entered = true; + status = wait(reqId, result); + returned = true; + }); + while (!entered) std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(!returned, name, "the wait returned before the request finished"); finish(reqId); waiter.join(); return status; @@ -164,7 +173,7 @@ void CheckLookupAsync() { int result = 7; Require(sceNpLookupPollAsync(async + 100, &result) == InvalidId, "sceNpLookupPollAsync", "an unknown request was taken"); Require(sceNpLookupPollAsync(async, nullptr) == PollRunning, "sceNpLookupPollAsync", "an unstarted request was not running"); - Require(WaitInThread(sceNpLookupWaitAsync, async, &result, StartLookup) == 0 && result == SignedOut, "sceNpLookupWaitAsync", "waiting did not end with SIGNED_OUT when the lookup started"); + Require(WaitInThread("sceNpLookupWaitAsync", sceNpLookupWaitAsync, async, &result, StartLookup) == 0 && result == SignedOut, "sceNpLookupWaitAsync", "waiting did not end with SIGNED_OUT when the lookup started"); Require(sceNpLookupPollAsync(async, nullptr) == PollFinished, "sceNpLookupPollAsync", "a null result was refused on a finished request"); result = 7; Require(sceNpLookupPollAsync(async, &result) == PollFinished && result == SignedOut, "sceNpLookupPollAsync", "the request did not finish with SIGNED_OUT"); @@ -176,11 +185,11 @@ void CheckLookupAsync() { const int aborted = sceNpLookupCreateAsyncRequest(context, asyncParam); Require(sceNpLookupAbortRequest(aborted + 100) == InvalidId, "sceNpLookupAbortRequest", "an unknown request was taken"); - Require(WaitInThread(sceNpLookupWaitAsync, aborted, &result, AbortLookup) == 0 && result == Aborted, "sceNpLookupAbortRequest", "waiting did not end with ABORTED"); + Require(WaitInThread("sceNpLookupWaitAsync", sceNpLookupWaitAsync, aborted, &result, AbortLookup) == 0 && result == Aborted, "sceNpLookupAbortRequest", "waiting did not end with ABORTED"); Require(sceNpLookupNpId(aborted, nullptr, npId, nullptr) == InsufficientArgument, "sceNpLookupNpId", "arguments were not checked before the abort"); const int deleted = sceNpLookupCreateAsyncRequest(context, asyncParam); - Require(WaitInThread(sceNpLookupWaitAsync, deleted, &result, DeleteLookup) == InvalidId, "sceNpLookupWaitAsync", "waiting on a deleted request did not return INVALID_ID"); + Require(WaitInThread("sceNpLookupWaitAsync", sceNpLookupWaitAsync, deleted, &result, DeleteLookup) == InvalidId, "sceNpLookupWaitAsync", "waiting on a deleted request did not return INVALID_ID"); Require(sceNpLookupWaitAsync(deleted, &result) == InvalidId, "sceNpLookupWaitAsync", "an unknown request was taken"); sceNpLookupDeleteTitleCtx(context); } From 200c0aef2b4b5408b4ae7b7aa66b6d1a09c1e7a3 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 25 Oct 2026 21:06:21 +1300 Subject: [PATCH 35/63] test(videoout): check the null resolution status buffer through the throwing helper --- core/libs/tests/GuestVideoOutResolutionStatus.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/tests/GuestVideoOutResolutionStatus.cpp b/core/libs/tests/GuestVideoOutResolutionStatus.cpp index fb4d14f108..58fcd7cbb5 100644 --- a/core/libs/tests/GuestVideoOutResolutionStatus.cpp +++ b/core/libs/tests/GuestVideoOutResolutionStatus.cpp @@ -68,8 +68,7 @@ int main() { for (const auto byte : vrr) Require(byte == std::byte{0xff}); Require(Rejects(handle, nullptr)); - Require(sceVideoOutGetResolutionStatus(handle, nullptr) == static_cast(0x80290002u)); - Require(sceVideoOutGetResolutionStatus(-1, nullptr) == static_cast(0x80290002u)); + Require(Rejects(-1, nullptr)); Require(Rejects(0, &status)); Require(Rejects(-1, &status)); Require(Rejects(NEVER_OPENED_HANDLE, &status)); From 855e4ea6a0a3b9dd67b374a6d5614f328b7958a3 Mon Sep 17 00:00:00 2001 From: louise-patra Date: Sun, 25 Oct 2026 13:57:19 +0530 Subject: [PATCH 36/63] test(libSceNpUtility): keep the export ABI in the wait callback Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestNpUtilityOffline.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/GuestNpUtilityOffline.cpp b/core/libs/tests/GuestNpUtilityOffline.cpp index 21c4fa0481..f4c627f347 100644 --- a/core/libs/tests/GuestNpUtilityOffline.cpp +++ b/core/libs/tests/GuestNpUtilityOffline.cpp @@ -121,7 +121,7 @@ void CheckWordFilter() { } -int WaitInThread(const char* name, int (*wait)(int, int*), int reqId, int* result, void (*finish)(int)) { +int WaitInThread(const char* name, int (APS5_VABI *wait)(int, int*), int reqId, int* result, void (*finish)(int)) { int status = 7; std::atomic entered{false}; std::atomic returned{false}; From d426ab75476ffd812c0aa704b3df94b4f4ab05a9 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sat, 24 Oct 2026 15:33:54 +0200 Subject: [PATCH 37/63] test(libkernel): keep the raise-in-system-call writer in guest code until each raise lands On a busy Windows runner the writer could stay inside NtWriteFile for over a second, because each 64 KiB block waited on 16 reader wake-ups. The raise then hit its 1 s retry limit and redirected the writer inside the call with the stream lock held, so the test deadlocked until ctest killed it at 30 s. Each raise now targets one 8 KiB write that blocks for about 100 ms on a single reader wake-up, and the writer spins in its own code until the raise is delivered. The watchdog uses a steady_clock deadline, so a hang aborts at 25 s instead of outliving the ctest timeout. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestRaiseInSystemCall.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/core/libs/tests/GuestRaiseInSystemCall.cpp b/core/libs/tests/GuestRaiseInSystemCall.cpp index f1b75ad913..262d7714f7 100644 --- a/core/libs/tests/GuestRaiseInSystemCall.cpp +++ b/core/libs/tests/GuestRaiseInSystemCall.cpp @@ -2,7 +2,6 @@ #include "SceTypes.hpp" #include #include -#include #include #include #include @@ -19,8 +18,7 @@ static void Require(bool value) { if (!value) std::abort(); } static std::FILE* shared = nullptr; static std::atomic deliveries{0}; static std::atomic released{0}; -static std::atomic stop{false}; -static std::atomic blocks{0}; +static std::atomic writing{0}; static void APS5_VABI Handler(int, void*) { const auto delivery = deliveries.fetch_add(1) + 1; @@ -28,12 +26,12 @@ static void APS5_VABI Handler(int, void*) { } static void* APS5_VABI Writer(void*) { - const std::string block(16u * PipeBytes, 'x'); - while (!stop.load()) { + const std::string block(2u * PipeBytes, 'x'); + for (int raise = 1; raise <= Raises; ++raise) { + writing.store(raise); Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); - blocks.fetch_add(1); - volatile std::uint64_t spin = 0; - for (std::uint64_t i = 0; i < 200000; ++i) spin = spin + i; + while (deliveries.load() < raise) { + } } return nullptr; } @@ -41,7 +39,8 @@ static void* APS5_VABI Writer(void*) { int main() { std::atomic done{false}; std::thread watchdog([&done] { - for (int i = 0; i < 25000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(25); + while (!done.load() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(10)); if (!done.load()) std::abort(); }); HANDLE read = nullptr; @@ -54,20 +53,19 @@ int main() { char chunk[PipeBytes]; DWORD got = 0; while (ReadFile(read, chunk, sizeof(chunk), &got, nullptr) != 0 && got != 0) { - if (draining.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + if (draining.load()) std::this_thread::sleep_for(std::chrono::milliseconds(100)); } }); Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); Pthread writer = nullptr; Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "pipe writer") == 0); - while (blocks.load() == 0) std::this_thread::yield(); for (int raise = 1; raise <= Raises; ++raise) { + while (writing.load() < raise) std::this_thread::yield(); Require(sceKernelRaiseException(writer, SigUsr1) == 0); while (deliveries.load() < raise) std::this_thread::yield(); Require(std::fputc('y', shared) == 'y'); released.store(raise); } - stop.store(true); draining.store(false); Require(scePthreadJoin(writer, nullptr) == 0); Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); From 493aac4e6cb56473586d22d00ef72c5a3132c8fc Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sat, 24 Oct 2026 16:29:53 +0200 Subject: [PATCH 38/63] test(libkernel): raise only once the writer is blocked inside its write The writer used to publish its block number just before fwrite, so a delayed raise could land before the call and pass without exercising the system-call path. The stream is now unbuffered, so each block is one 16 KiB WriteFile into the 4 KiB pipe. The reader counts the bytes, signals when the first byte of a block arrives (the writer is then inside WriteFile with more than a pipe's worth left), and stops reading until the main thread has started the raise, then 100 ms more. It checks the write has not returned, and the handler checks the delivery came after it did. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestRaiseInSystemCall.cpp | 27 +++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/core/libs/tests/GuestRaiseInSystemCall.cpp b/core/libs/tests/GuestRaiseInSystemCall.cpp index 262d7714f7..999f4b5755 100644 --- a/core/libs/tests/GuestRaiseInSystemCall.cpp +++ b/core/libs/tests/GuestRaiseInSystemCall.cpp @@ -2,6 +2,7 @@ #include "SceTypes.hpp" #include #include +#include #include #include #include @@ -13,23 +14,27 @@ static constexpr int SigUsr1 = 30; static constexpr int Raises = 10; static constexpr DWORD PipeBytes = 4096; +static constexpr std::uint64_t BlockBytes = 4u * PipeBytes; static void Require(bool value) { if (!value) std::abort(); } static std::FILE* shared = nullptr; static std::atomic deliveries{0}; static std::atomic released{0}; -static std::atomic writing{0}; +static std::atomic written{0}; +static std::atomic blocked{0}; +static std::atomic raising{0}; static void APS5_VABI Handler(int, void*) { const auto delivery = deliveries.fetch_add(1) + 1; + Require(written.load() == delivery); while (released.load() < delivery) std::this_thread::yield(); } static void* APS5_VABI Writer(void*) { - const std::string block(2u * PipeBytes, 'x'); + const std::string block(BlockBytes, 'x'); for (int raise = 1; raise <= Raises; ++raise) { - writing.store(raise); Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); + written.store(raise); while (deliveries.load() < raise) { } } @@ -47,26 +52,32 @@ int main() { HANDLE write = nullptr; Require(CreatePipe(&read, &write, nullptr, PipeBytes) != 0); shared = _fdopen(_open_osfhandle(reinterpret_cast(write), _O_WRONLY | _O_BINARY), "wb"); - Require(shared != nullptr); - std::atomic draining{true}; + Require(shared != nullptr && std::setvbuf(shared, nullptr, _IONBF, 0) == 0); std::thread reader([&] { char chunk[PipeBytes]; DWORD got = 0; + std::uint64_t total = 0; + int block = 0; while (ReadFile(read, chunk, sizeof(chunk), &got, nullptr) != 0 && got != 0) { - if (draining.load()) std::this_thread::sleep_for(std::chrono::milliseconds(100)); + total += got; + if (block == Raises || total <= static_cast(block) * (BlockBytes + 1)) continue; + blocked.store(++block); + while (raising.load() < block) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(written.load() < block); } }); Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); Pthread writer = nullptr; Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "pipe writer") == 0); for (int raise = 1; raise <= Raises; ++raise) { - while (writing.load() < raise) std::this_thread::yield(); + while (blocked.load() < raise) std::this_thread::yield(); + raising.store(raise); Require(sceKernelRaiseException(writer, SigUsr1) == 0); while (deliveries.load() < raise) std::this_thread::yield(); Require(std::fputc('y', shared) == 'y'); released.store(raise); } - draining.store(false); Require(scePthreadJoin(writer, nullptr) == 0); Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); std::fclose(shared); From 86709434faae43bb020effbdcd509ce5aec33ee5 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sun, 25 Oct 2026 10:10:01 +0100 Subject: [PATCH 39/63] test(libkernel): gate the raise-in-system-call hold on the raise sampling the writer The reader held the pipe for a fixed 100 ms after the main thread flagged the raise, so a main thread descheduled before sceKernelRaiseException let the write finish first and the test passed without the system-call path. The reader now waits until the writer is suspended (NtQueryInformationThread ThreadSuspendCount), which only the raise does while the write is blocked, then holds 100 ms. The handler checked that the write had returned through a flag the writer sets after fwrite, so a correct delivery between the two failed. It now checks that the interrupted rip lies in the test's own image. Two more registrations run the same test with a 200 ms pause before the raise and with a guest-code spin between fwrite and the flag. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 5 +++ core/libs/tests/GuestRaiseInSystemCall.cpp | 46 +++++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 9da2728d50..9bc366006b 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1973,6 +1973,11 @@ if(BUILD_TESTING) add_test(NAME guest_raise_in_system_call COMMAND guest_raise_in_system_call_tests) set_tests_properties(guest_raise_in_system_call PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + foreach(delay IN ITEMS raise return) + add_test(NAME guest_raise_in_system_call_delay_${delay} COMMAND guest_raise_in_system_call_tests --delay-${delay}) + set_tests_properties(guest_raise_in_system_call_delay_${delay} PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endforeach() add_test_executable(guest_raise_in_host_code_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseInHostCode.cpp) target_include_directories(guest_raise_in_host_code_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_raise_in_host_code_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestRaiseInSystemCall.cpp b/core/libs/tests/GuestRaiseInSystemCall.cpp index 999f4b5755..6f0d08c8be 100644 --- a/core/libs/tests/GuestRaiseInSystemCall.cpp +++ b/core/libs/tests/GuestRaiseInSystemCall.cpp @@ -5,9 +5,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -15,6 +17,8 @@ static constexpr int SigUsr1 = 30; static constexpr int Raises = 10; static constexpr DWORD PipeBytes = 4096; static constexpr std::uint64_t BlockBytes = 4u * PipeBytes; +static constexpr std::size_t RipOffset = 0xe0; +static constexpr ULONG ThreadSuspendCount = 35; static void Require(bool value) { if (!value) std::abort(); } static std::FILE* shared = nullptr; @@ -22,18 +26,43 @@ static std::atomic deliveries{0}; static std::atomic released{0}; static std::atomic written{0}; static std::atomic blocked{0}; -static std::atomic raising{0}; +static std::atomic writerThread{nullptr}; +static bool delayReturn = false; -static void APS5_VABI Handler(int, void*) { +static bool InTestImage(std::uint64_t rip) { + const auto base = reinterpret_cast(GetModuleHandleW(nullptr)); + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + return rip >= base && rip - base < nt->OptionalHeader.SizeOfImage; +} + +static bool Suspended(HANDLE thread) { + using Query = LONG(NTAPI*)(HANDLE, ULONG, void*, ULONG, ULONG*); + static const auto query = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationThread")); + ULONG count = 0; + Require(query != nullptr && query(thread, ThreadSuspendCount, &count, sizeof(count), nullptr) >= 0); + return count > 0; +} + +static void APS5_VABI Handler(int, void* context) { const auto delivery = deliveries.fetch_add(1) + 1; - Require(written.load() == delivery); + std::uint64_t rip = 0; + std::memcpy(&rip, static_cast(context) + RipOffset, sizeof(rip)); + Require(InTestImage(rip)); while (released.load() < delivery) std::this_thread::yield(); } static void* APS5_VABI Writer(void*) { + HANDLE self = nullptr; + Require(DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &self, THREAD_QUERY_INFORMATION, FALSE, 0) != 0); + writerThread.store(self); const std::string block(BlockBytes, 'x'); for (int raise = 1; raise <= Raises; ++raise) { Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); + if (delayReturn) { + volatile std::uint64_t spin = 0; + for (std::uint64_t i = 0; i < 100000000; ++i) spin = spin + i; + } written.store(raise); while (deliveries.load() < raise) { } @@ -41,7 +70,10 @@ static void* APS5_VABI Writer(void*) { return nullptr; } -int main() { +int main(int argc, char** argv) { + const std::string_view mode = argc > 1 ? argv[1] : ""; + Require(mode.empty() || mode == "--delay-raise" || mode == "--delay-return"); + delayReturn = mode == "--delay-return"; std::atomic done{false}; std::thread watchdog([&done] { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(25); @@ -62,7 +94,8 @@ int main() { total += got; if (block == Raises || total <= static_cast(block) * (BlockBytes + 1)) continue; blocked.store(++block); - while (raising.load() < block) std::this_thread::yield(); + while (!Suspended(writerThread.load())) { + } std::this_thread::sleep_for(std::chrono::milliseconds(100)); Require(written.load() < block); } @@ -72,7 +105,7 @@ int main() { Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "pipe writer") == 0); for (int raise = 1; raise <= Raises; ++raise) { while (blocked.load() < raise) std::this_thread::yield(); - raising.store(raise); + if (mode == "--delay-raise") std::this_thread::sleep_for(std::chrono::milliseconds(200)); Require(sceKernelRaiseException(writer, SigUsr1) == 0); while (deliveries.load() < raise) std::this_thread::yield(); Require(std::fputc('y', shared) == 'y'); @@ -83,6 +116,7 @@ int main() { std::fclose(shared); reader.join(); CloseHandle(read); + CloseHandle(writerThread.load()); done.store(true); watchdog.join(); } From 66473085c2e7a82a76de6a5938abaae471f18ec0 Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Sun, 25 Oct 2026 11:21:16 +0100 Subject: [PATCH 40/63] fix(relinker): accept a Linux entry in a PT_LOAD without flags The Linux output maps a PT_LOAD whose p_flags is 0 as RWX, so the e_entry check now accepts an entry inside such a segment instead of rejecting it. A new test case relinks that input in both Linux modes and checks that the output maps the entry's segment RWX. The R_X86_64_NONE skip in the relocation offset loop is removed, together with its none-outside case, because extractRela drops NONE before the relocation tables are rebuilt. --- .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 5 ++-- .../relinker/tests/test_linux_load_bounds.py | 29 ++++++++++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 2817ea4271..32a7cf27ba 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -106,7 +106,7 @@ void RequireFileRange(const std::vector& buf, const Domain::Progra void RequireLoaded(const std::vector& headers, const std::uint64_t address, const std::uint64_t size, const std::uint32_t flags, const std::string& what) { for (const auto& header : headers) - if (header.Type == PT_LOAD && (header.Flags & flags) == flags && address >= header.MappedAddress && address - header.MappedAddress < header.MemorySize && size <= header.MemorySize - (address - header.MappedAddress)) + if (header.Type == PT_LOAD && (header.Flags == 0 || (header.Flags & flags) == flags) && address >= header.MappedAddress && address - header.MappedAddress < header.MemorySize && size <= header.MemorySize - (address - header.MappedAddress)) return; const auto range = size > 1 ? " (" + std::to_string(size) + " bytes) does not fit inside " : std::string(" is not inside "); throw Domain::RelinkerException(what + " " + Hex(address) + range + ((flags & PF_X) != 0 ? "an executable" : "a") + " PT_LOAD segment"); @@ -375,8 +375,7 @@ std::vector LinuxElfPatcher::Patch( RequireLoaded(originalHeaders, Io::ReadU64(sourceElf, kEhdrEntryOffset), 1, PF_X, "e_entry"); for (const auto& [relocationTable, name] : {std::pair{&dynSection.RelaData, "RELA r_offset"}, std::pair{&dynSection.RelaPltData, "JMPREL r_offset"}}) for (std::size_t entry = 0; entry < relocationTable->size(); entry += kRelaEntrySize) - if (static_cast(Io::ReadU64(*relocationTable, entry + 8)) != R_X86_64_NONE) - RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry), 0, name); + RequireLoaded(originalHeaders, Io::ReadU64(*relocationTable, entry), RelocationWidth(*relocationTable, entry), 0, name); if (!dynSection.RelaPltData.empty()) RequireLoaded(originalHeaders, originalPltGotVaddr, lazyBinding ? kLazyPltGotBytes : kPltGotEntryBytes, 0, "DT_PLTGOT"); const auto traced = _traceImports ? TracedImports(dynSection) : std::vector{}; diff --git a/core/relinker/relinker/tests/test_linux_load_bounds.py b/core/relinker/relinker/tests/test_linux_load_bounds.py index 49e9a994c4..687e95ae2b 100644 --- a/core/relinker/relinker/tests/test_linux_load_bounds.py +++ b/core/relinker/relinker/tests/test_linux_load_bounds.py @@ -11,14 +11,18 @@ from test_jmprel_table_bounds import fixture PT_LOAD = 1 PT_SCE_RELRO = 0x61000010 +PF_X = 1 PF_W = 2 PF_R = 4 R_X86_64_PC32 = 2 R_X86_64_32 = 10 E_ENTRY = 24 +FIRST_LOAD_FLAGS = 64 + 4 SPARE_HEADER = 64 + 3 * 56 PLTGOT_VALUE = 0x400 + 9 * 16 + 8 OUTSIDE = 0x2000 +ENTRY = 0x200 +FIXED_BASE = 0x400000 def with_bss(image): @@ -41,6 +45,18 @@ def with_pltgot(image, value): return image +def without_load_flags(image): + struct.pack_into(" Date: Sat, 24 Oct 2026 10:32:57 +0300 Subject: [PATCH 41/63] feat(libkernel_sys): report a retail console from sceKernelIsCEX and sceKernelIsDevKit --- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 10 ---------- core/libs/prx/libkernel_sys/Export.cpp | 8 ++++++++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 3addcda651..bf1df5d5f0 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -1417,21 +1417,11 @@ int APS5_VABI sceKernelIsAuthenticNeo(void) { return 0; } -int APS5_VABI sceKernelIsCEX(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelIsDebuggerAttached(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceKernelIsDevKit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelIsExperimentalBeta(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 4125e11eea..a0a7f0f467 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -337,6 +337,14 @@ std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask) { return File::ExchangeCreationMask_nid_no_patch(mask); } +int APS5_VABI sceKernelIsCEX(void) { + return 1; +} + +int APS5_VABI sceKernelIsDevKit(void) { + return 0; +} + int APS5_VABI fstatfs_nid_postfix(int d, KernelStatfs* buf) { return _fstatfs_nid_postfix(d, buf); } From cd94200d5fd9821989e80d2d1640c3d041a7ce19 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sat, 24 Oct 2026 10:32:57 +0300 Subject: [PATCH 42/63] feat(libkernel_sys): describe the guest process through get_authinfo --- .../libs/prx/libkernel_sys/Export.Discovery.cpp | 5 ----- core/libs/prx/libkernel_sys/Export.cpp | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index bf1df5d5f0..76361d486d 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -67,11 +67,6 @@ int APS5_VABI futimesat_nid_postfix(void) { return 0; } -int APS5_VABI get_authinfo_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI get_module_info_list_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index a0a7f0f467..56edb6de2d 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -62,6 +62,7 @@ static int GuestFstat(int fd, bool* withoutVnode = nullptr) { constexpr int GuestEnoent = 2; constexpr int GuestEperm = 1; +constexpr int GuestEsrch = 3; constexpr int GuestEio = 5; constexpr int GuestEbadf = 9; constexpr int GuestEfault = 14; @@ -126,6 +127,14 @@ int GuestFileError(int error) { } } +struct KernelAuthInfo { + std::uint64_t programAuthorityId; + std::uint64_t capabilities[4]; + std::uint64_t attributes[4]; + std::uint64_t credentials[8]; +}; +static_assert(sizeof(KernelAuthInfo) == 0x88); + std::int64_t Fail(int error) { *__error_nid_postfix() = error; return -1; @@ -345,6 +354,14 @@ int APS5_VABI sceKernelIsDevKit(void) { return 0; } +int APS5_VABI get_authinfo_nid_postfix(int pid, KernelAuthInfo* info) { + if (info == nullptr) return static_cast(Fail(GuestEperm)); + if (pid != 0 && pid != getpid_nid_postfix()) return static_cast(Fail(GuestEsrch)); + *info = {}; + info->capabilities[0] = 0x2000000000000000; + return 0; +} + int APS5_VABI fstatfs_nid_postfix(int d, KernelStatfs* buf) { return _fstatfs_nid_postfix(d, buf); } From afa6f6dcbc99768dc74a9be885ea75fc6e4b1aa6 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sat, 24 Oct 2026 10:32:57 +0300 Subject: [PATCH 43/63] feat(libkernel_sys): return the OpenPSID from sceKernelGetOpenPsIdForSystem --- core/libs/prx/libkernel_sys/Export.Discovery.cpp | 5 ----- core/libs/prx/libkernel_sys/Export.cpp | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 76361d486d..c7f6bc1e49 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -982,11 +982,6 @@ int APS5_VABI sceKernelGetMemoryPstate(void) { return 0; } -int APS5_VABI sceKernelGetOpenPsIdForSystem(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelGetPagingStatsOfAllObjects(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 56edb6de2d..c6504ce0ec 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -346,6 +346,12 @@ std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask) { return File::ExchangeCreationMask_nid_no_patch(mask); } +int APS5_VABI sceKernelGetOpenPsId(void* id); + +int APS5_VABI sceKernelGetOpenPsIdForSystem(void* id) { + return sceKernelGetOpenPsId(id); +} + int APS5_VABI sceKernelIsCEX(void) { return 1; } From af7192f686ef69ca6cd3f013a5f6f0762ad040a5 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sat, 24 Oct 2026 10:33:45 +0300 Subject: [PATCH 44/63] test(libkernel_sys): check the console kind, auth info and system OpenPSID --- core/libs/CMakeLists.txt | 9 +++ .../tests/GuestKernelSysConsoleIdentity.cpp | 70 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 core/libs/tests/GuestKernelSysConsoleIdentity.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index eea852c88a..81720715b7 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -269,6 +269,15 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_sys_sdk_version PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kernel_sys_console_identity_tests tests/GuestKernelSysConsoleIdentity.cpp) + target_include_directories(guest_kernel_sys_console_identity_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_console_identity_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_console_identity_tests) + add_test(NAME guest_kernel_sys_console_identity COMMAND guest_kernel_sys_console_identity_tests) + if(WIN32) + set_tests_properties(guest_kernel_sys_console_identity PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp) target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include) target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20) diff --git a/core/libs/tests/GuestKernelSysConsoleIdentity.cpp b/core/libs/tests/GuestKernelSysConsoleIdentity.cpp new file mode 100644 index 0000000000..14b664e133 --- /dev/null +++ b/core/libs/tests/GuestKernelSysConsoleIdentity.cpp @@ -0,0 +1,70 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelIsCEX(void); +int APS5_VABI sceKernelIsDevKit(void); +int APS5_VABI get_authinfo_nid_postfix(int pid, void* info); +int APS5_VABI sceKernelGetOpenPsId(void* id); +int APS5_VABI sceKernelGetOpenPsIdForSystem(void* id); +int APS5_VABI getpid_nid_postfix(void); +int* APS5_VABI __error_nid_postfix(void); +} + +namespace { + +constexpr int GuestEperm = 1; +constexpr int GuestEsrch = 3; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "KERNEL SYS CONSOLE IDENTITY: %s\n", message); + std::abort(); + } +} + +void CheckConsoleKind() { + Require(sceKernelIsCEX() == 1, "the console must be reported as a retail unit"); + Require(sceKernelIsDevKit() == 0, "the console must not be reported as a development kit"); +} + +void CheckAuthInfo() { + std::array info; + *__error_nid_postfix() = 0; + Require(get_authinfo_nid_postfix(0, nullptr) == -1 && *__error_nid_postfix() == GuestEperm, "a null result must fail with EPERM"); + info.fill(0x5a5a5a5a5a5a5a5a); + *__error_nid_postfix() = 0; + Require(get_authinfo_nid_postfix(getpid_nid_postfix() + 1, info.data()) == -1 && *__error_nid_postfix() == GuestEsrch, + "another process must fail with ESRCH"); + Require(info[0] == 0x5a5a5a5a5a5a5a5a, "a refused call must not write the result"); + for (const int pid : {0, getpid_nid_postfix()}) { + info.fill(0x5a5a5a5a5a5a5a5a); + Require(get_authinfo_nid_postfix(pid, info.data()) == 0, "the guest process must be described"); + Require(info[0] == 0 && info[1] == 0x2000000000000000, "the authority ID or first capability word is wrong"); + for (std::size_t i = 2; i < 17; ++i) Require(info[i] == 0, "the rest of the result must be cleared"); + Require(info[17] == 0x5a5a5a5a5a5a5a5a, "get_authinfo wrote past its 0x88-byte result"); + } +} + +void CheckOpenPsId() { + std::array plain{}; + std::array system; + system.fill(0xa5); + Require(sceKernelGetOpenPsId(plain.data()) == 0, "the OpenPSID must be readable"); + Require(sceKernelGetOpenPsIdForSystem(system.data()) == 0, "the system OpenPSID must be readable"); + Require(std::memcmp(plain.data(), system.data(), plain.size()) == 0, "both OpenPSID queries must agree"); + for (std::size_t i = plain.size(); i < system.size(); ++i) Require(system[i] == 0xa5, "the system OpenPSID wrote past 16 bytes"); +} + +} + +int main() { + CheckConsoleKind(); + CheckAuthInfo(); + CheckOpenPsId(); + return 0; +} From 0d851dd1f3ee28f8e65aa802c5591a86c002edb8 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sat, 24 Oct 2026 10:33:58 +0300 Subject: [PATCH 45/63] docs(debt): note the retail console identity in libkernel_sys --- docs/dev/debt/libkernel_sys.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index fd4084515b..e992912552 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -27,6 +27,7 @@ - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [sceKernelGetModuleInfoInternal and sceKernelGetModuleListInternal](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) answer for every loaded module, system modules included. `sceKernelGetModuleInfoInternal` fills the 0x1A8-byte `SceKernelModuleInfoEx` of a module handle the way `sceKernelGetModuleInfoFromAddr` does for an address, with [shadPS4's checks](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/process.cpp#L243-L258) in their order: `EFAULT` for a null pointer, `EINVAL` when `st_size` is not 0x1A8, `ESRCH` for an unknown handle. `sceKernelGetModuleListInternal` returns the same list as `sceKernelGetModuleList`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_libkernel.pas#L852-L858) does; fpPS4 marks that as a guess, and neither is checked on a PS5 - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) +- [sceKernelIsCEX, sceKernelIsDevKit, get_authinfo and sceKernelGetOpenPsIdForSystem](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - the guest always runs on a retail unit, as in [shadPS4's process.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/process.cpp) with its devkit option off. `get_authinfo` takes shadPS4's [kernel.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/kernel.cpp) errors and capability word, so the authority ID and the other capability, attribute and credential words stay 0. `sceKernelGetOpenPsIdForSystem` returns `sceKernelGetOpenPsId`'s value, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas) does; whether the console keeps a separate ID for system callers is not known. Not measured on a console - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it - [chown, lchown and fchownat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled, as for `fchown`: owner and group 0 and the unchanged value (-1) are accepted, and any other owner or group throws, naming it. The path is looked up first, as in FreeBSD 11's `kern_fchownat`, with `stat`, with `lstat` for `lchown`, and with `fstatat` for `fchownat`, so a lookup error is returned before the owner is checked and nothing is changed on the host. `fchownat` flags other than `AT_SYMLINK_NOFOLLOW` (0x200) return `EINVAL`, as in FreeBSD 11. The console's ownership rules are unverified - [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) resolves `AT_FDCWD` (-100) and absolute paths as `link` does, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. A symbolic link as the source without `AT_SYMLINK_FOLLOW` throws, where FreeBSD links the symbolic link itself. Console behaviour is unverified From 803601ffa4d665dd4688340101e50e141c463434 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sun, 25 Oct 2026 13:07:08 +0200 Subject: [PATCH 46/63] fix(libkernel_sys): publish identity types and reject null system IDs --- core/libs/prx/libkernel_sys/Export.cpp | 11 +---- core/libs/prx/libkernel_sys/include/Api.hpp | 12 +++++- .../tests/GuestKernelSysConsoleIdentity.cpp | 41 ++++++++++--------- docs/dev/debt/libkernel_sys.md | 2 +- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index c6504ce0ec..444c3f19ba 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -127,14 +127,6 @@ int GuestFileError(int error) { } } -struct KernelAuthInfo { - std::uint64_t programAuthorityId; - std::uint64_t capabilities[4]; - std::uint64_t attributes[4]; - std::uint64_t credentials[8]; -}; -static_assert(sizeof(KernelAuthInfo) == 0x88); - std::int64_t Fail(int error) { *__error_nid_postfix() = error; return -1; @@ -346,9 +338,8 @@ std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask) { return File::ExchangeCreationMask_nid_no_patch(mask); } -int APS5_VABI sceKernelGetOpenPsId(void* id); - int APS5_VABI sceKernelGetOpenPsIdForSystem(void* id) { + if (id == nullptr) return GuestEinval; return sceKernelGetOpenPsId(id); } diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index 938bc8178b..dc67a83fbe 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -24,6 +24,14 @@ struct KernelTitleWorkaround { std::uint64_t ids[2]; }; +struct KernelAuthInfo { + std::uint64_t programAuthorityId; + std::uint64_t capabilities[4]; + std::uint64_t attributes[4]; + std::uint64_t credentials[8]; +}; +static_assert(sizeof(KernelAuthInfo) == 0x88); + extern "C" { int APS5_VABI aio_read_nid_postfix(GuestAiocb* aiocb); @@ -80,7 +88,7 @@ int APS5_VABI cpuset_getid_nid_postfix(void); int APS5_VABI cpuset_setid_nid_postfix(void); int APS5_VABI dynlib_get_obj_member_nid_postfix(void); int APS5_VABI futimesat_nid_postfix(void); -int APS5_VABI get_authinfo_nid_postfix(void); +int APS5_VABI get_authinfo_nid_postfix(int pid, KernelAuthInfo* info); int APS5_VABI get_module_info_list_nid_postfix(void); int APS5_VABI get_page_table_stats_nid_postfix(void); int APS5_VABI get_sdk_compiled_version_nid_postfix(void); @@ -265,7 +273,7 @@ int APS5_VABI sceKernelGetIpcPath(void); int APS5_VABI sceKernelGetKnobs(void); int APS5_VABI sceKernelGetKqueueFromEqueue(void); int APS5_VABI sceKernelGetMemoryPstate(void); -int APS5_VABI sceKernelGetOpenPsIdForSystem(void); +int APS5_VABI sceKernelGetOpenPsIdForSystem(void* id); int APS5_VABI sceKernelGetPagingStatsOfAllObjects(void); int APS5_VABI sceKernelGetPagingStatsOfAllThreads(void); int APS5_VABI sceKernelGetParentApp(void); diff --git a/core/libs/tests/GuestKernelSysConsoleIdentity.cpp b/core/libs/tests/GuestKernelSysConsoleIdentity.cpp index 14b664e133..f01ac5f625 100644 --- a/core/libs/tests/GuestKernelSysConsoleIdentity.cpp +++ b/core/libs/tests/GuestKernelSysConsoleIdentity.cpp @@ -1,20 +1,12 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel_sys/libkernel_sys.hpp" +#include "prx/libc/libc.hpp" #include +#include #include #include #include #include -extern "C" { -int APS5_VABI sceKernelIsCEX(void); -int APS5_VABI sceKernelIsDevKit(void); -int APS5_VABI get_authinfo_nid_postfix(int pid, void* info); -int APS5_VABI sceKernelGetOpenPsId(void* id); -int APS5_VABI sceKernelGetOpenPsIdForSystem(void* id); -int APS5_VABI getpid_nid_postfix(void); -int* APS5_VABI __error_nid_postfix(void); -} - namespace { constexpr int GuestEperm = 1; @@ -33,20 +25,25 @@ void CheckConsoleKind() { } void CheckAuthInfo() { - std::array info; + struct { + KernelAuthInfo value; + std::uint64_t guard; + } info; *__error_nid_postfix() = 0; Require(get_authinfo_nid_postfix(0, nullptr) == -1 && *__error_nid_postfix() == GuestEperm, "a null result must fail with EPERM"); - info.fill(0x5a5a5a5a5a5a5a5a); + std::memset(&info, 0x5a, sizeof(info)); *__error_nid_postfix() = 0; - Require(get_authinfo_nid_postfix(getpid_nid_postfix() + 1, info.data()) == -1 && *__error_nid_postfix() == GuestEsrch, + Require(get_authinfo_nid_postfix(getpid_nid_postfix() + 1, &info.value) == -1 && *__error_nid_postfix() == GuestEsrch, "another process must fail with ESRCH"); - Require(info[0] == 0x5a5a5a5a5a5a5a5a, "a refused call must not write the result"); + for (const auto byte : std::bit_cast>(info)) + Require(byte == 0x5a, "a refused call must not write the result"); for (const int pid : {0, getpid_nid_postfix()}) { - info.fill(0x5a5a5a5a5a5a5a5a); - Require(get_authinfo_nid_postfix(pid, info.data()) == 0, "the guest process must be described"); - Require(info[0] == 0 && info[1] == 0x2000000000000000, "the authority ID or first capability word is wrong"); - for (std::size_t i = 2; i < 17; ++i) Require(info[i] == 0, "the rest of the result must be cleared"); - Require(info[17] == 0x5a5a5a5a5a5a5a5a, "get_authinfo wrote past its 0x88-byte result"); + std::memset(&info, 0x5a, sizeof(info)); + Require(get_authinfo_nid_postfix(pid, &info.value) == 0, "the guest process must be described"); + Require(info.value.programAuthorityId == 0 && info.value.capabilities[0] == 0x2000000000000000, "the authority ID or first capability word is wrong"); + const auto bytes = std::bit_cast>(info.value); + for (std::size_t i = 16; i < bytes.size(); ++i) Require(bytes[i] == 0, "the rest of the result must be cleared"); + Require(info.guard == 0x5a5a5a5a5a5a5a5a, "get_authinfo wrote past its 0x88-byte result"); } } @@ -54,6 +51,10 @@ void CheckOpenPsId() { std::array plain{}; std::array system; system.fill(0xa5); + *__error_nid_postfix() = GuestEsrch; + Require(sceKernelGetOpenPsIdForSystem(nullptr) == 22, "a null system OpenPSID must return positive EINVAL"); + Require(*__error_nid_postfix() == GuestEsrch, "a null system OpenPSID must leave errno unchanged"); + for (auto byte : system) Require(byte == 0xa5, "a null system OpenPSID must not write output"); Require(sceKernelGetOpenPsId(plain.data()) == 0, "the OpenPSID must be readable"); Require(sceKernelGetOpenPsIdForSystem(system.data()) == 0, "the system OpenPSID must be readable"); Require(std::memcmp(plain.data(), system.data(), plain.size()) == 0, "both OpenPSID queries must agree"); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index e992912552..0286dda596 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -27,7 +27,7 @@ - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [sceKernelGetModuleInfoInternal and sceKernelGetModuleListInternal](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) answer for every loaded module, system modules included. `sceKernelGetModuleInfoInternal` fills the 0x1A8-byte `SceKernelModuleInfoEx` of a module handle the way `sceKernelGetModuleInfoFromAddr` does for an address, with [shadPS4's checks](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/process.cpp#L243-L258) in their order: `EFAULT` for a null pointer, `EINVAL` when `st_size` is not 0x1A8, `ESRCH` for an unknown handle. `sceKernelGetModuleListInternal` returns the same list as `sceKernelGetModuleList`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_libkernel.pas#L852-L858) does; fpPS4 marks that as a guess, and neither is checked on a PS5 - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) -- [sceKernelIsCEX, sceKernelIsDevKit, get_authinfo and sceKernelGetOpenPsIdForSystem](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - the guest always runs on a retail unit, as in [shadPS4's process.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/process.cpp) with its devkit option off. `get_authinfo` takes shadPS4's [kernel.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/kernel.cpp) errors and capability word, so the authority ID and the other capability, attribute and credential words stay 0. `sceKernelGetOpenPsIdForSystem` returns `sceKernelGetOpenPsId`'s value, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas) does; whether the console keeps a separate ID for system callers is not known. Not measured on a console +- [sceKernelIsCEX, sceKernelIsDevKit, get_authinfo and sceKernelGetOpenPsIdForSystem](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - the guest always runs on a retail unit, as in [shadPS4's process.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/process.cpp) with its devkit option off. `get_authinfo` takes shadPS4's [kernel.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/kernel.cpp) errors and capability word, so the authority ID and the other capability, attribute and credential words stay 0. `sceKernelGetOpenPsIdForSystem` returns `sceKernelGetOpenPsId`'s value, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas) does; firmware 13.42 reads a separate `machdep.openpsid_for_sys`, but whether its value differs is unknown. Null returns positive 22 per the [module report](https://github.com/boykopovar/AnyPS5/pull/10524#issuecomment-6393566914). The kernel-side `get_authinfo` behavior remains unverified - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it - [chown, lchown and fchownat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled, as for `fchown`: owner and group 0 and the unchanged value (-1) are accepted, and any other owner or group throws, naming it. The path is looked up first, as in FreeBSD 11's `kern_fchownat`, with `stat`, with `lstat` for `lchown`, and with `fstatat` for `fchownat`, so a lookup error is returned before the owner is checked and nothing is changed on the host. `fchownat` flags other than `AT_SYMLINK_NOFOLLOW` (0x200) return `EINVAL`, as in FreeBSD 11. The console's ownership rules are unverified - [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) resolves `AT_FDCWD` (-100) and absolute paths as `link` does, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. A symbolic link as the source without `AT_SYMLINK_FOLLOW` throws, where FreeBSD links the symbolic link itself. Console behaviour is unverified From 866ab27a801c944bbbf469953f6b69d040626166 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sun, 25 Oct 2026 13:11:03 +0200 Subject: [PATCH 47/63] docs(debt): clarify unverified console identity behavior --- docs/dev/debt/libkernel_sys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 0286dda596..ad1f62a1fa 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -27,7 +27,7 @@ - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [sceKernelGetModuleInfoInternal and sceKernelGetModuleListInternal](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) answer for every loaded module, system modules included. `sceKernelGetModuleInfoInternal` fills the 0x1A8-byte `SceKernelModuleInfoEx` of a module handle the way `sceKernelGetModuleInfoFromAddr` does for an address, with [shadPS4's checks](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/process.cpp#L243-L258) in their order: `EFAULT` for a null pointer, `EINVAL` when `st_size` is not 0x1A8, `ESRCH` for an unknown handle. `sceKernelGetModuleListInternal` returns the same list as `sceKernelGetModuleList`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_libkernel.pas#L852-L858) does; fpPS4 marks that as a guess, and neither is checked on a PS5 - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) -- [sceKernelIsCEX, sceKernelIsDevKit, get_authinfo and sceKernelGetOpenPsIdForSystem](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - the guest always runs on a retail unit, as in [shadPS4's process.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/process.cpp) with its devkit option off. `get_authinfo` takes shadPS4's [kernel.cpp](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/kernel.cpp) errors and capability word, so the authority ID and the other capability, attribute and credential words stay 0. `sceKernelGetOpenPsIdForSystem` returns `sceKernelGetOpenPsId`'s value, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas) does; firmware 13.42 reads a separate `machdep.openpsid_for_sys`, but whether its value differs is unknown. Null returns positive 22 per the [module report](https://github.com/boykopovar/AnyPS5/pull/10524#issuecomment-6393566914). The kernel-side `get_authinfo` behavior remains unverified +- [Console identity](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - `get_authinfo` errors and capability bits remain unverified on PS5; they follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/396c2a09a38d22eaa02aab32c1967263dceab95f/src/core/libraries/kernel/kernel.cpp), leaving the authority ID and other capability, attribute and credential words zero. The host models a retail unit (`IsCEX` 1, `IsDevKit` 0). The system OpenPSID reuses the ordinary ID; firmware 13.42 reads a separate `machdep.openpsid_for_sys`, whose value is unknown. Null returns positive 22 per the [module report](https://github.com/boykopovar/AnyPS5/pull/10524#issuecomment-6393566914). - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it - [chown, lchown and fchownat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled, as for `fchown`: owner and group 0 and the unchanged value (-1) are accepted, and any other owner or group throws, naming it. The path is looked up first, as in FreeBSD 11's `kern_fchownat`, with `stat`, with `lstat` for `lchown`, and with `fstatat` for `fchownat`, so a lookup error is returned before the owner is checked and nothing is changed on the host. `fchownat` flags other than `AT_SYMLINK_NOFOLLOW` (0x200) return `EINVAL`, as in FreeBSD 11. The console's ownership rules are unverified - [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) resolves `AT_FDCWD` (-100) and absolute paths as `link` does, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. A symbolic link as the source without `AT_SYMLINK_FOLLOW` throws, where FreeBSD links the symbolic link itself. Console behaviour is unverified From 10f2994bb65e5f55d3d4c7b1271b4bd6bc2611fc Mon Sep 17 00:00:00 2001 From: louise-patra Date: Sun, 25 Oct 2026 18:50:17 +0530 Subject: [PATCH 48/63] fix(libSceNpUtility): refuse null results and synchronous requests in polls and waits Firmware 13.42's module checks a null result first (0x80550704) and refuses a synchronous request in the lookup poll and wait with 0x80550307; the word filter keeps INVALID_ID. The debt entry records the measurement and that IDs here do not carry the console's service bits. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNpUtility/Export.cpp | 27 +++++++++++++---------- core/libs/tests/GuestNpUtilityOffline.cpp | 11 ++++++--- docs/dev/debt/libSceNpUtility.md | 2 +- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceNpUtility/Export.cpp b/core/libs/prx/libSceNpUtility/Export.cpp index bd009d549a..78daa1fe62 100644 --- a/core/libs/prx/libSceNpUtility/Export.cpp +++ b/core/libs/prx/libSceNpUtility/Export.cpp @@ -17,6 +17,7 @@ static constexpr int SCE_NP_COMMUNITY_ERROR_ABORTED = static_cast(0x8055070 static constexpr int SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT = static_cast(0x8055070C); static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ID = static_cast(0x8055070E); static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_TYPE = static_cast(0x80550711); +static constexpr int SCE_NP_LOOKUP_ERROR_SYNCHRONOUS_REQUEST = static_cast(0x80550307); static constexpr int POLL_FINISHED = 0; static constexpr int POLL_RUNNING = 1; static constexpr std::size_t MAX_TITLE_CONTEXTS = 32; @@ -110,25 +111,29 @@ int AbortRequest(Service& service, int reqId) { return 0; } -int PollAsync(Service& service, int reqId, int* result, bool asyncOnly) { +int PollAsync(Service& service, int reqId, int* result, int synchronousError) { + if (!result) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; std::lock_guard lock(service.mutex); const auto request = service.requests.find(reqId); - if (request == service.requests.end() || (asyncOnly && !request->second.async)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (request == service.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.async) return synchronousError; if (!request->second.finished) return POLL_RUNNING; - if (result) *result = request->second.result; + *result = request->second.result; return POLL_FINISHED; } -int WaitAsync(Service& service, int reqId, int* result, bool asyncOnly) { +int WaitAsync(Service& service, int reqId, int* result, int synchronousError) { + if (!result) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; std::unique_lock lock(service.mutex); auto request = service.requests.find(reqId); - if (request == service.requests.end() || (asyncOnly && !request->second.async)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (request == service.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.async) return synchronousError; service.finished.wait(lock, [&] { request = service.requests.find(reqId); return request == service.requests.end() || request->second.finished; }); if (request == service.requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; - if (result) *result = request->second.result; + *result = request->second.result; return 0; } @@ -498,7 +503,7 @@ int APS5_VABI sceNpLookupNpId(int reqId, const NpOnlineId* onlineId, void* npId, } int APS5_VABI sceNpLookupPollAsync(int reqId, int* result) { - return PollAsync(lookup, reqId, result, false); + return PollAsync(lookup, reqId, result, SCE_NP_LOOKUP_ERROR_SYNCHRONOUS_REQUEST); } int APS5_VABI sceNpLookupSetTimeout(int id, int resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout) { @@ -506,7 +511,7 @@ int APS5_VABI sceNpLookupSetTimeout(int id, int resolveRetry, std::uint32_t reso } int APS5_VABI sceNpLookupWaitAsync(int reqId, int* result) { - return WaitAsync(lookup, reqId, result, false); + return WaitAsync(lookup, reqId, result, SCE_NP_LOOKUP_ERROR_SYNCHRONOUS_REQUEST); } int APS5_VABI sceNpWordFilterAbortRequest(int reqId) { @@ -542,8 +547,7 @@ int APS5_VABI sceNpWordFilterDeleteTitleCtx(int titleCtxId) { } int APS5_VABI sceNpWordFilterPollAsync(int reqId, int* result) { - if (!result) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - return PollAsync(wordFilter, reqId, result, true); + return PollAsync(wordFilter, reqId, result, SCE_NP_COMMUNITY_ERROR_INVALID_ID); } int APS5_VABI sceNpWordFilterSanitizeComment(int reqId, const char* comment, char* sanitizedComment, void* option) { @@ -558,8 +562,7 @@ int APS5_VABI sceNpWordFilterSetTimeout(int id, int resolveRetry, std::uint32_t } int APS5_VABI sceNpWordFilterWaitAsync(int reqId, int* result) { - if (!result) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; - return WaitAsync(wordFilter, reqId, result, true); + return WaitAsync(wordFilter, reqId, result, SCE_NP_COMMUNITY_ERROR_INVALID_ID); } } diff --git a/core/libs/tests/GuestNpUtilityOffline.cpp b/core/libs/tests/GuestNpUtilityOffline.cpp index f4c627f347..b8879b9256 100644 --- a/core/libs/tests/GuestNpUtilityOffline.cpp +++ b/core/libs/tests/GuestNpUtilityOffline.cpp @@ -16,6 +16,7 @@ constexpr int Aborted = static_cast(0x80550707u); constexpr int InsufficientArgument = static_cast(0x8055070Cu); constexpr int InvalidId = static_cast(0x8055070Eu); constexpr int InvalidType = static_cast(0x80550711u); +constexpr int LookupSynchronousRequest = static_cast(0x80550307u); constexpr int PollFinished = 0; constexpr int PollRunning = 1; constexpr int WordFilterTitleCtxBase = 0x01000000; @@ -172,16 +173,20 @@ void CheckLookupAsync() { Require(sceNpLookupSetTimeout(async, 0, 1000000, 10000000, 10000000, 10000000) == 0, "sceNpLookupSetTimeout", "a request timeout was refused"); int result = 7; Require(sceNpLookupPollAsync(async + 100, &result) == InvalidId, "sceNpLookupPollAsync", "an unknown request was taken"); - Require(sceNpLookupPollAsync(async, nullptr) == PollRunning, "sceNpLookupPollAsync", "an unstarted request was not running"); + Require(sceNpLookupPollAsync(async, nullptr) == InvalidArgument, "sceNpLookupPollAsync", "a null result was taken"); + Require(sceNpLookupPollAsync(async + 100, nullptr) == InvalidArgument, "sceNpLookupPollAsync", "an unknown request was checked before a null result"); + Require(sceNpLookupWaitAsync(async, nullptr) == InvalidArgument, "sceNpLookupWaitAsync", "a null result was taken"); + Require(sceNpLookupPollAsync(async, &result) == PollRunning && result == 7, "sceNpLookupPollAsync", "an unstarted request was not running"); Require(WaitInThread("sceNpLookupWaitAsync", sceNpLookupWaitAsync, async, &result, StartLookup) == 0 && result == SignedOut, "sceNpLookupWaitAsync", "waiting did not end with SIGNED_OUT when the lookup started"); - Require(sceNpLookupPollAsync(async, nullptr) == PollFinished, "sceNpLookupPollAsync", "a null result was refused on a finished request"); + Require(sceNpLookupPollAsync(async, nullptr) == InvalidArgument, "sceNpLookupPollAsync", "a null result was taken on a finished request"); result = 7; Require(sceNpLookupPollAsync(async, &result) == PollFinished && result == SignedOut, "sceNpLookupPollAsync", "the request did not finish with SIGNED_OUT"); const int sync = sceNpLookupCreateRequest(context); StartLookup(sync); result = 7; - Require(sceNpLookupWaitAsync(sync, &result) == 0 && result == SignedOut, "sceNpLookupWaitAsync", "a finished synchronous request was not returned"); + Require(sceNpLookupWaitAsync(sync, &result) == LookupSynchronousRequest && result == 7, "sceNpLookupWaitAsync", "a synchronous request was not refused with 0x80550307"); + Require(sceNpLookupPollAsync(sync, &result) == LookupSynchronousRequest && result == 7, "sceNpLookupPollAsync", "a synchronous request was not refused with 0x80550307"); const int aborted = sceNpLookupCreateAsyncRequest(context, asyncParam); Require(sceNpLookupAbortRequest(aborted + 100) == InvalidId, "sceNpLookupAbortRequest", "an unknown request was taken"); diff --git a/docs/dev/debt/libSceNpUtility.md b/docs/dev/debt/libSceNpUtility.md index d83539b646..f163571610 100644 --- a/docs/dev/debt/libSceNpUtility.md +++ b/docs/dev/debt/libSceNpUtility.md @@ -19,4 +19,4 @@ ### Functional - [NpLookup and NpWordFilter](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - offline only, not measured on a PS5: the argument checks and their order, the limit of 32 title contexts and 32 requests, the word filter id bases `0x01000001` and `0x02000001`, the 0x400-byte comment limit and single-use requests (`INVALID_TYPE`) follow shadPS4's [lookup](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_utility/np_utility.cpp#L41-L222) and [word filter](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_utility/np_utility.cpp#L705-L909) calls. `sceNpLookupNpId`, `sceNpWordFilterCensorComment` and `sceNpWordFilterSanitizeComment` then finish with `SCE_NP_ERROR_SIGNED_OUT` and write no NpId or comment; shadPS4's word filter accepts every comment as a stand-in for the server, so its result is not used -- [NpLookup and NpWordFilter waits and timeouts](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - offline, not measured on a PS5: the timeouts of `sceNpLookupSetTimeout` and `sceNpWordFilterSetTimeout` are checked and then have no effect, since no request reaches a server. A wait on a request deleted meanwhile returns `INVALID_ID`. The checks and results follow shadPS4's [lookup](https://github.com/shadps4-emu/shadPS4/blob/cc4dbd45bc10049ea8a445eb0e697de268ef4723/src/core/libraries/np/np_utility/np_utility.cpp#L120-L321) and [word filter](https://github.com/shadps4-emu/shadPS4/blob/cc4dbd45bc10049ea8a445eb0e697de268ef4723/src/core/libraries/np/np_utility/np_utility.cpp#L911-L951) calls: the lookup poll and wait take a null result and a synchronous request, the word filter ones refuse both +- [NpLookup and NpWordFilter waits and timeouts](../../../core/libs/prx/libSceNpUtility/Export.cpp) (libSceNpUtility) - IDs here carry no service: on the console, measured on firmware 13.42's module in an emulator (#10640), each ID holds its service in its high bits (Lookup requests `0x2xxxxxxx` and title contexts `0x4xxxxxxx`, word filter `0x02xxxxxx` and `0x04xxxxxx`), and `SetTimeout`, `AbortRequest`, `PollAsync` and `WaitAsync` find the object by those bits whichever service's call is made; here each call finds only its own service's IDs, which keep the ranges above. The timeouts are checked as measured and then have no effect, since no request reaches a server; the console stores them. A wait on a request deleted meanwhile returns `INVALID_ID`, unmeasured. The measured checks are followed: a null result first (`0x80550704`), a synchronous request (`0x80550307` for Lookup, `INVALID_ID` for the word filter), the timeout bounds and the 0x18-byte async parameter From 3086bd25ea2883c0ac7a550684f1684e1ce64bff Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:38:24 +0530 Subject: [PATCH 49/63] fix(libSceNpGameIntent): return error for invalid arguments Return NpGameIntentErrorInvalidArgument (0x80550004) instead of throwing APS5_INVALID_ARG_EX when invalid arguments are passed to sceNpGameIntentGetPropertyValueString or sceNpGameIntentReceiveIntent. Add regression test coverage in GuestNpGameIntent. --- core/libs/prx/libSceNpGameIntent/Export.cpp | 4 ++-- core/libs/tests/GuestNpGameIntent.cpp | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNpGameIntent/Export.cpp b/core/libs/prx/libSceNpGameIntent/Export.cpp index 751b4b1c5b..783411faca 100644 --- a/core/libs/prx/libSceNpGameIntent/Export.cpp +++ b/core/libs/prx/libSceNpGameIntent/Export.cpp @@ -19,7 +19,7 @@ extern "C" { int APS5_VABI sceNpGameIntentGetPropertyValueString(const NpGameIntentData* intentData, const char* key, char* valueBuf, size_t bufSize) { if (intentData == nullptr || key == nullptr || valueBuf == nullptr || bufSize == 0) { - APS5_INVALID_ARG_EX; + return NpGameIntentErrorInvalidArgument; } valueBuf[0] = '\0'; @@ -33,7 +33,7 @@ int APS5_VABI sceNpGameIntentInitialize(const void* initParam) { int APS5_VABI sceNpGameIntentReceiveIntent(NpGameIntentInfo* intentInfo) { if (intentInfo == nullptr) { - APS5_INVALID_ARG_EX; + return NpGameIntentErrorInvalidArgument; } intentInfo->user_id = NpGameIntentUserIdInvalid; diff --git a/core/libs/tests/GuestNpGameIntent.cpp b/core/libs/tests/GuestNpGameIntent.cpp index dd362cdad4..3ffb412f14 100644 --- a/core/libs/tests/GuestNpGameIntent.cpp +++ b/core/libs/tests/GuestNpGameIntent.cpp @@ -23,6 +23,7 @@ static bool Throws(TCall call) { } int main() { + constexpr int kInvalidArgument = -2141898748; constexpr int kIntentNotFound = -2141898746; constexpr int kValueNotFound = -2141898745; @@ -42,9 +43,18 @@ int main() { Require(info.user_id == -1, "info user_id mismatch"); Require(sceNpGameIntentInitialize(&initParam) == 0, "initialization failed"); + Require(sceNpGameIntentReceiveIntent(nullptr) == kInvalidArgument, "null receive intent did not fail"); Require(sceNpGameIntentReceiveIntent(&info) == kIntentNotFound, "receive intent mismatch"); char buf[64]{}; + Require(sceNpGameIntentGetPropertyValueString(nullptr, "key", buf, sizeof(buf)) == kInvalidArgument, + "null intent data did not fail"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, nullptr, buf, sizeof(buf)) == kInvalidArgument, + "null key did not fail"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", nullptr, sizeof(buf)) == kInvalidArgument, + "null value buf did not fail"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", buf, 0) == kInvalidArgument, + "zero buf size did not fail"); Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", buf, sizeof(buf)) == kValueNotFound, "get property value mismatch"); From d0269109d5e184a3be80fb6b6f3ad5d17657cb13 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:52:39 +0530 Subject: [PATCH 50/63] test(libSceNpGameIntent): remove obsolete app launcher throw assertion --- core/libs/tests/GuestNpGameIntent.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/libs/tests/GuestNpGameIntent.cpp b/core/libs/tests/GuestNpGameIntent.cpp index 3ffb412f14..6e326bb65e 100644 --- a/core/libs/tests/GuestNpGameIntent.cpp +++ b/core/libs/tests/GuestNpGameIntent.cpp @@ -65,8 +65,6 @@ int main() { unsigned char appLauncherParam[0x28]{}; *reinterpret_cast(appLauncherParam) = 0x28; Require(sceNpAppLauncherInitialize(appLauncherParam) == 0, "app launcher init failed"); - Require(Throws([] { sceNpAppLauncherLaunchApp(); }), - "app launcher launch must throw until implemented"); Require(sceNpAppLauncherTerminate() == 0, "app launcher terminate failed"); *reinterpret_cast(appLauncherParam) = 0x10; Require(sceNpAppLauncherInitialize(appLauncherParam) == 0, From b08f1f0234be1947ea113aaffa5fe3a05fb37540 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:52:51 +0530 Subject: [PATCH 51/63] fix(libSceNpGameIntent): return errors for invalid arguments and uninitialized calls --- core/libs/prx/libSceNpGameIntent/Export.cpp | 45 ++++++++++++------ core/libs/tests/GuestNpGameIntent.cpp | 52 ++++++++++++++++++++- docs/dev/debt/libSceNpGameIntent.md | 7 +-- 3 files changed, 84 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceNpGameIntent/Export.cpp b/core/libs/prx/libSceNpGameIntent/Export.cpp index 783411faca..dfea590bc3 100644 --- a/core/libs/prx/libSceNpGameIntent/Export.cpp +++ b/core/libs/prx/libSceNpGameIntent/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceNpGameIntent/include/Api.hpp" +#include #include #include #include @@ -8,43 +9,57 @@ namespace { +constexpr int NpGameIntentErrorNotInitialized = -2141898750; constexpr int NpGameIntentErrorInvalidArgument = -2141898748; constexpr int NpGameIntentErrorIntentNotFound = -2141898746; constexpr int NpGameIntentErrorValueNotFound = -2141898745; constexpr int NpGameIntentUserIdInvalid = -1; +std::atomic_bool& NpGameIntentInitialized() { + static std::atomic_bool initialized{false}; + return initialized; +} + } extern "C" { int APS5_VABI sceNpGameIntentGetPropertyValueString(const NpGameIntentData* intentData, const char* key, char* valueBuf, size_t bufSize) { - if (intentData == nullptr || key == nullptr || valueBuf == nullptr || bufSize == 0) { - return NpGameIntentErrorInvalidArgument; - } + if (!NpGameIntentInitialized()) { + return NpGameIntentErrorNotInitialized; + } + if (intentData == nullptr || key == nullptr || key[0] == '\0' || valueBuf == nullptr || bufSize == 0) { + return NpGameIntentErrorInvalidArgument; + } - valueBuf[0] = '\0'; - return NpGameIntentErrorValueNotFound; + valueBuf[0] = '\0'; + return NpGameIntentErrorValueNotFound; } int APS5_VABI sceNpGameIntentInitialize(const void* initParam) { - (void)initParam; - return 0; + (void)initParam; + NpGameIntentInitialized() = true; + return 0; } int APS5_VABI sceNpGameIntentReceiveIntent(NpGameIntentInfo* intentInfo) { - if (intentInfo == nullptr) { - return NpGameIntentErrorInvalidArgument; - } + if (!NpGameIntentInitialized()) { + return NpGameIntentErrorNotInitialized; + } + if (intentInfo == nullptr || intentInfo->size != 0x4140) { + return NpGameIntentErrorInvalidArgument; + } - intentInfo->user_id = NpGameIntentUserIdInvalid; - std::memset(intentInfo->intent_type, 0, sizeof(intentInfo->intent_type)); - std::memset(&intentInfo->intent_data, 0, sizeof(intentInfo->intent_data)); + intentInfo->user_id = NpGameIntentUserIdInvalid; + std::memset(intentInfo->intent_type, 0, sizeof(intentInfo->intent_type)); + std::memset(&intentInfo->intent_data, 0, sizeof(intentInfo->intent_data)); - return NpGameIntentErrorIntentNotFound; + return NpGameIntentErrorIntentNotFound; } int APS5_VABI sceNpGameIntentTerminate(void) { - return 0; + NpGameIntentInitialized() = false; + return 0; } int APS5_VABI sceNpGameIntentInitParamInit(NpGameIntentInitParam* param) { diff --git a/core/libs/tests/GuestNpGameIntent.cpp b/core/libs/tests/GuestNpGameIntent.cpp index 6e326bb65e..8e74607008 100644 --- a/core/libs/tests/GuestNpGameIntent.cpp +++ b/core/libs/tests/GuestNpGameIntent.cpp @@ -4,6 +4,7 @@ #include #include +#include #include static void Require(bool value, const char* what) { @@ -23,6 +24,7 @@ static bool Throws(TCall call) { } int main() { + constexpr int kNotInitialized = -2141898750; constexpr int kInvalidArgument = -2141898748; constexpr int kIntentNotFound = -2141898746; constexpr int kValueNotFound = -2141898745; @@ -42,26 +44,72 @@ int main() { Require(info.size == sizeof(NpGameIntentInfo), "info size mismatch"); Require(info.user_id == -1, "info user_id mismatch"); + char preBuf[64]{}; + Require(sceNpGameIntentGetPropertyValueString(nullptr, "key", preBuf, sizeof(preBuf)) == kNotInitialized, + "uninitialized get property string with null data must report not-initialized"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", preBuf, sizeof(preBuf)) == kNotInitialized, + "uninitialized get property string with valid data must report not-initialized"); + Require(sceNpGameIntentReceiveIntent(nullptr) == kNotInitialized, + "uninitialized receive intent with null pointer must report not-initialized"); + Require(sceNpGameIntentReceiveIntent(&info) == kNotInitialized, + "uninitialized receive intent with valid pointer must report not-initialized"); + Require(sceNpGameIntentInitialize(&initParam) == 0, "initialization failed"); Require(sceNpGameIntentReceiveIntent(nullptr) == kInvalidArgument, "null receive intent did not fail"); - Require(sceNpGameIntentReceiveIntent(&info) == kIntentNotFound, "receive intent mismatch"); + Require(sceNpGameIntentReceiveIntent(&info) == kInvalidArgument, "mismatched struct size must report invalid argument"); + info.size = 0; + Require(sceNpGameIntentReceiveIntent(&info) == kInvalidArgument, "zero size must report invalid argument"); + info.size = 0x413f; + Require(sceNpGameIntentReceiveIntent(&info) == kInvalidArgument, "0x413f size must report invalid argument"); + info.size = 0x4141; + Require(sceNpGameIntentReceiveIntent(&info) == kInvalidArgument, "0x4141 size must report invalid argument"); + info.size = sizeof(NpGameIntentInfo); char buf[64]{}; Require(sceNpGameIntentGetPropertyValueString(nullptr, "key", buf, sizeof(buf)) == kInvalidArgument, "null intent data did not fail"); Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, nullptr, buf, sizeof(buf)) == kInvalidArgument, "null key did not fail"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "", buf, sizeof(buf)) == kInvalidArgument, + "empty key did not fail"); Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", nullptr, sizeof(buf)) == kInvalidArgument, "null value buf did not fail"); Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", buf, 0) == kInvalidArgument, "zero buf size did not fail"); - Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", buf, sizeof(buf)) == kValueNotFound, + + char canaryBuf[64]{}; + std::memset(canaryBuf, 'X', sizeof(canaryBuf)); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", canaryBuf, 0) == kInvalidArgument, + "zero buf size canary call did not fail"); + Require(canaryBuf[0] == 'X', "buffer was modified on zero size error"); + Require(sceNpGameIntentGetPropertyValueString(nullptr, "key", canaryBuf, sizeof(canaryBuf)) == kInvalidArgument, + "null data canary call did not fail"); + Require(canaryBuf[0] == 'X', "buffer was modified on null data error"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, nullptr, canaryBuf, sizeof(canaryBuf)) == kInvalidArgument, + "null key canary call did not fail"); + Require(canaryBuf[0] == 'X', "buffer was modified on null key error"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "", canaryBuf, sizeof(canaryBuf)) == kInvalidArgument, + "empty key canary call did not fail"); + Require(canaryBuf[0] == 'X', "buffer was modified on empty key error"); + + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", canaryBuf, sizeof(canaryBuf)) == kValueNotFound, "get property value mismatch"); + Require(canaryBuf[0] == '\0', "valid call did not write terminator"); + + canaryBuf[0] = 'Y'; + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", canaryBuf, 1) == kValueNotFound, + "bufSize 1 did not succeed"); + Require(canaryBuf[0] == '\0', "bufSize 1 did not write terminator"); Require(sceNpGameIntentVshInitialize() == 0, "vsh init failed"); Require(sceNpGameIntentVshTerminate() == 0, "vsh terminate failed"); Require(sceNpGameIntentTerminate() == 0, "terminate failed"); + Require(sceNpGameIntentGetPropertyValueString(&info.intent_data, "key", buf, sizeof(buf)) == kNotInitialized, + "post-terminate get property value must report not-initialized"); + Require(sceNpGameIntentReceiveIntent(&info) == kNotInitialized, + "post-terminate receive intent must report not-initialized"); + unsigned char appLauncherParam[0x28]{}; *reinterpret_cast(appLauncherParam) = 0x28; Require(sceNpAppLauncherInitialize(appLauncherParam) == 0, "app launcher init failed"); diff --git a/docs/dev/debt/libSceNpGameIntent.md b/docs/dev/debt/libSceNpGameIntent.md index 622b40a2c2..8476d2c449 100644 --- a/docs/dev/debt/libSceNpGameIntent.md +++ b/docs/dev/debt/libSceNpGameIntent.md @@ -2,11 +2,12 @@ ### Silent stubs -- [sceNpGameIntentVshInitialize, sceNpGameIntentVshTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - VSH initialization and termination return 0 without performing shell operations, matching `sceNpGameIntentInitialize` and `sceNpGameIntentTerminate` -- [sceNpAppLauncherInitialize, sceNpAppLauncherTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - app-launcher lifecycle returns success without tracking launch state; `sceNpAppLauncherInitialize` accepts its init parameter without validation, following the silent-stub precedent of `sceNpGameIntentInitialize` — a title calls it early in boot, and returning success lets it continue +- [sceNpGameIntentVshInitialize, sceNpGameIntentVshTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - VSH initialization and termination return 0 without performing shell operations +- [sceNpAppLauncherInitialize, sceNpAppLauncherTerminate](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - app-launcher lifecycle returns success without tracking launch state; `sceNpAppLauncherInitialize` accepts its init parameter without validation — a title calls it early in boot, and returning success lets it continue ### Unknown function info +- [sceNpGameIntentReceiveIntent, sceNpGameIntentGetPropertyValueString](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - firmware 13.42 emulator traces report `0x80553802` when called before `sceNpGameIntentInitialize` (precedence over argument validity), and `0x80553804` for invalid arguments (null pointer, empty key, or zero buffer size); `sceNpGameIntentTerminate` resetting the initialization state is modeled lifecycle behavior rather than independently hardware-verified. `sceNpGameIntentReceiveIntent` reportedly requires `info->size == 0x4140` on firmware 13.42, while the current AnyPS5 `NpGameIntentInfo` is `0x560` bytes (matching fpPS4) and `sceNpGameIntentInfoInit` sets that size. Consequently, an object initialized by the current helper is rejected by the receiver's measured size guard. The complete retail layout, its output field offsets, and the retail behavior of `sceNpGameIntentInfoInit` have not been verified, and expanding the type with unverified reserved bytes is not an evidence-backed solution. - [sceNpAppLauncherLaunchApp, sceNpGameIntentGetUdsNpComId, sceNpGameIntentLaunchApp, sceNpGameIntentLaunchApp2, sceNpGameIntentNotifyAppLaunched, sceNpSessionServiceGetJoinedPartySession, sceNpSessionServiceGetRunningGamePlayerSession and sceNpSessionServiceSendPlayerSessionInvitations](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpGameIntent.c). Call sites in a title's executable give a partial prototype: `sceNpAppLauncherLaunchApp` takes `(user_id, uri struct, context)` and returns int — throws because no launcher-service semantics are derivable. The rest are declared without parameters solely to resolve known imports and make no ABI claim. - [sceNpGameIntentInitParamInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L17) `SceNpGameIntentInitParam`; setting `size`, zeroing reserved bytes, and throwing on null pointer follow Sony SDK `*ParamInit` conventions but are unverified on retail hardware -- [sceNpGameIntentInfoInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L28) `SceNpGameIntentInfo`; setting `size`, `user_id = -1`, zeroing fields, and throwing on null pointer match `sceNpGameIntentReceiveIntent` defaults but are unverified on retail hardware +- [sceNpGameIntentInfoInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L28) `SceNpGameIntentInfo`; setting `size = 0x560`, `user_id = -1`, zeroing fields, and throwing on null pointer follow fpPS4 defaults, but reported firmware 13.42 behavior requires `0x4140` for `sceNpGameIntentReceiveIntent`, so the helper's `0x560` size does not match retail receiver defaults; the complete retail structure layout and retail behavior of `sceNpGameIntentInfoInit` remain unverified From 59c7e535c0d2d6ea2c6e2ba0ac8e0ec5f752e105 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:21:19 +0530 Subject: [PATCH 52/63] docs(libSceNpGameIntent): concise technical debt entry for receive intent size guard --- docs/dev/debt/libSceNpGameIntent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpGameIntent.md b/docs/dev/debt/libSceNpGameIntent.md index 8476d2c449..cc0daa50c5 100644 --- a/docs/dev/debt/libSceNpGameIntent.md +++ b/docs/dev/debt/libSceNpGameIntent.md @@ -7,7 +7,7 @@ ### Unknown function info -- [sceNpGameIntentReceiveIntent, sceNpGameIntentGetPropertyValueString](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - firmware 13.42 emulator traces report `0x80553802` when called before `sceNpGameIntentInitialize` (precedence over argument validity), and `0x80553804` for invalid arguments (null pointer, empty key, or zero buffer size); `sceNpGameIntentTerminate` resetting the initialization state is modeled lifecycle behavior rather than independently hardware-verified. `sceNpGameIntentReceiveIntent` reportedly requires `info->size == 0x4140` on firmware 13.42, while the current AnyPS5 `NpGameIntentInfo` is `0x560` bytes (matching fpPS4) and `sceNpGameIntentInfoInit` sets that size. Consequently, an object initialized by the current helper is rejected by the receiver's measured size guard. The complete retail layout, its output field offsets, and the retail behavior of `sceNpGameIntentInfoInit` have not been verified, and expanding the type with unverified reserved bytes is not an evidence-backed solution. +- [sceNpGameIntentReceiveIntent, sceNpGameIntentGetPropertyValueString](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - firmware 13.42 traces report `0x80553802` before `sceNpGameIntentInitialize` (precedence over argument validity), and `0x80553804` for invalid arguments (null pointer, empty key, or zero buffer size); `sceNpGameIntentTerminate` resetting initialized state is modeled lifecycle behavior rather than hardware-verified. `sceNpGameIntentReceiveIntent` reportedly requires `info->size == 0x4140` on firmware 13.42, while AnyPS5 `NpGameIntentInfo` is `0x560` bytes (matching fpPS4) and set by `sceNpGameIntentInfoInit`, so initialized objects are rejected by the receiver's measured size guard. Complete retail layout, output field offsets, and retail behavior of `sceNpGameIntentInfoInit` are unverified; expanding the type with unverified reserved bytes is not an evidence-backed solution. - [sceNpAppLauncherLaunchApp, sceNpGameIntentGetUdsNpComId, sceNpGameIntentLaunchApp, sceNpGameIntentLaunchApp2, sceNpGameIntentNotifyAppLaunched, sceNpSessionServiceGetJoinedPartySession, sceNpSessionServiceGetRunningGamePlayerSession and sceNpSessionServiceSendPlayerSessionInvitations](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpGameIntent.c). Call sites in a title's executable give a partial prototype: `sceNpAppLauncherLaunchApp` takes `(user_id, uri struct, context)` and returns int — throws because no launcher-service semantics are derivable. The rest are declared without parameters solely to resolve known imports and make no ABI claim. - [sceNpGameIntentInitParamInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L17) `SceNpGameIntentInitParam`; setting `size`, zeroing reserved bytes, and throwing on null pointer follow Sony SDK `*ParamInit` conventions but are unverified on retail hardware - [sceNpGameIntentInfoInit](../../../core/libs/prx/libSceNpGameIntent/Export.cpp) (libSceNpGameIntent) - struct layout matches [fpPS4](https://github.com/red-prig/fpPS4/blob/master/src/np/ps4_libscenpgameintent.pas#L28) `SceNpGameIntentInfo`; setting `size = 0x560`, `user_id = -1`, zeroing fields, and throwing on null pointer follow fpPS4 defaults, but reported firmware 13.42 behavior requires `0x4140` for `sceNpGameIntentReceiveIntent`, so the helper's `0x560` size does not match retail receiver defaults; the complete retail structure layout and retail behavior of `sceNpGameIntentInfoInit` remain unverified From 814aea6f50903428d3629fb61c616a1db249afd3 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 05:18:43 +1300 Subject: [PATCH 53/63] test(playgo): use the valid handle and the pinned error codes for the speed, ETA and prefetch checks The implementation checks the handle first and reports BAD_HANDLE (0x80B20009) or BAD_POINTER (0x80B2000A); handle 1 is the valid one, so the pointer and speed cases use it and the invalid-handle cases expect BAD_HANDLE. --- core/libs/tests/GuestPlayGo.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index fc0b371eb8..2df22de9cc 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -262,12 +262,16 @@ int main(int argc, char** argv) { RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); std::int64_t eta = -1; std::int32_t speed = -1; - Require(scePlayGoGetEta(-1, nullptr, 0, &eta) == static_cast(0x80CF0002)); - Require(scePlayGoGetEta(0x506C476F, nullptr, 0, nullptr) == static_cast(0x80CF0004)); - Require(scePlayGoGetInstallSpeed(-1, &speed) == static_cast(0x80CF0002)); - Require(scePlayGoGetInstallSpeed(0x506C476F, nullptr) == static_cast(0x80CF0004)); - Require(scePlayGoGetInstallSpeed(0x506C476F, &speed) == 0 && speed == 2); - Require(scePlayGoPrefetch(-1, nullptr, 0, 0) == static_cast(0x80CF0002)); + constexpr int validHandle = 1; + constexpr int badHandleError = static_cast(0x80B20009); + constexpr int badPointerError = static_cast(0x80B2000A); + Require(scePlayGoGetEta(-1, nullptr, 0, &eta) == badHandleError); + Require(scePlayGoGetEta(validHandle, nullptr, 0, nullptr) == badPointerError); + Require(scePlayGoGetInstallSpeed(-1, &speed) == badHandleError); + Require(scePlayGoGetInstallSpeed(0x506C476F, &speed) == badHandleError && speed == -1); + Require(scePlayGoGetInstallSpeed(validHandle, nullptr) == badPointerError); + Require(scePlayGoGetInstallSpeed(validHandle, &speed) == 0 && speed == 2); + Require(scePlayGoPrefetch(-1, nullptr, 0, 0) == badHandleError); RunLayout(argv[0], "--scenario-only", {{"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--ficm-only", {{"playgo-ficm.dat", "x"}}); RunLayout(argv[0], "--hash-table-only", {{"playgo-hash-table.dat", "x"}}); From 8c7b35c6725448e8dd43e9ca76715c5b62b8e5d0 Mon Sep 17 00:00:00 2001 From: slopgobblercobbler Date: Sun, 25 Oct 2026 02:28:42 +0100 Subject: [PATCH 54/63] feat(libkernel): complete FreeBSD 11.4 errno parity (ENOTRECOVERABLE and EOWNERDEAD) --- core/libs/prx/libkernel/KernelErrors.hpp | 2 ++ core/libs/tests/KernelError.cpp | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/KernelErrors.hpp b/core/libs/prx/libkernel/KernelErrors.hpp index b7d37ba4f2..e3e5b759fd 100644 --- a/core/libs/prx/libkernel/KernelErrors.hpp +++ b/core/libs/prx/libkernel/KernelErrors.hpp @@ -99,5 +99,7 @@ inline constexpr int SCE_KERNEL_ERROR_ENOLINK = SceKernelError(91); inline constexpr int SCE_KERNEL_ERROR_EPROTO = SceKernelError(92); inline constexpr int SCE_KERNEL_ERROR_ENOTCAPABLE = SceKernelError(93); inline constexpr int SCE_KERNEL_ERROR_ECAPMODE = SceKernelError(94); +inline constexpr int SCE_KERNEL_ERROR_ENOTRECOVERABLE = SceKernelError(95); +inline constexpr int SCE_KERNEL_ERROR_EOWNERDEAD = SceKernelError(96); #endif diff --git a/core/libs/tests/KernelError.cpp b/core/libs/tests/KernelError.cpp index 52c41c28db..d0c1aa0bac 100644 --- a/core/libs/tests/KernelError.cpp +++ b/core/libs/tests/KernelError.cpp @@ -12,14 +12,17 @@ static void Require(bool value) { if (!value) throw std::runtime_error("Kernel e int main() { *__error_nid_postfix() = 123; - const std::array, 6> cases{{ + const std::array, 8> cases{{ {0, 0}, {1, SCE_KERNEL_ERROR_EPERM}, {2, SCE_KERNEL_ERROR_ENOENT}, - {4, SCE_KERNEL_ERROR_EINTR}, {14, SCE_KERNEL_ERROR_EFAULT}, {22, SCE_KERNEL_ERROR_EINVAL} + {4, SCE_KERNEL_ERROR_EINTR}, {14, SCE_KERNEL_ERROR_EFAULT}, {22, SCE_KERNEL_ERROR_EINVAL}, + {95, SCE_KERNEL_ERROR_ENOTRECOVERABLE}, {96, SCE_KERNEL_ERROR_EOWNERDEAD} }}; for (const auto [input, expected] : cases) { Require(sceKernelError(input) == expected); Require(*__error_nid_postfix() == 123); } + Require(SCE_KERNEL_ERROR_ENOTRECOVERABLE == static_cast(0x8002005fu)); + Require(SCE_KERNEL_ERROR_EOWNERDEAD == static_cast(0x80020060u)); Require(sceKernelError(-1) == static_cast(0x8001ffffu)); Require(sceKernelError(std::numeric_limits::max()) == 0x1ffff); Require(sceKernelError(std::numeric_limits::min()) == 0x20000); From 31a69542058e8272820a20a7bab4d44f2ec1a2cf Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 05:39:51 +1300 Subject: [PATCH 55/63] test(playgo): initialize and open PlayGo for the valid-handle checks The speed, ETA and prefetch checks now take their valid handle from scePlayGoInitialize and scePlayGoOpen in the same process and close it afterwards, instead of assuming handle 1. --- core/libs/tests/GuestPlayGo.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index 2df22de9cc..76bfd50679 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -262,7 +262,10 @@ int main(int argc, char** argv) { RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); std::int64_t eta = -1; std::int32_t speed = -1; - constexpr int validHandle = 1; + PlayGoInitParams fixtureInit{}; + int validHandle = -1; + Require(scePlayGoInitialize(&fixtureInit) == 0); + Require(scePlayGoOpen(&validHandle, nullptr) == 0); constexpr int badHandleError = static_cast(0x80B20009); constexpr int badPointerError = static_cast(0x80B2000A); Require(scePlayGoGetEta(-1, nullptr, 0, &eta) == badHandleError); @@ -272,6 +275,7 @@ int main(int argc, char** argv) { Require(scePlayGoGetInstallSpeed(validHandle, nullptr) == badPointerError); Require(scePlayGoGetInstallSpeed(validHandle, &speed) == 0 && speed == 2); Require(scePlayGoPrefetch(-1, nullptr, 0, 0) == badHandleError); + Require(scePlayGoClose(validHandle) == 0); RunLayout(argv[0], "--scenario-only", {{"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--ficm-only", {{"playgo-ficm.dat", "x"}}); RunLayout(argv[0], "--hash-table-only", {{"playgo-hash-table.dat", "x"}}); From b7c91d33721a75221fdfa54bbb4691b08e65cf6e Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 10:30:46 +1300 Subject: [PATCH 56/63] fix(libSceVideoOut): declare the sized color wrappers once in VideoOut.hpp --- core/libs/prx/libSceVideoOut/include/Output.hpp | 2 -- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/include/Output.hpp b/core/libs/prx/libSceVideoOut/include/Output.hpp index f4c0e74aa2..caebc2514a 100644 --- a/core/libs/prx/libSceVideoOut/include/Output.hpp +++ b/core/libs/prx/libSceVideoOut/include/Output.hpp @@ -27,8 +27,6 @@ int APS5_VABI sceVideoOutLatencyControlWaitBeforeInput(int handle); int APS5_VABI sceVideoOutLatencyMeasureSetStartPoint(int handle, uint32_t point); int APS5_VABI sceVideoOutColorSettingsSetGamma(VideoOutColorSettings* settings, float gamma); int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* settings); -int APS5_VABI sceVideoOutColorSettingsSetGamma_(VideoOutColorSettings* settings, float gamma, uint32_t settings_size); -int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size); int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings); int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option); int APS5_VABI sceVideoOutAdjustColorSpaceConversion(int handle, const VideoOutColorSpaceConversionSettings* settings); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 0b30de6e98..e7ce91e7b8 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -2,6 +2,7 @@ #include "prx/libSceVideoOut/include/VideoOutDriver.hpp" #include "prx/libSceVideoOut/include/Buffer.hpp" #include "prx/libSceVideoOut/include/Output.hpp" +#include "prx/libSceVideoOut/include/VideoOut.hpp" #include "prx/libSceVideoOut/include/Event.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" From a36fd27f64513a9dd20efbc92f9f3d88509f958f Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:43:17 +0000 Subject: [PATCH 57/63] fix(freetype): reconcile provider NIDs and public declarations --- core/libs/prx/libSceFreeType/include/Api.hpp | 3 +++ .../prx/libSceFreeType/src/OutlineBorders.cpp | 2 +- .../prx/libSceFreeTypeOl/libSceFreeTypeOl.hpp | 2 ++ core/libs/prx/libSceFreeTypeOt/Export.cpp | 20 +++++++++---------- .../prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp | 2 ++ .../libs/tests/GuestFreeTypeOutlineBorder.cpp | 6 ++++-- docs/dev/debt/libSceFreeTypeOt.md | 4 +++- 7 files changed, 25 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index ac044b7919..163d4120df 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -6,6 +6,9 @@ extern "C" { +std::int32_t APS5_VABI FT_Outline_GetInsideBorder_nid_postfix(const FreeType::GuestOutline* outline); +std::int32_t APS5_VABI FT_Outline_GetOutsideBorder_nid_postfix(const FreeType::GuestOutline* outline); + int APS5_VABI FT_Init_FreeType_nid_postfix(FreeType::GuestLibrary** alibrary); int APS5_VABI FT_Done_FreeType_nid_postfix(FreeType::GuestLibrary* library); int APS5_VABI FT_New_Face_nid_postfix(FreeType::GuestLibrary* library, const char* filepathname, std::int64_t faceIndex, FreeType::GuestFaceRec** aface); diff --git a/core/libs/prx/libSceFreeType/src/OutlineBorders.cpp b/core/libs/prx/libSceFreeType/src/OutlineBorders.cpp index 0362e25ff0..34d54d7c03 100644 --- a/core/libs/prx/libSceFreeType/src/OutlineBorders.cpp +++ b/core/libs/prx/libSceFreeType/src/OutlineBorders.cpp @@ -1,4 +1,4 @@ -#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libSceFreeType/include/Api.hpp" #include #include #include diff --git a/core/libs/prx/libSceFreeTypeOl/libSceFreeTypeOl.hpp b/core/libs/prx/libSceFreeTypeOl/libSceFreeTypeOl.hpp index 6f70f09bee..e1970ef93b 100644 --- a/core/libs/prx/libSceFreeTypeOl/libSceFreeTypeOl.hpp +++ b/core/libs/prx/libSceFreeTypeOl/libSceFreeTypeOl.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceFreeType/include/Api.hpp" diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index dd0e06da52..d4478564f0 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -216,10 +216,10 @@ int APS5_VABI sceFreeTypeOtUnknown_4RRjK_Wd1lU_nid_no_patch() { } APS5_EXPORT("4RRjK-Wd1lU", sceFreeTypeOtUnknown_4RRjK_Wd1lU_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_weVsBCT_TTQ_nid_no_patch() { +int APS5_VABI sceFreeTypeOtOutlineGetOrientation_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("weVsBCT+TTQ", sceFreeTypeOtUnknown_weVsBCT_TTQ_nid_no_patch); +APS5_EXPORT("weVsBCT+TTQ", sceFreeTypeOtOutlineGetOrientation_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_QH2emcgeFck_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -301,10 +301,10 @@ int APS5_VABI sceFreeTypeOtUnknown_z9Wsaf34JA4_nid_no_patch() { } APS5_EXPORT("z9Wsaf34JA4", sceFreeTypeOtUnknown_z9Wsaf34JA4_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_PaDk9bdmdhU_nid_no_patch() { +int APS5_VABI sceFreeTypeOtRequestSize_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("PaDk9bdmdhU", sceFreeTypeOtUnknown_PaDk9bdmdhU_nid_no_patch); +APS5_EXPORT("PaDk9bdmdhU", sceFreeTypeOtRequestSize_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_Z8KG6700sw4_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -341,10 +341,10 @@ int APS5_VABI sceFreeTypeOtUnknown_yCEJIsjIOO8_nid_no_patch() { } APS5_EXPORT("yCEJIsjIOO8", sceFreeTypeOtUnknown_yCEJIsjIOO8_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_L1uHkWTazkI_nid_no_patch() { +int APS5_VABI sceFreeTypeOtSin_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("L1uHkWTazkI", sceFreeTypeOtUnknown_L1uHkWTazkI_nid_no_patch); +APS5_EXPORT("L1uHkWTazkI", sceFreeTypeOtSin_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_C7N2GuwqTA0_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -616,10 +616,10 @@ int APS5_VABI sceFreeTypeOtUnknown_B3uU5aDZn94_nid_no_patch() { } APS5_EXPORT("B3uU5aDZn94", sceFreeTypeOtUnknown_B3uU5aDZn94_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_dojyaLKzKN0_nid_no_patch() { +int APS5_VABI sceFreeTypeOtTan_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("dojyaLKzKN0", sceFreeTypeOtUnknown_dojyaLKzKN0_nid_no_patch); +APS5_EXPORT("dojyaLKzKN0", sceFreeTypeOtTan_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_08_mhD_AAds_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -666,10 +666,10 @@ int APS5_VABI sceFreeTypeOtUnknown_PrZtZyFwM6c_nid_no_patch() { } APS5_EXPORT("PrZtZyFwM6c", sceFreeTypeOtUnknown_PrZtZyFwM6c_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_3WpTQrvGUcU_nid_no_patch() { +int APS5_VABI sceFreeTypeOtVectorUnit_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("3WpTQrvGUcU", sceFreeTypeOtUnknown_3WpTQrvGUcU_nid_no_patch); +APS5_EXPORT("3WpTQrvGUcU", sceFreeTypeOtVectorUnit_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_P6WOAeb0lJ8_nid_no_patch() { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp b/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp index 6f70f09bee..e1970ef93b 100644 --- a/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp +++ b/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceFreeType/include/Api.hpp" diff --git a/core/libs/tests/GuestFreeTypeOutlineBorder.cpp b/core/libs/tests/GuestFreeTypeOutlineBorder.cpp index e191a4c131..28dd07b908 100644 --- a/core/libs/tests/GuestFreeTypeOutlineBorder.cpp +++ b/core/libs/tests/GuestFreeTypeOutlineBorder.cpp @@ -1,4 +1,4 @@ -#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libSceFreeType/include/Api.hpp" #include #include FT_FREETYPE_H #include FT_STROKER_H @@ -13,6 +13,7 @@ #include #include #include +#include #include #ifdef _WIN32 #include @@ -24,7 +25,8 @@ using FreeType::GuestOutline; using FreeType::GuestVector; -using Query = int (APS5_VABI*)(const GuestOutline*); +using Query = decltype(&FT_Outline_GetInsideBorder_nid_postfix); +static_assert(std::is_same_v); static_assert(sizeof(GuestOutline) == 40 && alignof(GuestOutline) == 8); static_assert(offsetof(GuestOutline, points) == 8 && offsetof(GuestOutline, contours) == 24); static_assert(offsetof(GuestOutline, flags) == 32 && sizeof(GuestVector) == 16); diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 09b25390e2..2babe4103e 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -4,4 +4,6 @@ - [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. -- The 148 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 143 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. + +- The FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan and FT_Vector_Unit NIDs in this provider are identified, but their provider-specific prototypes and behavior remain unverified. Their named helpers retain the explicit inventory aliases and throw; the separate libSceFreeType implementations do not supply this provider. From 54dadc019e415d286a2ed1ca6fb4091b4ece6e68 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:43:52 +0000 Subject: [PATCH 58/63] fix(freetype): retain newly identified Ot inventory aliases --- core/libs/prx/libSceFreeTypeOt/Export.cpp | 12 ++++++------ docs/dev/debt/libSceFreeTypeOt.md | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index d4478564f0..c4ec1cd9ca 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -156,10 +156,10 @@ int APS5_VABI sceFreeTypeOtUnknown_tCMhTh7h8D4_nid_no_patch() { } APS5_EXPORT("tCMhTh7h8D4", sceFreeTypeOtUnknown_tCMhTh7h8D4_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_zyezaHweY3U_nid_no_patch() { +int APS5_VABI sceFreeTypeOtNewSize_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("zyezaHweY3U", sceFreeTypeOtUnknown_zyezaHweY3U_nid_no_patch); +APS5_EXPORT("zyezaHweY3U", sceFreeTypeOtNewSize_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_C_PpdS5zris_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -176,15 +176,15 @@ int APS5_VABI sceFreeTypeOtUnknown_OeNH41MA2Vs_nid_no_patch() { } APS5_EXPORT("OeNH41MA2Vs", sceFreeTypeOtUnknown_OeNH41MA2Vs_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_Bhm8Injl0KA_nid_no_patch() { +int APS5_VABI sceFreeTypeOtOutlineCopy_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("Bhm8Injl0KA", sceFreeTypeOtUnknown_Bhm8Injl0KA_nid_no_patch); +APS5_EXPORT("Bhm8Injl0KA", sceFreeTypeOtOutlineCopy_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown___lgV9pRsO4_nid_no_patch() { +int APS5_VABI sceFreeTypeOtOutlineDecompose_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("++lgV9pRsO4", sceFreeTypeOtUnknown___lgV9pRsO4_nid_no_patch); +APS5_EXPORT("++lgV9pRsO4", sceFreeTypeOtOutlineDecompose_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_BqGJsJcFYaM_nid_no_patch() { NotImplemented_nid_no_patch(__func__); diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 2babe4103e..6e93bdf35f 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -4,6 +4,6 @@ - [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. -- The 143 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 140 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. -- The FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan and FT_Vector_Unit NIDs in this provider are identified, but their provider-specific prototypes and behavior remain unverified. Their named helpers retain the explicit inventory aliases and throw; the separate libSceFreeType implementations do not supply this provider. +- The FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan, FT_Vector_Unit, FT_New_Size, FT_Outline_Copy and FT_Outline_Decompose NIDs in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are identified, but their provider-specific prototypes and behavior remain unverified. Their named helpers retain the explicit inventory aliases and throw; the separate libSceFreeType implementations do not supply this provider. From 0a9e48f886f4478c281303e0896252429c6ade85 Mon Sep 17 00:00:00 2001 From: UnhealthyKraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:41:18 +0000 Subject: [PATCH 59/63] fix(freetype): build the patched kernel needed by border fixtures --- core/libs/prx/libSceFreeTypeOl/CMakeLists.txt | 2 +- core/libs/tests/GuestFreeTypeOutlineBorder.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt b/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt index a9053faf5c..2caced1fd2 100644 --- a/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt +++ b/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt @@ -11,7 +11,7 @@ if(BUILD_TESTING) endif() configure_windows_unwind(guest_freetype_outline_border_tests) add_custom_target(freetype_outline_border_patched DEPENDS - ${PATCHED_DIR}/libSceFreeTypeOl.prx ${PATCHED_DIR}/libSceFreeTypeOt.prx ${LIBC_PATCHED}) + ${PATCHED_DIR}/libSceFreeTypeOl.prx ${PATCHED_DIR}/libSceFreeTypeOt.prx ${PATCHED_DIR}/libkernel.prx ${LIBC_PATCHED}) add_dependencies(guest_freetype_outline_border_tests freetype_outline_border_patched) add_test(NAME guest_freetype_outline_border_reference COMMAND guest_freetype_outline_border_tests reference) foreach(module IN ITEMS Ol Ot) diff --git a/core/libs/tests/GuestFreeTypeOutlineBorder.cpp b/core/libs/tests/GuestFreeTypeOutlineBorder.cpp index 28dd07b908..609ec204f9 100644 --- a/core/libs/tests/GuestFreeTypeOutlineBorder.cpp +++ b/core/libs/tests/GuestFreeTypeOutlineBorder.cpp @@ -247,6 +247,7 @@ int main(int argc, char** argv) { auto query = reinterpret_cast(GetProcAddress(library, symbol)); #else const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + if (!library) std::fprintf(stderr, "%s\n", dlerror()); Require(library != nullptr, "load requested library"); auto query = reinterpret_cast(dlsym(library, symbol)); #endif From e9d4b8c282eda3ed49c7700f82f5a6228713dca2 Mon Sep 17 00:00:00 2001 From: UnhealthyKraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:44:55 +0000 Subject: [PATCH 60/63] test(freetype): enforce matching patched runtime DLL paths --- core/libs/prx/libSceFreeTypeOl/CMakeLists.txt | 5 +++-- core/libs/tests/GuestFreeTypeOutlineBorder.cpp | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt b/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt index 2caced1fd2..e8f18eac97 100644 --- a/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt +++ b/core/libs/prx/libSceFreeTypeOl/CMakeLists.txt @@ -23,9 +23,10 @@ if(BUILD_TESTING) set_tests_properties(guest_freetype_outline_border_${module}_${border} guest_freetype_outline_border_${module}_${border}_patched PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_freetype_outline_border_${module}_${border} - guest_freetype_outline_border_${module}_${border}_patched PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_freetype_outline_border_${module}_${border} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_outline_border_${module}_${border}_patched PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") endif() endforeach() endforeach() diff --git a/core/libs/tests/GuestFreeTypeOutlineBorder.cpp b/core/libs/tests/GuestFreeTypeOutlineBorder.cpp index 609ec204f9..069142dbb8 100644 --- a/core/libs/tests/GuestFreeTypeOutlineBorder.cpp +++ b/core/libs/tests/GuestFreeTypeOutlineBorder.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -244,6 +245,16 @@ int main(int argc, char** argv) { #ifdef _WIN32 const auto library = LoadLibraryA(argv[1]); Require(library != nullptr, "load requested library"); + const auto checkPath = [](HMODULE module, const std::filesystem::path& expected) { + Require(module != nullptr, "load matching runtime module"); + std::array path{}; + const auto length = GetModuleFileNameW(module, path.data(), static_cast(path.size())); + Require(length != 0 && length < path.size(), "read loaded module path"); + Require(std::filesystem::equivalent(expected, std::filesystem::path(path.data())), "loaded module variant path"); + }; + const auto requested = std::filesystem::absolute(argv[1]); + checkPath(library, requested); + checkPath(GetModuleHandleW(L"libc.prx"), requested.parent_path() / "libc.prx"); auto query = reinterpret_cast(GetProcAddress(library, symbol)); #else const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); From a385eee97cf430665f8b27a5a611d418c5350aa9 Mon Sep 17 00:00:00 2001 From: vaibhavindalia Date: Tue, 20 Oct 2026 13:17:42 +0530 Subject: [PATCH 61/63] fix(libkernel_sys): validate descriptor first in fchown --- core/libs/prx/libkernel_sys/Export.cpp | 4 +--- core/libs/tests/GuestKernelSys.cpp | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 4aa5f49a80..60e3d831b8 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -725,15 +725,13 @@ int APS5_VABI lchmod_nid_postfix(const char* path, std::uint16_t mode) { } int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid) { - constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; - if (uid != 0 && uid != Unchanged) NotImplemented_nid_no_patch("fchown: an owner other than the guest user"); - if (gid != 0 && gid != Unchanged) NotImplemented_nid_no_patch("fchown: a group other than the guest group"); #ifdef _WIN32 if (GuestFstat(fd) != 0) return static_cast(Fail(GuestEbadf)); #else struct stat status{}; if (::fstat(fd, &status) != 0) return static_cast(Fail(GuestEbadf)); #endif + RequireGuestOwner("fchown", uid, gid); return 0; } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 850fc67fcb..99ffc5a124 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -1107,7 +1107,12 @@ int main() { #endif Require(fchown_nid_postfix(base, 0, 0) == 0); Require(fchown_nid_postfix(base, 0xFFFFFFFFu, 0xFFFFFFFFu) == 0); + Require(fchown_nid_postfix(base, 0, 0xFFFFFFFFu) == 0); + Require(fchown_nid_postfix(base, 0xFFFFFFFFu, 0) == 0); Require(fchown_nid_postfix(-1, 0, 0) == -1 && Error() == 9); + Require(fchown_nid_postfix(-1, 1, 0) == -1 && Error() == 9); + Require(fchown_nid_postfix(-1, 0, 1) == -1 && Error() == 9); + Require(fchown_nid_postfix(-1, 1, 1) == -1 && Error() == 9); bool fchownThrew = false; try { fchown_nid_postfix(base, 1, 0); } catch (const std::exception&) { fchownThrew = true; } Require(fchownThrew); From 5d347949b02ba16dd84e4109e321bb83da69a505 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 23 Oct 2026 18:24:43 -0400 Subject: [PATCH 62/63] fix(libc): accept signed char values in the ctype functions as titles' libc.prx does The ctype functions threw std::invalid_argument for any value below EOF, so a title passing a plain char with its high bit set, such as a UTF-8 byte, aborted. The libc.prx bundled with titles accepts -128 to -2: each class function returns 0 for them, as for 128 to 255 in the C locale, and toupper and tolower return them unchanged. Values outside -128 to 255 still throw. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/LocaleSupport.cpp | 6 +++--- core/libs/tests/GuestCharacter.cpp | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index db06ec8eaa..7173c10cbf 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -329,18 +329,18 @@ constexpr auto g_lowerTable = MakeCaseTable(false); constexpr auto g_upperTable = MakeCaseTable(true); void ValidateCharacter(int value) { - if (value != EOF && (value < 0 || value > UCHAR_MAX)) throw std::invalid_argument("Invalid character value"); + if (value < SCHAR_MIN || value > UCHAR_MAX) throw std::invalid_argument("Invalid character value"); } int ClassifyCharacter(int value, short mask) { ValidateCharacter(value); if (value == EOF) return 0; - return g_classificationTable[static_cast(value) + 1] & mask; + return g_classificationTable[static_cast(value < 0 ? value + UCHAR_MAX + 1 : value) + 1] & mask; } int ConvertCharacter(int value, bool upper) { ValidateCharacter(value); - if (value == EOF) return EOF; + if (value < 0) return value; const auto& facet = std::use_facet>(std::locale::classic()); const auto character = static_cast(value); return static_cast(upper ? facet.toupper(character) : facet.tolower(character)); diff --git a/core/libs/tests/GuestCharacter.cpp b/core/libs/tests/GuestCharacter.cpp index 211b36ccfb..7f900acea7 100644 --- a/core/libs/tests/GuestCharacter.cpp +++ b/core/libs/tests/GuestCharacter.cpp @@ -37,8 +37,18 @@ static void CheckConsoleClassValues() { Require(isprint_nid_postfix(c) == (bits & 0x3e) && isgraph_nid_postfix(c) == (bits & 0x3a) && ispunct_nid_postfix(c) == (bits & 0x08)); } } +static void CheckSignedCharValues() { + for (int c = -128; c <= -2; ++c) { + Require(isupper_nid_postfix(c) == 0 && islower_nid_postfix(c) == 0 && isalpha_nid_postfix(c) == 0 && isdigit_nid_postfix(c) == 0); + Require(isalnum_nid_postfix(c) == 0 && isspace_nid_postfix(c) == 0 && isblank_nid_postfix(c) == 0 && iscntrl_nid_postfix(c) == 0); + Require(isprint_nid_postfix(c) == 0 && isgraph_nid_postfix(c) == 0 && ispunct_nid_postfix(c) == 0 && isxdigit_nid_postfix(c) == 0); + Require(toupper_nid_postfix(c) == c && tolower_nid_postfix(c) == c); + } +} + int main() { CheckConsoleClassValues(); + CheckSignedCharValues(); Require(std::setlocale(LC_CTYPE, "C") != nullptr); for (int category = 0; category <= 6; ++category) { const char* result = setlocale_nid_postfix(category, "C"); From f91f0450b5fec2067ce35019e53438d65d5c9b21 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 23 Oct 2026 15:25:05 -0400 Subject: [PATCH 63/63] fix(libc): quiet signalling NaNs in trunc, ceil, floor and nearbyint as titles' libc.prx does trunc, ceil, floor and nearbyint and their float forms returned the host's result, which on Windows passes a signalling NaN through unchanged. The titles' copies follow FreeBSD msun, which returns x + x for a NaN, so the NaN comes back quieted with its payload and sign; these functions now do the same before calling the host for other arguments. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 16 ++++++++-------- core/libs/tests/GuestMath.cpp | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index d88fc6d00b..b6f00eabb3 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -416,9 +416,9 @@ int APS5_VABI __isfinitef_nid_postfix(float x) { return std::isfinite(x) ? 1 : 0 int APS5_VABI __isnormal_nid_postfix(double x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isnormalf_nid_postfix(float x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isinff_nid_postfix(float x) { return std::isinf(x) ? 1 : 0; } -float APS5_VABI nearbyintf_nid_postfix(float x) { return std::nearbyintf(x); } +float APS5_VABI nearbyintf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::nearbyintf(x); } long double APS5_VABI nearbyintl_nid_postfix(long double x) { return std::nearbyint(x); } -double APS5_VABI nearbyint_nid_postfix(double x) { return std::nearbyint(x); } +double APS5_VABI nearbyint_nid_postfix(double x) { return std::isnan(x) ? x + x : std::nearbyint(x); } double APS5_VABI cbrt_nid_postfix(double x) { return std::cbrt(x); } double APS5_VABI asin_nid_postfix(double x) { @@ -441,12 +441,12 @@ float APS5_VABI copysignf_nid_postfix(float x, float y) { return std::copysign(x, y); } int APS5_VABI abs_nid_postfix(int x) { return x < 0 ? static_cast(0u - static_cast(x)) : x; } -double APS5_VABI trunc_nid_postfix(double x) { return std::trunc(x); } -double APS5_VABI ceil_nid_postfix(double x) { return std::ceil(x); } -double APS5_VABI floor_nid_postfix(double x) { return std::floor(x); } -float APS5_VABI truncf_nid_postfix(float x) { return std::trunc(x); } -float APS5_VABI ceilf_nid_postfix(float x) { return std::ceil(x); } -float APS5_VABI floorf_nid_postfix(float x) { return std::floor(x); } +double APS5_VABI trunc_nid_postfix(double x) { return std::isnan(x) ? x + x : std::trunc(x); } +double APS5_VABI ceil_nid_postfix(double x) { return std::isnan(x) ? x + x : std::ceil(x); } +double APS5_VABI floor_nid_postfix(double x) { return std::isnan(x) ? x + x : std::floor(x); } +float APS5_VABI truncf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::trunc(x); } +float APS5_VABI ceilf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::ceil(x); } +float APS5_VABI floorf_nid_postfix(float x) { return std::isnan(x) ? x + x : std::floor(x); } double APS5_VABI fma_nid_postfix(double x, double y, double z) { return std::fma(x, y, z); } float APS5_VABI fmaf_nid_postfix(float x, float y, float z) { return std::fma(x, y, z); } double APS5_VABI fmin_nid_postfix(double x, double y) { return GuestFmin(x, y); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 8297d06458..a715ca26e8 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -630,6 +630,26 @@ static void CheckLog1pConsoleBits() { } } +static void CheckRoundingNaNConsoleBits() { + constexpr std::uint64_t cases[][2] = { + {0x7ff0000000000001ull, 0x7ff8000000000001ull}, {0xfff4000000000123ull, 0xfffc000000000123ull}, {0x7ff8000000000456ull, 0x7ff8000000000456ull}, + {0x7ff0000000000000ull, 0x7ff0000000000000ull}, {0xfff0000000000000ull, 0xfff0000000000000ull}, + }; + constexpr std::uint32_t floatCases[][2] = { + {0x7f800001u, 0x7fc00001u}, {0xffa00123u, 0xffe00123u}, {0x7fc00456u, 0x7fc00456u}, {0x7f800000u, 0x7f800000u}, {0xff800000u, 0xff800000u}, + }; + for (const auto function : {trunc_nid_postfix, ceil_nid_postfix, floor_nid_postfix, nearbyint_nid_postfix}) { + for (const auto& [input, expected] : cases) { + Require(std::bit_cast(function(std::bit_cast(input))) == expected); + } + } + for (const auto function : {truncf_nid_postfix, ceilf_nid_postfix, floorf_nid_postfix, nearbyintf_nid_postfix}) { + for (const auto& [input, expected] : floatCases) { + Require(std::bit_cast(function(std::bit_cast(input))) == expected); + } + } +} + static void CheckAsinl() { const struct { std::uint64_t inputMantissa; std::uint16_t inputSignExponent; std::uint64_t outputMantissa; std::uint16_t outputSignExponent; } cases[] = { {0, 0x0000, 0, 0x0000}, {0, 0x8000, 0, 0x8000}, @@ -1722,6 +1742,7 @@ int main() { CheckExpm1ConsoleBits(); CheckLog1pConsoleBits(); CheckNextafterConsoleBits(); + CheckRoundingNaNConsoleBits(); CheckAsinl(); Require(scalbn_nid_postfix(0.75, -2) == 0.1875); Require(scalbnf_nid_postfix(0.75f, 4) == 12.f);