diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f02464e06..547ca02381 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,6 +56,7 @@ jobs: relinker: ${{ steps.gate.outputs.relinker }} register-context: ${{ steps.gate.outputs.register-context }} write-watch: ${{ steps.gate.outputs.write-watch }} + mingw-host-tests: ${{ steps.gate.outputs.mingw-host-tests }} full: ${{ steps.gate.outputs.full }} relinker-hosts: ${{ steps.gate.outputs.relinker-hosts }} steps: @@ -138,6 +139,10 @@ jobs: registerContext: edits('register-context') || touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc), writeWatch: edits('write-watch') || touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc), + mingwHostTests: edits('eligibility') || edits('windows') || touches( + 'core/libs/', 'tools/build_mingw_host_only_tests.py', + 'tools/tests/test_mingw_host_only_test_targets.py', 'CMakeLists.txt' + ), }; }; const relinkerHosts = (full, linux) => JSON.stringify([ @@ -172,6 +177,7 @@ jobs: core.setOutput('relinker', run.relinker); core.setOutput('register-context', run.registerContext); core.setOutput('write-watch', run.writeWatch); + core.setOutput('mingw-host-tests', false); core.setOutput('full', true); core.setOutput('relinker-hosts', relinkerHosts(true, true)); await core.summary.addRaw( @@ -261,6 +267,7 @@ jobs: core.setOutput('relinker', relinker); core.setOutput('register-context', registerContext); core.setOutput('write-watch', writeWatch); + core.setOutput('mingw-host-tests', !manual && run.mingwHostTests); core.setOutput('full', full); core.setOutput('relinker-hosts', relinkerHosts(full, linux)); const merged = pulls.map((pull) => `PR #${pull.number} at ${pull.head} merged into main at ${main.commit.sha}`); @@ -526,7 +533,7 @@ jobs: env: BASE: ${{ needs.eligibility.outputs.sha }} run: | - git diff --name-only "$BASE" HEAD > changed.txt + git diff --name-only -z "$BASE" HEAD -- > changed.txt python3 tools/select_tests.py select build test-graph.json changed.txt - id: build run: | @@ -561,7 +568,7 @@ jobs: windows: needs: eligibility - if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }} + if: ${{ ((github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true') || (github.event_name == 'pull_request' && needs.eligibility.outputs.mingw-host-tests == 'true') }} runs-on: windows-latest timeout-minutes: 45 env: @@ -646,6 +653,9 @@ jobs: with: path: configure-seed.cmake key: configure-seed-${{ env.WINLIBS_ZIP }}-${{ steps.submodules.outputs.key }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'core/libs/CMakeLists.txt', 'tools/configure_seed.py', '.github/workflows/build.yml') }} + - if: github.event_name == 'pull_request' + shell: bash + run: mkdir -p build/.cmake/api/v1/query && touch build/.cmake/api/v1/query/codemodel-v2 - shell: cmd run: if exist configure-seed.cmake (cmake -C configure-seed.cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") else (cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") - if: steps.seed-cache.outputs.cache-hit != 'true' @@ -678,17 +688,30 @@ jobs: BASE: ${{ needs.eligibility.outputs.sha }} shell: cmd run: | - git diff --name-only %BASE% HEAD > changed.txt + git diff --name-only -z %BASE% HEAD -- > changed.txt python tools/select_tests.py select build test-graph.json changed.txt - id: build + if: github.event_name != 'pull_request' shell: cmd run: if exist build\select.ninja (ninja -C build -f select.ninja selected) else (cmake --build build) + - name: List changed files for host-only test linking + if: github.event_name == 'pull_request' + env: + BASE: ${{ needs.eligibility.outputs.sha }} + run: git diff --name-only "$BASE" HEAD > changed.txt + - name: Link changed host-only core/libs test executables + if: github.event_name == 'pull_request' + shell: cmd + run: python tools/build_mingw_host_only_tests.py build changed.txt - id: libs + if: github.event_name != 'pull_request' shell: cmd run: cmake --build build --target libs - - shell: cmd + - if: github.event_name != 'pull_request' + shell: cmd run: python tools/select_tests.py ctest build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml - - run: python tools/check_skips.py build/ctest.xml --job windows + - if: github.event_name != 'pull_request' + run: python tools/check_skips.py build/ctest.xml --job windows - name: Write the test graph of main if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }} continue-on-error: true diff --git a/3rdparty/vorbis b/3rdparty/vorbis index 0657aee69d..c2aa86b05e 160000 --- a/3rdparty/vorbis +++ b/3rdparty/vorbis @@ -1 +1 @@ -Subproject commit 0657aee69dec8508a0011f47f3b69d7538e9d262 +Subproject commit c2aa86b05e981c96bf381fc6aa11cdd03eccc2fb diff --git a/CMakeLists.txt b/CMakeLists.txt index a9b92c348b..7db1bb7432 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,8 @@ if(BUILD_TESTING) add_test(NAME pad_command_file COMMAND pad_command_file_tests) set_tests_properties(pad_command_file PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) + add_test(NAME mingw_host_only_test_targets COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_mingw_host_only_test_targets.py) add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ShaderCacheVersion.cmake ${CMAKE_COMMAND}) @@ -294,6 +296,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) add_test(NAME symbolize_samples COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_symbolize_samples.py) + add_test(NAME ab_split COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_ab_split.py) + set_tests_properties(ab_split PROPERTIES TIMEOUT 20) add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py) set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index ef0732d727..b1de7b4e4a 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -258,6 +258,8 @@ endif() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) +include(prx/libSceLibreSsl/ProtocolSelection.cmake) + if(BUILD_TESTING) add_test_executable(guest_posix_scalar_io_tests tests/GuestPosixScalarIo.cpp) target_include_directories(guest_posix_scalar_io_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) @@ -1097,3 +1099,5 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +include(prx/libSceFreeType/CurveBounds.cmake) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 0eb23cef6d..2290a2a3b3 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1432,7 +1432,9 @@ struct NetEpollEvent { struct NetCtlNatInfo { std::uint32_t size; - std::uint8_t opaque[12]; + std::int32_t stun_status; + std::int32_t nat_type; + std::uint32_t mapped_addr; }; static_assert(sizeof(NetCtlNatInfo) == 16); diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0924d1dd2a..d2ab24a9a8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -327,6 +327,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineLibrary.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineJournal.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/RuntimeSwitches.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DeferredDestruction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PlaceholderImages.cpp @@ -535,6 +536,18 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(agc_runtime_switches_tests tests/RuntimeSwitches.cpp Graphics/src/RuntimeSwitches.cpp) + target_include_directories(agc_runtime_switches_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_runtime_switches_tests PRIVATE libc) + configure_windows_unwind(agc_runtime_switches_tests) + add_test(NAME agc_runtime_switches COMMAND agc_runtime_switches_tests) + add_test(NAME agc_runtime_switches_refused COMMAND agc_runtime_switches_tests refused) + set_tests_properties(agc_runtime_switches agc_runtime_switches_refused PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_runtime_switches agc_runtime_switches_refused PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(agc_async_log_tests tests/AsyncLog.cpp) target_include_directories(agc_async_log_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_async_log_tests PRIVATE libc) @@ -3541,7 +3554,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() - if(WIN32) + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(agc_driver_frame_capture_eop_tests tests/FrameCaptureEop.cpp Execution/src/FrameCapture.cpp) target_include_directories(agc_driver_frame_capture_eop_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_driver_frame_capture_eop_tests PRIVATE libSceAgcDriver libkernel libc libzstd_static) @@ -3551,8 +3564,10 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_frame_capture_eop PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FRAME_CAPTURE=${CMAKE_CURRENT_BINARY_DIR}/frame-capture-eop;APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=3" TIMEOUT 120) - set_tests_properties(agc_driver_frame_capture_eop PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + if(WIN32) + set_tests_properties(agc_driver_frame_capture_eop PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() add_test_executable(agc_driver_frame_replay_state_tests tests/FrameReplayState.cpp Execution/src/FrameCapture.cpp) @@ -3561,9 +3576,15 @@ if(BUILD_TESTING) set_target_properties(agc_driver_frame_replay_state_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_frame_replay_state_tests) set(agcFrameReplayModes order tokens blocking) - if(WIN32) + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") list(APPEND agcFrameReplayModes zero shader shader_host) endif() + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + list(APPEND agcFrameReplayModes unwatched) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + list(APPEND agcFrameReplayModes shared_page host_neighbors) + endif() foreach(mode IN LISTS agcFrameReplayModes) set(capture "${CMAKE_CURRENT_BINARY_DIR}/frame-replay-${mode}") add_test(NAME agc_driver_frame_replay_${mode} COMMAND agc_driver_frame_replay_state_tests ${mode} "${capture}") @@ -3572,13 +3593,17 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_frame_replay_${mode} PROPERTIES ENVIRONMENT "APS5_FRAME_CAPTURE=${capture};APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=1") endif() + if(mode STREQUAL "unwatched") + set_tests_properties(agc_driver_frame_replay_${mode} PROPERTIES ENVIRONMENT + "APS5_NO_WRITE_WATCH=1;APS5_FRAME_CAPTURE=${capture};APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=1") + endif() if(WIN32) set_tests_properties(agc_driver_frame_replay_${mode} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endforeach() - if(WIN32) + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(agc_driver_frame_replay_tests tests/FrameReplay.cpp Execution/src/FrameCapture.cpp) target_include_directories(agc_driver_frame_replay_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_driver_frame_replay_tests PRIVATE libSceAgc libSceAgcDriver libkernel libc libzstd_static) @@ -3589,14 +3614,18 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_frame_replay PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FRAME_CAPTURE=${CMAKE_CURRENT_BINARY_DIR}/frame-replay-capture;APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=2" TIMEOUT 120) - set_tests_properties(agc_driver_frame_replay PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + if(WIN32) + set_tests_properties(agc_driver_frame_replay PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_frame_replay_concurrent COMMAND agc_driver_frame_replay_tests $ concurrent) set_tests_properties(agc_driver_frame_replay_concurrent PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FRAME_CAPTURE=${CMAKE_CURRENT_BINARY_DIR}/frame-replay-concurrent-capture;APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=8" TIMEOUT 120) - set_tests_properties(agc_driver_frame_replay_concurrent PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + if(WIN32) + set_tests_properties(agc_driver_frame_replay_concurrent PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 7e0d9e571c..0acd2860fa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" @@ -408,6 +409,16 @@ void Driver::execute(const Submission& submission) { ++flipsCounted; if (batchesAtFlip != 0) flipSerial = batchesAtFlip; flipBatchesUnsignaled += unsignaledAtFlip; + if (Graphics::AbConfigured()) { + static const auto abClock = std::chrono::steady_clock::now(); + std::vector abLines; + { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + abLines = Graphics::AbAtPresent(flipsCounted.load(), std::chrono::duration(std::chrono::steady_clock::now() - abClock).count()); + } + for (const auto& line : abLines) AgcDriver::ProfilePrint_nid_no_patch("%s", line.c_str()); + } timing.Mark("flip_flush"); timing.FinishPacket(submission.serial, cursor, 0xffffu, count); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp index f9ac5965b4..ea7b322287 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp @@ -8,6 +8,7 @@ #include "prx/libc/include/General.hpp" #include #include +#include #include #include #include @@ -26,6 +27,10 @@ #include extern char** environ; #endif +#ifdef __linux__ +#include "prx/libc/include/specifics/linux/HostMapping.hpp" +#include +#endif namespace AgcDriver { namespace { @@ -89,6 +94,39 @@ std::uint64_t flipValue(std::int64_t value) { return static_cast(value); } +#ifdef __linux__ +std::uint32_t windowsProtection(const HostMapping::Mapping& mapping) { + if (mapping.executable) return mapping.writable ? 0x40u : mapping.readable ? 0x20u : 0x10u; + if (mapping.writable) return 0x04u; + return mapping.readable ? 0x02u : 0x01u; +} + +bool pageReadable(std::uint64_t address) { + char byte = 0; + iovec local{&byte, 1}; + iovec remote{reinterpret_cast(static_cast(address)), 1}; + const auto read = process_vm_readv(getpid(), &local, 1, &remote, 1, 0); + if (read == 1) return true; + if (read < 0 && errno == EFAULT) return false; + throw std::runtime_error(std::string("frame capture: cannot probe a file mapping with process_vm_readv: ") + (read < 0 ? std::strerror(errno) : "nothing was read")); +} + +std::uint64_t fileBackedEnd(const HostMapping::Mapping& mapping) { + if (mapping.inode == 0 || !mapping.readable) return mapping.end; + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + if (pageReadable(mapping.end - page)) return mapping.end; + if (!pageReadable(mapping.start)) return mapping.start; + std::uint64_t readable = 0; + std::uint64_t unreadable = (mapping.end - mapping.start) / page - 1; + while (unreadable - readable > 1) { + const auto middle = readable + (unreadable - readable) / 2; + if (pageReadable(mapping.start + middle * page)) readable = middle; + else unreadable = middle; + } + return mapping.start + unreadable * page; +} +#endif + std::uint64_t contentHash(std::span bytes) { std::uint64_t hash = 0xcbf29ce484222325ull; for (const auto value : bytes) hash = (hash ^ static_cast(value)) * 0x100000001b3ull; @@ -479,6 +517,7 @@ void FrameRecorder::start() { } return; } + if (!GuestMemory::WriteWatched()) throw std::runtime_error("frame capture: the guest write watch is unavailable, so CPU stores during the frames cannot be recorded"); startedAt = std::chrono::steady_clock::now(); startBoundary = boundaries; allocationsGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); @@ -598,8 +637,46 @@ std::vector FrameRecorder::committedRanges() { } } return result; +#elif defined(__linux__) + std::uintptr_t base = 0; + std::size_t size = 0; + GuestArena::GuestArenaRange_nid_postfix(&base, &size); + if (size == 0) throw std::runtime_error("frame capture: the guest arena is not reserved"); + std::vector result; + for (const auto& allocation : GuestAllocations::GuestAllocationsAcquireAll_nid_postfix()) { + if (!allocation->readable) continue; + const auto first = allocation->address; + const auto end = first + allocation->bytes; + const bool host = first < base || end > base + size; + const auto access = FrameRangeAccess::Readable | (allocation->writable ? FrameRangeAccess::Writable : 0u) | (allocation->gpu ? FrameRangeAccess::Gpu : 0u); + HostMapping::Walk(first, end, "frame capture", [&](const HostMapping::Mapping& mapping) { + if (!mapping.readable && !mapping.writable && !mapping.executable) return; + const auto add = [&](std::uint64_t address, std::uint64_t regionEnd, bool readable) { + if (address >= regionEnd) return; + Range range; + range.address = address; + range.bytes = regionEnd - address; + range.protection = windowsProtection(mapping); + range.access = access; + range.readable = readable; + if (host) range.kind = FrameRangeKind::Host; + else if (GuestMemory::Watched(range.address, static_cast(range.bytes))) range.kind = FrameRangeKind::Watched; + else if (!mapping.shared && mapping.inode == 0) range.kind = FrameRangeKind::Unwatched; + else range.kind = FrameRangeKind::Section; + auto& last = result.empty() ? range : result.back(); + if (!result.empty() && last.address + last.bytes == range.address && last.kind == range.kind && last.protection == range.protection && last.readable == range.readable && last.access == range.access) last.bytes += range.bytes; + else result.push_back(range); + }; + const auto address = std::max(first, mapping.start); + const auto regionEnd = std::min(end, mapping.end); + const auto backed = std::clamp(fileBackedEnd(mapping), address, regionEnd); + add(address, backed, mapping.readable); + add(backed, regionEnd, false); + }); + } + return result; #else - throw std::runtime_error("frame capture: guest memory capture is implemented on Windows only"); + throw std::runtime_error("frame capture: guest memory capture is implemented on Windows and Linux only"); #endif } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 87483bee96..4b94e1833c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -202,7 +202,7 @@ bool CaptureComputePipeline(std::size_t words) { using ResourceCache = Graphics::ResourceCache; std::uint32_t DumpScale(); -void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full, std::uint32_t scale); +void WriteFrameBmp(int index, std::uint32_t width, std::uint32_t height, std::span pixels); FrameDumpOutput frameDumpOutput; class FrameDumpWriter { @@ -216,7 +216,7 @@ public: if (worker.joinable()) worker.join(); } - void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, bool screenshot, std::vector pixels) { + void Enqueue(int index, std::uint32_t width, std::uint32_t height, bool screenshot, std::vector pixels) { std::unique_lock lock(mutex); require(!stopping, "frame dump writer has stopped"); if (!screenshot && !frameDumpOutput.Enabled_nid_no_patch()) return; @@ -225,7 +225,7 @@ public: require(!stopping, "frame dump writer has stopped"); if (!worker.joinable()) worker = std::thread([this] { run(); }); const auto bytes = pixels.size(); - pending.push_back({index, width, height, scale, screenshot, std::move(pixels)}); + pending.push_back({index, width, height, screenshot, std::move(pixels)}); queuedBytes += bytes; changed.notify_all(); } @@ -240,7 +240,6 @@ private: int index; std::uint32_t width; std::uint32_t height; - std::uint32_t scale; bool screenshot; std::vector pixels; }; @@ -257,7 +256,7 @@ private: } for (const auto& frame : batch) { if (frame.screenshot) std::fprintf(stderr, "[gpu] screenshot saved: %s\n", Screenshot::Write(GetAppTitleId_nid_postfix().value, frame.width, frame.height, frame.pixels).string().c_str()); - else WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale); + else WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels); std::lock_guard lock(mutex); queuedBytes -= frame.pixels.size(); changed.notify_all(); @@ -815,7 +814,7 @@ struct VulkanDevice::State { const auto index = slot.dumpIndex; if (slot.dumpScreenshot) { require(full.size() == static_cast(slot.dumpWidth) * slot.dumpHeight * 4, "invalid screenshot pixels"); - dumpWriter.Enqueue(index, slot.dumpWidth, slot.dumpHeight, 1, true, std::vector(full.begin(), full.end())); + dumpWriter.Enqueue(index, slot.dumpWidth, slot.dumpHeight, true, std::vector(full.begin(), full.end())); return; } const auto scale = slot.dumpScale; @@ -828,7 +827,7 @@ struct VulkanDevice::State { } static const auto start = std::chrono::steady_clock::now(); AgcDriver::ProfilePrint_nid_no_patch("[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration(std::chrono::steady_clock::now() - start).count()); - dumpWriter.Enqueue(index, width, height, 1, false, std::move(pixels)); + dumpWriter.Enqueue(index, width, height, false, std::move(pixels)); } // Blocks on the fence, or polls it every 50 us with APS5_PRESENT_POLL_FENCE=1 (the difference @@ -3265,16 +3264,20 @@ std::uint32_t DumpScale() { return scale; } -// frame_.bmp: every DumpScale()-th pixel of a BGRA8 image, 32-bit top-down. -void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full, std::uint32_t scale) { - require(scale != 0 && full.size() == static_cast(fullWidth) * fullHeight * 4, "invalid frame dump pixels"); - if (!frameDumpOutput.Enabled_nid_no_patch()) return; +void QueueFrameDump(FrameDumpWriter& writer, int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full) { + const auto scale = DumpScale(); + require(full.size() == static_cast(fullWidth) * fullHeight * 4, "invalid frame dump pixels"); const std::uint32_t width = (fullWidth + scale - 1) / scale; const std::uint32_t height = (fullHeight + scale - 1) / scale; std::vector pixels(static_cast(width) * height * 4); for (std::uint32_t y = 0; y < height; ++y) { for (std::uint32_t x = 0; x < width; ++x) std::memcpy(pixels.data() + (static_cast(y) * width + x) * 4, full.data() + (static_cast(y) * scale * fullWidth + x * scale) * 4, 4); } + writer.Enqueue(index, width, height, false, std::move(pixels)); +} + +void WriteFrameBmp(int index, std::uint32_t width, std::uint32_t height, std::span pixels) { + if (!frameDumpOutput.Enabled_nid_no_patch()) return; char name[32]; std::snprintf(name, sizeof(name), "frame_%03d.bmp", index); frameDumpOutput.Write_nid_no_patch(name, width, height, pixels); @@ -3450,24 +3453,25 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer, const Displ if (passthrough) ensureHdrScaler(DisplayTexelFormat(buffer.pixelFormat)); const auto pixels = passthrough ? ReadDisplayBufferTexels(buffer) : ReadDisplayBuffer(buffer); bool dumpFrame = false; + std::optional cpuDump; if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { - if (passthrough) { - WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, ReadDisplayBuffer(buffer), DumpScale()); - } else if (dumps.cpu) { - WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, pixels, DumpScale()); + if (passthrough || dumps.cpu) { + cpuDump = dumps.dumped++; } else { state->nextDumpIndex = dumps.dumped++; dumpFrame = true; } } const bool screenshot = takeScreenshot(dumpFrame); + if (cpuDump && passthrough) QueueFrameDump(state->dumpWriter, *cpuDump, buffer.width, buffer.height, ReadDisplayBuffer(buffer)); + else if (cpuDump) QueueFrameDump(state->dumpWriter, *cpuDump, buffer.width, buffer.height, pixels); auto shot = screenshot && passthrough ? ReadDisplayBuffer(buffer) : std::vector{}; if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame || (screenshot && !passthrough), false, nullptr, 0, passthrough, plane)) { if (dumpFrame) --dumps.dumped; if (screenshot) Screenshot::Request(); return false; } - if (!shot.empty()) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot)); + if (!shot.empty()) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, true, std::move(shot)); return true; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; @@ -3500,9 +3504,10 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer, const Displ } const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt; bool dumpFrame = false; + std::optional cpuDump; if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu || (passthrough && resident == nullptr)) { - WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, DisplayPixels(buffer, cleared), DumpScale()); + cpuDump = dumps.dumped++; } else { state->nextDumpIndex = dumps.dumped++; dumpFrame = true; @@ -3514,6 +3519,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer, const Displ AgcDriver::ProfilePrint_nid_no_patch("[flip] (cumulative): %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast(residentPresents), static_cast(refreshedPresents), static_cast(notPending), static_cast(unsuitable), static_cast(gpuDumps)); } const bool screenshot = takeScreenshot(dumpFrame); + if (cpuDump) QueueFrameDump(state->dumpWriter, *cpuDump, buffer.width, buffer.height, DisplayPixels(buffer, cleared)); const bool cpuScreenshot = screenshot && passthrough && resident == nullptr; auto shot = cpuScreenshot ? DisplayPixels(buffer, cleared) : std::vector{}; CaptureTrace::Log("present dump=%d address=%llx width=%u height=%u resident=%d generation=%llu", dumpFrame ? state->nextDumpIndex : -1, static_cast(buffer.address), buffer.width, buffer.height, resident != nullptr, static_cast(resident ? resident->Generation() : 0)); @@ -3531,7 +3537,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer, const Displ if (screenshot) Screenshot::Request(); return false; } - if (cpuScreenshot) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot)); + if (cpuScreenshot) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, true, std::move(shot)); return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 04f7e5ccbd..386aa4c94c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -72,7 +72,7 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr& owner, VkImage* image = nullptr); void KeepMultisampleTarget_nid_no_patch(const std::shared_ptr& owner); void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target); -VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource); +VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource, bool uintStorage = false); bool MultisampleTargetStorable(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index b9cea62862..d246e5b4f1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -146,6 +146,7 @@ bool HasStencil(VkFormat format); // Device limit checks of the viewport, which is dynamic state and so no longer checked by Pipeline. void ValidateViewport(const Context& context, const VkViewport& viewport); void ValidateDepthBounds(const Context& context, const State& state); +void ValidateMeshLimits(const Context& context, const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t invocations); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); bool BarycentricPatches(const State& state, std::span shaders); bool ValidateSampleLocations(const Context& context, const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp new file mode 100644 index 0000000000..199ea68922 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp @@ -0,0 +1,67 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RUNTIMESWITCHES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RUNTIMESWITCHES_HPP + +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +enum class RuntimeSwitch : std::size_t { + NoBarrierBatch, + Count, +}; + +namespace RuntimeSwitches { + +const char* Name(RuntimeSwitch id); +bool Set(RuntimeSwitch id); +void Assign(RuntimeSwitch id, bool set); +std::optional Find(std::string_view name); + +} + +class AbSchedule { +public: + struct Config { + double phaseSeconds = 0; + double settleSeconds = 2; + std::uint64_t startPresent = 0; + std::optional startSeconds; + std::vector switches; + }; + + enum class Event { None, Begin, Settled }; + + struct Step { + Event event = Event::None; + std::uint64_t phase = 0; + bool b = false; + }; + + static Config Parse(std::string_view ab, std::string_view start, std::string_view settle); + + explicit AbSchedule(Config config); + + Step AtPresent(std::uint64_t present, double seconds); + + const Config& Settings() const { return config; } + +private: + Config config; + bool started = false; + bool settled = false; + std::uint64_t phase = 0; + double phaseStart = 0; + std::optional firstSeconds; +}; + +bool AbConfigured(); +std::vector AbAtPresent(std::uint64_t present, double seconds); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 00d812afd4..a65300d107 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -599,14 +599,6 @@ private: std::vector descriptorBuffers; std::vector descriptorImages; std::vector descriptorWrites; - struct MovedWordBuffer { - std::size_t allocation; - std::vector words; - std::size_t size; - std::shared_ptr buffer; - }; - mutable std::vector movedWordBuffers; - mutable std::size_t movedWordBufferNext = 0; mutable std::vector drawPoolSizes; // Guest buffer elements bound read-only: each use of this object skips that many pending-write // notes (counted in MarkGpuWrites for the [buffers] line). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 66f2c886a8..adb46e8112 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -185,7 +185,7 @@ public: // so successive mip writes of a chain share one image and one write-back. VkImageView View(std::uint32_t mip); VkImageView FirstLayerView(std::uint32_t mip); - VkImageView StorageView(std::uint32_t mip, bool firstLayer); + VkImageView StorageView(std::uint32_t mip, bool firstLayer, bool uintStorage = false); VkImageView ElementView(); VkImageView AtomicView(std::uint32_t mip, bool firstLayer); VkImageView Atomic64View(std::uint32_t mip, bool firstLayer); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index b1f8929f22..00510dabcd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4190,13 +4190,17 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& for (const auto& mapped : ranges) { const auto range = mapped.lock(); if (range == nullptr || range->bytes <= 0x10000) continue; - if (current.empty()) current = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + if (current.empty()) current = GuestAllocations::GuestAllocationsAcquireMapped_nid_no_patch(ranges); if (leasedRangeOwner(current, range->address) != range) continue; try { + const auto run = importRun(current, range->address, std::numeric_limits::max(), effectiveBudget(context, state)); + const bool replacing = wouldRetireImport(state, run); if (!GuestMemory::GpuMutex().HeldByThisThread()) { - const auto run = importRun(current, range->address, std::numeric_limits::max(), effectiveBudget(context, state)); const std::array, 1> span{{{run.base, run.base + run.bytes}}}; - if (wouldRetireImport(state, run) || StorageTexture::AnyPendingOverlaps(span)) return false; + if (replacing || StorageTexture::AnyPendingOverlaps(span)) return false; + } else if (replacing) { + current = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + if (leasedRangeOwner(current, range->address) != range) continue; } if (!state.importedAtMap.emplace(range->address, range->bytes).second && findImport(state, range->address, range->address + range->bytes) == nullptr) continue; importAllocation(context, state, range->address, current, ImportRequester::Map, 0, false); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 7cb1eafdad..99cc95be63 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -33,6 +33,23 @@ namespace AgcDriver::Graphics { namespace { +VkFormat uintStorageViewFormat(VkFormat format) { + switch (format) { + case VK_FORMAT_R8G8_SINT: return VK_FORMAT_R8G8_UINT; + case VK_FORMAT_R8G8B8A8_SINT: return VK_FORMAT_R8G8B8A8_UINT; + case VK_FORMAT_R16_SINT: + case VK_FORMAT_R16_SFLOAT: return VK_FORMAT_R16_UINT; + case VK_FORMAT_R16G16_SINT: + case VK_FORMAT_R16G16_SFLOAT: return VK_FORMAT_R16G16_UINT; + case VK_FORMAT_R16G16B16A16_SINT: + case VK_FORMAT_R16G16B16A16_SFLOAT: return VK_FORMAT_R16G16B16A16_UINT; + case VK_FORMAT_R32_SINT: return VK_FORMAT_R32_UINT; + case VK_FORMAT_R32G32_SINT: return VK_FORMAT_R32G32_UINT; + case VK_FORMAT_R32G32B32A32_SINT: return VK_FORMAT_R32G32B32A32_UINT; + default: return VK_FORMAT_UNDEFINED; + } +} + std::uint32_t MultisampledBlockLog2(ColorTileMode mode) { switch (mode) { case ColorTileMode::Standard4KB: return 12u; @@ -146,12 +163,19 @@ public: Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, orderFlags, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); storageFormat = HostStorageFormat(context, target.format); - storageFlags = storageFormat != target.format ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u; + const auto uintFormat = uintStorageViewFormat(storageFormat); + storageFlags = storageFormat != target.format || (uintFormat != VK_FORMAT_UNDEFINED && uintFormat != target.format) ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u; const auto storageSupports = [&](VkFormat format, VkImageUsageFlags formatUsage, VkImageCreateFlags flags) { VkImageFormatProperties properties{}; return context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, formatUsage, flags, &properties) == VK_SUCCESS && (properties.sampleCounts & samples) != 0; }; - if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags))) storageFormat = VK_FORMAT_UNDEFINED; + const auto uintStorageSupported = [&] { + if (uintFormat == VK_FORMAT_UNDEFINED) return true; + VkFormatProperties uintProperties{}; + context.formatProperties(context.physical, uintFormat, &uintProperties); + return (uintProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0 && storageSupports(uintFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u); + }; + if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags) || !uintStorageSupported())) storageFormat = VK_FORMAT_UNDEFINED; twinFormat = colorTwinFormat(context, target.format); if (twinFormat != VK_FORMAT_UNDEFINED) { VkFormatProperties twinProperties{}; @@ -355,7 +379,7 @@ public: }); } - VkImageView Storage(VkFormat format) { + VkImageView Storage(VkFormat format, bool uintStorage) { Require(storageFormat != VK_FORMAT_UNDEFINED, "the device cannot write the multisampled color target's format as a storage image at its sample count"); Require(format == storageFormat, "a storage image access to a multisampled color target in another format than the target's storage form is not implemented"); Require(cmask.Refusal().empty(), cmask.Refusal()); @@ -366,20 +390,28 @@ public: Keep(); makeStorable(*recording); ApplyFastClear(target); - if (storageView == VK_NULL_HANDLE) { + const auto uintFormat = uintStorage ? uintStorageViewFormat(storageFormat) : VK_FORMAT_UNDEFINED; + const bool useUintStorage = uintStorage && uintFormat != VK_FORMAT_UNDEFINED; + auto& selectedView = useUintStorage ? uintStorageView : storageView; + if (selectedView == VK_NULL_HANDLE) { VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - viewInfo.format = storageFormat; + viewInfo.format = useUintStorage ? uintFormat : storageFormat; viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &storageView), "vkCreateImageView multisampled target storage"); + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + if (useUintStorage) { + viewUsage.usage = VK_IMAGE_USAGE_STORAGE_BIT; + viewInfo.pNext = &viewUsage; + } + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &selectedView), "vkCreateImageView multisampled target storage"); } recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); }); storageWritten = true; - return storageView; + return selectedView; } bool Storable() const { @@ -528,6 +560,7 @@ private: VkImageView twinView = VK_NULL_HANDLE; std::vector retired; VkImageView storageView = VK_NULL_HANDLE; + VkImageView uintStorageView = VK_NULL_HANDLE; bool storageWritten = false; VkDeviceSize allocationBytes = 0; std::optional fmaskRange; @@ -717,6 +750,7 @@ private: if (orderView) context.Function("vkDestroyImageView")(context.device, orderView, nullptr); if (twinView) context.Function("vkDestroyImageView")(context.device, twinView, nullptr); if (storageView) context.Function("vkDestroyImageView")(context.device, storageView, nullptr); + if (uintStorageView) context.Function("vkDestroyImageView")(context.device, uintStorageView, nullptr); destroy({image, memory, view}); for (const auto& allocation : retired) destroy(allocation); } @@ -1297,7 +1331,7 @@ void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget acquire(context, target); } -VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource) { +VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource, bool uintStorage) { std::lock_guard lock(targetsMutex()); discardRetiredTargets(); const auto format = ResolveTextureFormat(resource.format); @@ -1308,13 +1342,13 @@ VkImageView MultisampleTargetStorageView(const Context& context, const GuestText const auto& rendered = target->target; if (sameStorageForm(context, rendered.format, format) && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) { if (!holdHostOnly(*target)) continue; - return target->Storage(StorageFormatForGuest(context, resource.format)); + return target->Storage(StorageFormatForGuest(context, resource.format), uintStorage); } other = target.get(); } if (other == nullptr) { Require(Recorder::Active() != nullptr, "a multisampled color target is only written by recorded dispatches"); - return createFilled(context, resource, format, "writing", true).Storage(StorageFormatForGuest(context, resource.format)); + return createFilled(context, resource, format, "writing", true).Storage(StorageFormatForGuest(context, resource.format), uintStorage); } char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: storage image access to multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 5a7489ecf8..353e5a7d1e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -251,6 +251,15 @@ bool ValidateSampleLocations(const Context& context, const State& state) { return enabled; } +void ValidateMeshLimits(const Context& context, const ShaderRecompiler::MeshConfiguration& mesh, std::uint32_t invocations) { + const auto& limits = context.meshLimits; + Require(invocations <= limits.maxMeshWorkGroupInvocations && invocations <= limits.maxMeshWorkGroupSize[0], "mesh workgroup of " + std::to_string(invocations) + " invocations exceeds device limits (" + std::to_string(limits.maxMeshWorkGroupInvocations) + " invocations, " + std::to_string(limits.maxMeshWorkGroupSize[0]) + " wide)"); + Require(mesh.maxVertices <= limits.maxMeshOutputVertices, "mesh output vertices " + std::to_string(mesh.maxVertices) + " exceed the device limit " + std::to_string(limits.maxMeshOutputVertices)); + Require(mesh.maxPrimitives <= limits.maxMeshOutputPrimitives, "mesh output primitives " + std::to_string(mesh.maxPrimitives) + " exceed the device limit " + std::to_string(limits.maxMeshOutputPrimitives)); + const auto sharedBytes = static_cast(mesh.ldsSizeDwords) * 4; + Require(sharedBytes <= limits.maxMeshSharedMemorySize, "mesh LDS " + std::to_string(sharedBytes) + " bytes exceeds the device shared memory limit " + std::to_string(limits.maxMeshSharedMemorySize)); +} + void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::isfinite(viewport.minDepth) && std::isfinite(viewport.maxDepth), "non-finite viewport depth range"); Require(context.depthRangeUnrestricted || (viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1), "viewport depth outside [0, 1] requires VK_EXT_depth_range_unrestricted"); @@ -479,8 +488,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Require(context.meshShader, "device does not support VK_EXT_mesh_shader"); const auto& mesh = *state.stages.mesh; const auto invocations = state.stages.vertexWaveSize == 64u && context.subgroup.subgroupSize == 32u ? mesh.threadsPerGroup / 2u : mesh.threadsPerGroup; - Require(invocations <= context.meshLimits.maxMeshWorkGroupInvocations && invocations <= context.meshLimits.maxMeshWorkGroupSize[0], "mesh workgroup exceeds device limits"); - Require(mesh.maxVertices <= context.meshLimits.maxMeshOutputVertices && mesh.maxPrimitives <= context.meshLimits.maxMeshOutputPrimitives && static_cast(mesh.ldsSizeDwords) * 4 <= context.meshLimits.maxMeshSharedMemorySize, "mesh output or LDS exceeds device limits"); + ValidateMeshLimits(context, mesh, invocations); } const auto pushStages = PushConstantStages(shaders); Require(pushStages == 0 || context.limits.maxPushConstantsSize >= PushBlockBytes(dynamicRendering), "graphics push constant range exceeds device limit"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index c657013f21..04e22b2b1d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp" @@ -1930,8 +1931,7 @@ bool Recorder::MergeBarriers() { } bool Recorder::BarrierBatch() { - static const bool batch = MergeBarriers() && std::getenv("APS5_NO_BARRIER_BATCH") == nullptr; - return batch; + return MergeBarriers() && !RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch); } bool Recorder::ElideCovered(CommandClass which, const BarrierMask& wanted, VkAccessFlags covered) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp new file mode 100644 index 0000000000..ebc1eac247 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp @@ -0,0 +1,212 @@ +#include "prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +namespace { + +constexpr std::size_t SwitchCount = static_cast(RuntimeSwitch::Count); + +constexpr std::array SwitchNames{ + "APS5_NO_BARRIER_BATCH", +}; + +struct SwitchValues { + std::array, SwitchCount> set{}; + SwitchValues() { + const std::array environment{ + std::getenv("APS5_NO_BARRIER_BATCH") != nullptr, + }; + for (std::size_t index = 0; index < SwitchCount; ++index) set[index].store(environment[index], std::memory_order_relaxed); + } +}; + +SwitchValues& Values() { + static SwitchValues values; + return values; +} + +std::string_view Trim(std::string_view text) { + while (!text.empty() && (text.front() == ' ' || text.front() == '\t')) text.remove_prefix(1); + while (!text.empty() && (text.back() == ' ' || text.back() == '\t' || text.back() == '\r' || text.back() == '\n')) text.remove_suffix(1); + return text; +} + +double ParseSeconds(std::string_view text, const char* what) { + const std::string copy(Trim(text)); + char* end = nullptr; + const double value = std::strtod(copy.c_str(), &end); + if (copy.empty() || end != copy.c_str() + copy.size() || !std::isfinite(value) || value < 0) throw std::invalid_argument(std::string(what) + ": '" + copy + "' is not a number of seconds"); + return value; +} + +std::string FlippableList() { + std::string list; + for (const auto* name : SwitchNames) list += std::string(list.empty() ? "" : ", ") + name; + return list; +} + +} + +namespace RuntimeSwitches { + +const char* Name(RuntimeSwitch id) { + return SwitchNames.at(static_cast(id)); +} + +bool Set(RuntimeSwitch id) { + return Values().set.at(static_cast(id)).load(std::memory_order_relaxed); +} + +void Assign(RuntimeSwitch id, bool set) { + Values().set.at(static_cast(id)).store(set, std::memory_order_relaxed); +} + +std::optional Find(std::string_view name) { + for (std::size_t index = 0; index < SwitchCount; ++index) { + if (name == SwitchNames[index]) return static_cast(index); + } + return std::nullopt; +} + +} + +AbSchedule::Config AbSchedule::Parse(std::string_view ab, std::string_view start, std::string_view settle) { + Config config; + ab = Trim(ab); + const auto colon = ab.find(':'); + if (colon == std::string_view::npos) throw std::invalid_argument("APS5_AB: expected :[+...], got '" + std::string(ab) + "'"); + config.phaseSeconds = ParseSeconds(ab.substr(0, colon), "APS5_AB phase length"); + if (config.phaseSeconds < 1) throw std::invalid_argument("APS5_AB: a phase must last at least 1 s"); + auto list = ab.substr(colon + 1); + if (Trim(list).empty()) throw std::invalid_argument("APS5_AB: no switch listed after the phase length"); + for (;;) { + const auto plus = list.find('+'); + const auto name = Trim(list.substr(0, plus)); + if (name.empty()) throw std::invalid_argument("APS5_AB: an empty switch name in '" + std::string(ab) + "'"); + const auto found = RuntimeSwitches::Find(name); + if (!found.has_value()) throw std::invalid_argument("APS5_AB: " + std::string(name) + " cannot change while the title runs; switches the A/B can change: " + FlippableList()); + if (std::find(config.switches.begin(), config.switches.end(), *found) != config.switches.end()) throw std::invalid_argument("APS5_AB: " + std::string(name) + " is listed twice"); + config.switches.push_back(*found); + if (plus == std::string_view::npos) break; + list = list.substr(plus + 1); + } + start = Trim(start); + if (!start.empty()) { + if (start.back() == 's') { + config.startSeconds = ParseSeconds(start.substr(0, start.size() - 1), "APS5_AB_START"); + } else { + const std::string copy(start); + char* end = nullptr; + errno = 0; + const auto value = std::strtoull(copy.c_str(), &end, 10); + if (end != copy.c_str() + copy.size() || errno == ERANGE || copy.front() < '0' || copy.front() > '9') throw std::invalid_argument("APS5_AB_START: '" + copy + "' is neither a present index nor s"); + config.startPresent = value; + } + } + if (!Trim(settle).empty()) config.settleSeconds = ParseSeconds(settle, "APS5_AB_SETTLE"); + if (config.settleSeconds >= config.phaseSeconds) throw std::invalid_argument("APS5_AB_SETTLE must be shorter than the phase"); + return config; +} + +AbSchedule::AbSchedule(Config config) : config(std::move(config)) {} + +AbSchedule::Step AbSchedule::AtPresent(std::uint64_t present, double seconds) { + if (!firstSeconds.has_value()) firstSeconds = seconds; + if (!started) { + const bool due = config.startSeconds.has_value() ? seconds - *firstSeconds >= *config.startSeconds : present >= config.startPresent; + if (!due) return {}; + started = true; + settled = false; + phase = 0; + phaseStart = seconds; + return {Event::Begin, 0, false}; + } + if (seconds - phaseStart >= config.phaseSeconds) { + ++phase; + settled = false; + phaseStart = seconds; + return {Event::Begin, phase, (phase & 1u) != 0}; + } + if (!settled && seconds - phaseStart >= config.settleSeconds) { + settled = true; + return {Event::Settled, phase, (phase & 1u) != 0}; + } + return {}; +} + +namespace { + +struct AbState { + std::mutex mutex; + std::optional schedule; + std::array environment{}; +}; + +AbState& Ab() { + static AbState* state = [] { + auto* created = new AbState(); + const char* ab = std::getenv("APS5_AB"); + const char* start = std::getenv("APS5_AB_START"); + const char* settle = std::getenv("APS5_AB_SETTLE"); + if (ab != nullptr) { + created->schedule.emplace(AbSchedule::Parse(ab, start != nullptr ? start : "", settle != nullptr ? settle : "")); + for (std::size_t index = 0; index < SwitchCount; ++index) created->environment[index] = RuntimeSwitches::Set(static_cast(index)); + } + return created; + }(); + return *state; +} + +std::string SwitchSettings(const AbState& state, bool b) { + std::string text; + for (const auto id : state.schedule->Settings().switches) { + const bool set = state.environment[static_cast(id)] != b; + text += std::string(text.empty() ? "" : ", ") + RuntimeSwitches::Name(id) + (set ? " set" : " unset"); + } + return text; +} + +} + +bool AbConfigured() { + static const bool configured = std::getenv("APS5_AB") != nullptr; + return configured; +} + +std::vector AbAtPresent(std::uint64_t present, double seconds) { + if (!AbConfigured()) return {}; + auto& state = Ab(); + std::lock_guard lock(state.mutex); + std::vector lines; + char text[512]; + const auto& config = state.schedule->Settings(); + const auto step = state.schedule->AtPresent(present, seconds); + if (step.event == AbSchedule::Event::None) return lines; + if (step.event == AbSchedule::Event::Begin) { + if (step.phase == 0) { + char from[96]; + if (config.startSeconds.has_value()) std::snprintf(from, sizeof(from), "%.1f s after the first present", *config.startSeconds); + else std::snprintf(from, sizeof(from), "present %llu", static_cast(config.startPresent)); + std::snprintf(text, sizeof(text), "[ab] in-run A/B: phases of %.1f s from %s, settle %.1f s; A: %s; B: %s\n", config.phaseSeconds, from, config.settleSeconds, SwitchSettings(state, false).c_str(), SwitchSettings(state, true).c_str()); + lines.emplace_back(text); + } + for (const auto id : config.switches) RuntimeSwitches::Assign(id, state.environment[static_cast(id)] != step.b); + std::snprintf(text, sizeof(text), "[ab] phase %c %llu begins at present %llu (t %.3f s): %s\n", step.b ? 'B' : 'A', static_cast(step.phase), static_cast(present), seconds, SwitchSettings(state, step.b).c_str()); + } else { + std::snprintf(text, sizeof(text), "[ab] phase %c %llu settled at present %llu (t %.3f s)\n", step.b ? 'B' : 'A', static_cast(step.phase), static_cast(present), seconds); + } + lines.emplace_back(text); + return lines; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b56db85413..a57229370e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2455,7 +2455,7 @@ void ShaderResources::buildComplete() { for (auto element = first; element < last; ++element) { const auto index = binding.imageAllocations[element]; if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) images.push_back({samplers[index]->Handle(), VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED}); - else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); + else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index], binding.layout.binding >= ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 7u && binding.layout.binding < ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 14u), VK_IMAGE_LAYOUT_GENERAL}); else images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } if (recorded) runs.emplace_back(write.dstArrayElement, write.descriptorCount); @@ -2500,7 +2500,7 @@ void ShaderResources::buildComplete() { for (const auto index : binding.imageAllocations) { if (index == std::numeric_limits::max()) images.push_back(absentImage(binding.layout.binding)); else if (storageTextures[index] == nullptr) images.push_back({VK_NULL_HANDLE, multisampleStorageViews[index], VK_IMAGE_LAYOUT_GENERAL}); - else images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); + else images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index], binding.layout.binding >= ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 7u && binding.layout.binding < ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 14u), VK_IMAGE_LAYOUT_GENERAL}); } Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding"); break; @@ -4931,7 +4931,8 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind Require(!(element < binding.imageAtomic.size() && binding.imageAtomic[element]) && !(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]), "storage image atomics on a multisampled texture are not implemented"); storageTextures.push_back(nullptr); multisampleStorageViews.resize(storageTextures.size(), VK_NULL_HANDLE); - multisampleStorageViews.back() = MultisampleTargetStorageView(context, resource); + const bool uintStorage = binding.binding >= ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 7u && binding.binding < ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 14u; + multisampleStorageViews.back() = MultisampleTargetStorageView(context, resource, uintStorage); multisampleSources.push_back(resource.baseAddress); storageMips.push_back(0); storageKeys.push_back(0); @@ -5643,23 +5644,13 @@ std::shared_ptr ShaderResources::PrepareDrawBindi const auto& item = allocations[index]; const auto override = std::find_if(moved.begin(), moved.end(), [&](const MovedBuffer& entry) { return entry.allocation == index; }); if (override != moved.end() && !override->words.empty()) { - constexpr std::size_t MovedWordBufferSlots = 8; - const auto held = std::find_if(movedWordBuffers.begin(), movedWordBuffers.end(), [&](const MovedWordBuffer& entry) { return entry.allocation == index && entry.size == override->size && std::ranges::equal(entry.words, override->words); }); - std::shared_ptr buffer; - if (held != movedWordBuffers.end()) { - buffer = held->buffer; - } else { - buffer = std::make_shared(context, override->size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - std::memcpy(buffer->Bytes().data(), override->words.data(), override->size); - for (const auto& patch : dataPatches) { - if (patch.allocation == index && patch.byte < override->size) buffer->Bytes()[patch.byte] = static_cast(patch.adjustment); - } - MovedWordBuffer entry{index, {override->words.begin(), override->words.end()}, override->size, buffer}; - if (movedWordBuffers.size() < MovedWordBufferSlots) movedWordBuffers.push_back(std::move(entry)); - else movedWordBuffers[movedWordBufferNext++ % MovedWordBufferSlots] = std::move(entry); + auto slice = recorder.DrawUpload(override->size); + std::memcpy(slice.bytes, override->words.data(), override->size); + for (const auto& patch : dataPatches) { + if (patch.allocation == index && patch.byte < override->size) slice.bytes[patch.byte] = static_cast(patch.adjustment); } selected.push_back(index); - keep({0, std::move(buffer), 0, override->size}); + keep({0, std::move(slice.buffer), slice.offset, override->size}); continue; } std::uint64_t address = item.address; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index cb1f93a09b..cac265e81d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1277,7 +1277,7 @@ VkFormat StorageFormatOrUndefined(const Context& context, VkFormat format) { return storage; } -VkFormat UintFormatOfSint(VkFormat format) { +VkFormat UintStorageViewFormat(VkFormat format) { switch (format) { case VK_FORMAT_R8G8_SINT: return VK_FORMAT_R8G8_UINT; case VK_FORMAT_R8G8B8A8_SINT: return VK_FORMAT_R8G8B8A8_UINT; @@ -1287,6 +1287,9 @@ VkFormat UintFormatOfSint(VkFormat format) { case VK_FORMAT_R32_SINT: return VK_FORMAT_R32_UINT; case VK_FORMAT_R32G32_SINT: return VK_FORMAT_R32G32_UINT; case VK_FORMAT_R32G32B32A32_SINT: return VK_FORMAT_R32G32B32A32_UINT; + case VK_FORMAT_R16_SFLOAT: return VK_FORMAT_R16_UINT; + case VK_FORMAT_R16G16_SFLOAT: return VK_FORMAT_R16G16_UINT; + case VK_FORMAT_R16G16B16A16_SFLOAT: return VK_FORMAT_R16G16B16A16_UINT; default: return VK_FORMAT_UNDEFINED; } } @@ -2038,9 +2041,9 @@ VkImageView StorageTexture::FirstLayerView(std::uint32_t mip) { return created; } -VkImageView StorageTexture::StorageView(std::uint32_t mip, bool firstLayer) { +VkImageView StorageTexture::StorageView(std::uint32_t mip, bool firstLayer, bool uintStorage) { const auto packed = PackedStorageViewFormat(storageFormat); - const auto format = packed != VK_FORMAT_UNDEFINED ? packed : UintFormatOfSint(storageFormat); + const auto format = packed != VK_FORMAT_UNDEFINED ? packed : uintStorage ? UintStorageViewFormat(storageFormat) : VK_FORMAT_UNDEFINED; if (format == VK_FORMAT_UNDEFINED) return firstLayer ? FirstLayerView(mip) : View(mip); if (format == VK_FORMAT_R32_UINT) return AtomicView(mip, firstLayer); const auto found = uintViews.find({mip, firstLayer}); diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp index 04702130a0..f07d5c39c5 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp @@ -4,6 +4,8 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include +#include +#include #include #include @@ -12,10 +14,16 @@ int AgcDriver::SubmitMany(std::uint32_t* const* addresses, const std::uint32_t* sizes, std::uint32_t count, std::uint32_t queue) { if (count == 0) return 0; + if (count > std::numeric_limits::max() / sizeof(std::uint32_t*)) { + throw std::runtime_error("AGC driver: multi-submit count overflow"); + } if (!addresses || !sizes) throw std::runtime_error("AGC driver: null multi-submit arrays"); AgcDriver::GuestMemory::CheckRange(addresses, count * sizeof(*addresses), alignof(std::uint32_t*)); AgcDriver::GuestMemory::CheckRange(sizes, count * sizeof(*sizes), alignof(std::uint32_t)); for (std::uint32_t i = 0; i < count; ++i) { + if (sizes[i] != 0 && addresses[i] == nullptr) { + throw std::runtime_error("AGC driver: null multi-submit command buffer"); + } const Packet packet{addresses[i], sizes[i], 0, {}}; AgcDriver::Submit(&packet, queue); } diff --git a/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp index 23e6c21513..9b557243eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "SceTypes.hpp" #include #include @@ -80,6 +81,9 @@ Layout allocate() { auto* const block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); check(block != nullptr, "cannot allocate guest memory"); GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, 0x04, BlockBytes); +#ifdef __linux__ + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); +#endif { GuestAllocations::Mutation mutation; mutation.Add(block, BlockBytes, true, true, true); @@ -184,8 +188,11 @@ void testCaptureAndReplay(const std::filesystem::path& tool) { const auto firstDump = capture / "first.bin"; const auto secondDump = capture / "second.bin"; - const auto command = "\"\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(layout.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + firstDump.string() + - "\" --dump " + std::to_string(at(late.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + secondDump.string() + "\"\""; + auto command = "\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(layout.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + firstDump.string() + + "\" --dump " + std::to_string(at(late.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + secondDump.string() + "\""; +#ifdef _WIN32 + command = "\"" + command + "\""; +#endif const auto code = std::system(command.c_str()); check(code == 0, "agc_frame_replay exited with " + std::to_string(code)); check(readFile(firstDump) == first, "the replay did not reproduce the first copy"); @@ -241,7 +248,10 @@ void testConcurrentCopies(const std::filesystem::path& tool) { check(memory != 0, "the capture holds no memory deltas"); const auto dump = capture / "targets.bin"; - const auto command = "\"\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(targets)) + ":" + std::to_string(Copies * DataBytes) + ":\"" + dump.string() + "\"\""; + auto command = "\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(targets)) + ":" + std::to_string(Copies * DataBytes) + ":\"" + dump.string() + "\""; +#ifdef _WIN32 + command = "\"" + command + "\""; +#endif const auto code = std::system(command.c_str()); check(code == 0, "agc_frame_replay exited with " + std::to_string(code)); const auto replayed = readFile(dump); diff --git a/core/libs/prx/libSceAgcDriver/tests/FrameReplayState.cpp b/core/libs/prx/libSceAgcDriver/tests/FrameReplayState.cpp index c54b01466a..29c816ebcd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FrameReplayState.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FrameReplayState.cpp @@ -2,12 +2,15 @@ #include "../tools/FrameReplay.cpp" #undef main #include "prx/libSceAgcDriver/Execution/include/FrameRecorder.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include namespace { using AgcDriver::FrameCaptureWriter; +constexpr std::uint32_t ReadWrite = 0x04; + void check(bool condition, const char* reason) { if (!condition) throw std::runtime_error(reason); } @@ -20,6 +23,9 @@ std::byte* allocate(std::size_t size, bool gpu = true) { auto* bytes = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(size, 65536)); check(bytes != nullptr, "guest allocation failed"); GuestArena::GuestArenaCommit_nid_postfix(bytes, size, 0x04, size); +#ifdef __linux__ + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(bytes, size); +#endif GuestAllocations::Mutation mutation; mutation.Add(bytes, size, true, true, gpu); return bytes; @@ -61,7 +67,7 @@ void testMemoryOrder(const std::filesystem::path& directory) { } void testReusedMemory(const std::filesystem::path& directory) { -#ifdef _WIN32 +#if defined(_WIN32) || defined(__linux__) constexpr std::size_t Size = 2 * FrameCaptureWriter::BlockBytes; auto* bytes = allocate(Size); auto* cpu = allocate(FrameCaptureWriter::BlockBytes, false); @@ -73,8 +79,8 @@ void testReusedMemory(const std::filesystem::path& directory) { std::size_t arenaSize = 0; GuestArena::GuestArenaRange_nid_postfix(&base, &arenaSize); FrameCaptureWriter writer(directory); - writer.WriteRange(address(bytes), Size, FrameRangeKind::Watched, PAGE_READWRITE, FrameRangeAccess::Readable | FrameRangeAccess::Writable | FrameRangeAccess::Gpu); - writer.WriteRange(address(cpu), FrameCaptureWriter::BlockBytes, FrameRangeKind::Unwatched, PAGE_READWRITE, FrameRangeAccess::Readable | FrameRangeAccess::Writable); + writer.WriteRange(address(bytes), Size, FrameRangeKind::Watched, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable | FrameRangeAccess::Gpu); + writer.WriteRange(address(cpu), FrameCaptureWriter::BlockBytes, FrameRangeKind::Unwatched, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable); check(writer.WriteSnapshot(address(bytes), expected) == FrameCaptureWriter::BlockBytes, "zero snapshot block was not elided"); writer.Finish("{\"arena_base\":" + std::to_string(base) + "}"); const FrameCaptureReader reader(directory); @@ -85,6 +91,67 @@ void testReusedMemory(const std::filesystem::path& directory) { #endif } +void testSharedPage(const std::filesystem::path& directory) { +#ifdef __linux__ + constexpr std::size_t Page = 65536; + auto* page = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(Page, Page)); + check(page != nullptr, "guest reservation failed"); + GuestArena::GuestArenaRelease_nid_postfix(page, Page); + const std::vector gpu(0x20, std::byte{0x3c}); + const std::vector cpu(0x40, std::byte{0x5a}); + std::uintptr_t base = 0; + std::size_t arenaSize = 0; + GuestArena::GuestArenaRange_nid_postfix(&base, &arenaSize); + FrameCaptureWriter writer(directory); + writer.WriteRange(address(page + 0x20), gpu.size(), FrameRangeKind::Watched, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable | FrameRangeAccess::Gpu); + writer.WriteRange(address(page + 0x40), cpu.size(), FrameRangeKind::Unwatched, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable); + writer.WriteSnapshot(address(page + 0x20), gpu); + writer.WriteSnapshot(address(page + 0x40), cpu); + writer.Finish("{\"arena_base\":" + std::to_string(base) + "}"); + const FrameCaptureReader reader(directory); + const auto skipped = restoreMemory(reader); + check(skipped.bytes == 0, "a range sharing a page with an earlier one was skipped"); + check(std::equal(gpu.begin(), gpu.end(), page + 0x20) && std::equal(cpu.begin(), cpu.end(), page + 0x40), "ranges sharing a page were not restored"); +#else + static_cast(directory); +#endif +} + +void testHostNeighbors(const std::filesystem::path& directory) { +#ifdef __linux__ + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + void* mapped = mmap(nullptr, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + check(mapped != MAP_FAILED, "host allocation failed"); + auto* occupied = static_cast(mapped); + auto* available = occupied + page; + check(munmap(available, page) == 0, "host page release failed"); + std::uintptr_t base = 0; + std::size_t arenaSize = 0; + GuestArena::GuestArenaRange_nid_postfix(&base, &arenaSize); + FrameCaptureWriter writer(directory); + writer.WriteRange(address(occupied), page, FrameRangeKind::Host, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable); + writer.WriteRange(address(available), page, FrameRangeKind::Host, ReadWrite, FrameRangeAccess::Readable | FrameRangeAccess::Writable | FrameRangeAccess::Gpu); + writer.Finish("{\"arena_base\":" + std::to_string(base) + "}"); + const FrameCaptureReader reader(directory); + const auto skipped = restoreMemory(reader); + check(skipped.bytes == page, "an occupied CPU host range was not the only range skipped"); +#else + static_cast(directory); +#endif +} + +void testUnwatched() { + auto* recorder = AgcDriver::FrameRecorder::Active(); + check(recorder != nullptr, "frame recorder is disabled"); + try { + recorder->FrameBoundary(); + } catch (const std::runtime_error& error) { + check(std::string_view(error.what()).find("write watch is unavailable") != std::string_view::npos, "a capture without the write watch failed for another reason"); + return; + } + check(false, "a capture started without the write watch"); +} + void testEopTokens() { Frames frames; auto output = std::make_shared(frames); @@ -170,9 +237,14 @@ void testShaderCode(const std::filesystem::path& directory, bool host) { auto* shader = reinterpret_cast(bytes); auto* code = bytes + (1u << 19) + 256; constexpr std::size_t CodeBytes = 3 * FrameCaptureWriter::BlockBytes; -#ifdef _WIN32 +#if defined(_WIN32) || defined(__linux__) if (host) { +#ifdef _WIN32 auto* memory = static_cast(VirtualAlloc(nullptr, CodeBytes + 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + void* mapped = mmap(nullptr, CodeBytes + 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + auto* memory = mapped != MAP_FAILED ? static_cast(mapped) : nullptr; +#endif check(memory != nullptr, "host shader allocation failed"); GuestAllocations::Mutation mutation; mutation.Add(memory, CodeBytes + 4096, true, true, true); @@ -226,6 +298,9 @@ int main(int argc, char** argv) { const std::filesystem::path directory = argv[2]; if (mode == "order") testMemoryOrder(directory); else if (mode == "zero") testReusedMemory(directory); + else if (mode == "shared_page") testSharedPage(directory); + else if (mode == "host_neighbors") testHostNeighbors(directory); + else if (mode == "unwatched") testUnwatched(); else if (mode == "tokens") testEopTokens(); else if (mode == "blocking") testBlockingSubmission(directory); else if (mode == "shader" || mode == "shader_host") testShaderCode(directory, mode == "shader_host"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d767e03481..90c946f214 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6252,8 +6252,8 @@ void descriptorSnapshotTests() { Require(descriptor.buffer == original[index].buffer && descriptor.offset == original[index].offset && descriptor.range == original[index].range, "snapshot update changed an untouched buffer descriptor"); } else { const auto& item = snapshots->snapshots[snapshot]; - Require(descriptor.buffer == item.buffer->Handle() && descriptor.offset == 0 && descriptor.range == moved[snapshot].size, "snapshot descriptor used another element's buffer or range"); - Require(sameBytes(bufferBytes(descriptor.buffer), moved[snapshot].words.data(), moved[snapshot].size), "snapshot descriptor lost its buffer data"); + Require(descriptor.buffer == item.buffer->Handle() && descriptor.offset == item.offset && descriptor.range == moved[snapshot].size, "snapshot descriptor used another element's buffer or range"); + Require(sameBytes(bufferBytes(descriptor.buffer).subspan(descriptor.offset), moved[snapshot].words.data(), moved[snapshot].size), "snapshot descriptor lost its buffer data"); ++snapshot; } } @@ -6879,7 +6879,7 @@ void imageTableSnapshotAllocationTests() { const auto snapshots = resources.PrepareDrawBindings(recorder, moved); Require(snapshots != nullptr && snapshots->snapshots.size() == 1 && cache.Counters().sets == sets + 1, "an empty image table prevented draw snapshot allocation"); Require(mock.variableDescriptorCount == 1u, "an empty draw snapshot table did not reserve one physical image descriptor"); - Require(sameBytes(bufferBytes(snapshots->snapshots[0].buffer->Handle()), moved[0].words.data(), moved[0].size), "an empty image table changed draw snapshot contents"); + Require(sameBytes(bufferBytes(snapshots->snapshots[0].buffer->Handle()).subspan(snapshots->snapshots[0].offset), moved[0].words.data(), moved[0].size), "an empty image table changed draw snapshot contents"); const auto& currentMap = findWrite(map.binding).buffers.at(0); Require(currentMap.buffer == originalMap.buffer && currentMap.offset == originalMap.offset && currentMap.range == originalMap.range, "draw snapshots replaced the empty image table map"); std::array header{}; @@ -11478,6 +11478,38 @@ void meshArgumentTests() { Require(same(ResolveMeshArguments(record(0xffffffffu, 1, 0), rules(points, 0xffffffffu)), {0, 0, 0, 0xffffffffu, 0}), "groups over the device limit draw nothing"); } +void meshLimitTests() { + AgcDriver::Graphics::Context context{}; + context.meshLimits.maxMeshWorkGroupInvocations = 128; + context.meshLimits.maxMeshWorkGroupSize[0] = 96; + context.meshLimits.maxMeshOutputVertices = 256; + context.meshLimits.maxMeshOutputPrimitives = 512; + context.meshLimits.maxMeshSharedMemorySize = 12284; + ShaderRecompiler::MeshConfiguration mesh{ + .inputPrimitive = 4u, + .primitivesPerGroup = 32u, + .verticesPerGroup = 96u, + .maxVertices = 256u, + .maxPrimitives = 512u, + .threadsPerGroup = 128u, + .ldsSizeDwords = 3071u, + .provokingVertex = 0u, + .esgsItemSize = 4u, + }; + AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 96); + expectFailure([&] { AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 97); }, "mesh workgroup of 97 invocations exceeds device limits (128 invocations, 96 wide)"); + mesh.maxVertices = 257; + expectFailure([&] { AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 96); }, "mesh output vertices 257 exceed the device limit 256"); + mesh.maxVertices = 256; + mesh.maxPrimitives = 513; + expectFailure([&] { AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 96); }, "mesh output primitives 513 exceed the device limit 512"); + mesh.maxPrimitives = 512; + mesh.ldsSizeDwords = 3072; + expectFailure([&] { AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 96); }, "mesh LDS 12288 bytes exceeds the device shared memory limit 12284"); + mesh.ldsSizeDwords = 3071; + AgcDriver::Graphics::ValidateMeshLimits(context, mesh, 96); +} + void debugBranchTests() { for (const auto opcode : {0x17u, 0x18u, 0x19u, 0x1au}) Require(recompilesDebugBranch(opcode), "a conditional debug branch did not recompile"); } @@ -12721,6 +12753,7 @@ int main(int argc, char** argv) { storageTextureBudgetTests(); sampledBudgetReportTests(); meshArgumentTests(); + meshLimitTests(); meshIndexBufferTests(); stagingPoolBudgetTests(); validationTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 6e37540a95..acec23f12a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4343,8 +4343,10 @@ void drawDescriptorWriteTests(const Device& device, Recorder& recorder) { Require(actual.buffers.size() == original.buffers.size() && actual.images.size() == original.images.size(), "snapshot update omitted unchanged descriptors"); for (const auto& [key, before] : original.buffers) { const auto after = actual.buffers.at(key); - const auto expected = key.first == (allocation == 0 ? 2u : 7u) ? draw->snapshots.front().buffer->Handle() : before.buffer; - Require(after.buffer == expected && after.offset == before.offset && after.range == before.range, "snapshot update replaced the wrong buffer or changed its range"); + const bool replaced = key.first == (allocation == 0 ? 2u : 7u); + const auto expected = replaced ? draw->snapshots.front().buffer->Handle() : before.buffer; + const auto expectedOffset = replaced ? draw->snapshots.front().offset : before.offset; + Require(after.buffer == expected && after.offset == expectedOffset && after.range == before.range, "snapshot update replaced the wrong buffer or changed its range"); } for (const auto& [key, before] : original.images) { const auto after = actual.images.at(key); diff --git a/core/libs/prx/libSceAgcDriver/tests/RuntimeSwitches.cpp b/core/libs/prx/libSceAgcDriver/tests/RuntimeSwitches.cpp new file mode 100644 index 0000000000..fe37684a2d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RuntimeSwitches.cpp @@ -0,0 +1,151 @@ +#include "prx/libSceAgcDriver/Graphics/include/RuntimeSwitches.hpp" +#include +#include +#include +#include +#include +#include + +using AgcDriver::Graphics::AbSchedule; +using AgcDriver::Graphics::RuntimeSwitch; +namespace RuntimeSwitches = AgcDriver::Graphics::RuntimeSwitches; + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void SetEnvironment(const char* name, const char* value) { +#ifdef _WIN32 + _putenv_s(name, value != nullptr ? value : ""); +#else + if (value != nullptr) setenv(name, value, 1); + else unsetenv(name); +#endif +} + +std::string Refusal(const char* ab, const char* start = "", const char* settle = "") { + try { + AbSchedule::Parse(ab, start, settle); + } catch (const std::invalid_argument& error) { + return error.what(); + } + return {}; +} + +void registry() { + for (std::size_t index = 0; index < static_cast(RuntimeSwitch::Count); ++index) { + const auto id = static_cast(index); + Require(RuntimeSwitches::Find(RuntimeSwitches::Name(id)) == id, std::string("the name of a switch does not find it: ") + RuntimeSwitches::Name(id)); + Require(RuntimeSwitches::Set(id) == (std::getenv(RuntimeSwitches::Name(id)) != nullptr), std::string("a switch does not start as the environment sets it: ") + RuntimeSwitches::Name(id)); + } + Require(RuntimeSwitches::Find("APS5_NO_BARRIER_BATCH") == RuntimeSwitch::NoBarrierBatch, "APS5_NO_BARRIER_BATCH is not a runtime switch"); + Require(RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "a switch set in the environment did not start set"); + Require(!RuntimeSwitches::Find("APS5_FULL_BARRIERS").has_value() && !RuntimeSwitches::Find("APS5_NO_BARRIER_BATCH ").has_value(), "a name that is not a runtime switch was found"); + RuntimeSwitches::Assign(RuntimeSwitch::NoBarrierBatch, false); + Require(!RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "an assigned switch did not read back unset"); + RuntimeSwitches::Assign(RuntimeSwitch::NoBarrierBatch, true); + Require(RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "an assigned switch did not read back set"); +} + +void parse() { + const auto config = AbSchedule::Parse(" 30 : APS5_NO_BARRIER_BATCH ", "815", "3"); + Require(config.phaseSeconds == 30 && config.settleSeconds == 3 && config.startPresent == 815 && !config.startSeconds.has_value(), "the phase settings were not parsed"); + Require(config.switches.size() == 1 && config.switches[0] == RuntimeSwitch::NoBarrierBatch, "the switch list was not parsed"); + const auto bySeconds = AbSchedule::Parse("60:APS5_NO_BARRIER_BATCH", "90s", ""); + Require(bySeconds.startSeconds == 90.0 && bySeconds.settleSeconds == 2 && bySeconds.startPresent == 0, "a start in seconds was not parsed"); + Require(Refusal("60:APS5_FULL_BARRIERS").find("APS5_FULL_BARRIERS cannot change while the title runs; switches the A/B can change: APS5_NO_BARRIER_BATCH") != std::string::npos, "a startup-only variable was not refused with the list of switches"); + Require(Refusal("60:APS5_NO_BARRIER_BATCH+APS5_NO_BARRIER_BATCH").find("listed twice") != std::string::npos, "a duplicate switch was not refused"); + Require(Refusal("60:+APS5_NO_BARRIER_BATCH").find("empty switch name") != std::string::npos && Refusal("60:APS5_NO_BARRIER_BATCH+").find("empty switch name") != std::string::npos, "an empty switch name was not refused"); + Require(Refusal("60:").find("no switch listed") != std::string::npos, "an empty switch list was not refused"); + Require(!Refusal("APS5_NO_BARRIER_BATCH").empty(), "a missing phase length was not refused"); + Require(!Refusal("0.5:APS5_NO_BARRIER_BATCH").empty() && !Refusal("x:APS5_NO_BARRIER_BATCH").empty() && !Refusal("-5:APS5_NO_BARRIER_BATCH").empty() && !Refusal("inf:APS5_NO_BARRIER_BATCH").empty(), "a bad phase length was not refused"); + Require(!Refusal("10:APS5_NO_BARRIER_BATCH", "", "10").empty() && !Refusal("10:APS5_NO_BARRIER_BATCH", "", "x").empty(), "a bad settle was not refused"); + for (const char* start : {"-3", "abc", "s", "+3", "3x", "99999999999999999999999"}) { + Require(Refusal("10:APS5_NO_BARRIER_BATCH", start).find("APS5_AB_START") != std::string::npos, std::string("the start '") + start + "' was not refused"); + } +} + +void schedule() { + AbSchedule plan(AbSchedule::Parse("60:APS5_NO_BARRIER_BATCH", "10", "2")); + using Event = AbSchedule::Event; + const auto expect = [&](std::uint64_t present, double seconds, Event event, std::uint64_t phase, bool b) { + const auto step = plan.AtPresent(present, seconds); + Require(step.event == event && (event == Event::None || (step.phase == phase && step.b == b)), "unexpected step at present " + std::to_string(present)); + }; + expect(1, 0.0, Event::None, 0, false); + expect(9, 100.0, Event::None, 0, false); + expect(10, 102.0, Event::Begin, 0, false); + expect(11, 103.0, Event::None, 0, false); + expect(12, 104.5, Event::Settled, 0, false); + expect(13, 150.0, Event::None, 0, false); + expect(14, 161.9, Event::None, 0, false); + expect(15, 162.0, Event::Begin, 1, true); + expect(16, 163.0, Event::None, 1, true); + expect(17, 300.0, Event::Begin, 2, false); + expect(18, 301.0, Event::None, 2, false); + expect(19, 302.5, Event::Settled, 2, false); + expect(20, 362.5, Event::Begin, 3, true); + + AbSchedule timed(AbSchedule::Parse("30:APS5_NO_BARRIER_BATCH", "90s", "")); + Require(timed.AtPresent(500, 1000.0).event == Event::None, "a start in seconds began at the first present"); + Require(timed.AtPresent(501, 1089.0).event == Event::None, "a start in seconds began early"); + const auto begin = timed.AtPresent(502, 1090.0); + Require(begin.event == Event::Begin && begin.phase == 0 && !begin.b, "a start in seconds did not begin phase A"); +} + +void glue() { + Require(AgcDriver::Graphics::AbConfigured(), "APS5_AB is not seen"); + Require(AgcDriver::Graphics::AbAtPresent(1, 0.0).empty(), "the A/B printed before its start"); + RuntimeSwitches::Assign(RuntimeSwitch::NoBarrierBatch, false); + auto lines = AgcDriver::Graphics::AbAtPresent(3, 1.0); + Require(lines.size() == 2 && lines[0] == "[ab] in-run A/B: phases of 5.0 s from present 3, settle 2.0 s; A: APS5_NO_BARRIER_BATCH set; B: APS5_NO_BARRIER_BATCH unset\n" && lines[1] == "[ab] phase A 0 begins at present 3 (t 1.000 s): APS5_NO_BARRIER_BATCH set\n", "the start lines differ: " + (lines.empty() ? std::string() : lines.front())); + Require(RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "phase A does not have the environment's setting"); + lines = AgcDriver::Graphics::AbAtPresent(4, 3.5); + Require(lines.size() == 1 && lines[0] == "[ab] phase A 0 settled at present 4 (t 3.500 s)\n", "the settle line differs"); + lines = AgcDriver::Graphics::AbAtPresent(5, 6.0); + Require(lines.size() == 1 && lines[0] == "[ab] phase B 1 begins at present 5 (t 6.000 s): APS5_NO_BARRIER_BATCH unset\n", "the phase B line differs"); + Require(!RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "phase B did not toggle the listed switch"); + Require(AgcDriver::Graphics::AbAtPresent(6, 7.0).empty(), "a present inside the settle printed"); + lines = AgcDriver::Graphics::AbAtPresent(7, 11.5); + Require(lines.size() == 1 && lines[0] == "[ab] phase A 2 begins at present 7 (t 11.500 s): APS5_NO_BARRIER_BATCH set\n", "phase A did not come back"); + Require(RuntimeSwitches::Set(RuntimeSwitch::NoBarrierBatch), "phase A did not restore the environment's setting"); +} + +int refused() { + SetEnvironment("APS5_AB", "5:APS5_FULL_BARRIERS"); + try { + AgcDriver::Graphics::AbAtPresent(1, 0.0); + } catch (const std::invalid_argument& error) { + if (std::string_view(error.what()).find("APS5_FULL_BARRIERS cannot change while the title runs") != std::string_view::npos) { + std::printf("runtime switch refusal test passed\n"); + return 0; + } + std::fprintf(stderr, "FAILED: unexpected refusal: %s\n", error.what()); + return 1; + } + std::fprintf(stderr, "FAILED: APS5_AB naming a startup-only variable was not refused at the first present\n"); + return 1; +} + +} + +int main(int argc, char** argv) { + if (argc > 1 && std::string_view(argv[1]) == "refused") return refused(); + try { + SetEnvironment("APS5_NO_BARRIER_BATCH", "1"); + SetEnvironment("APS5_AB", "5:APS5_NO_BARRIER_BATCH"); + SetEnvironment("APS5_AB_START", "3"); + SetEnvironment("APS5_AB_SETTLE", nullptr); + registry(); + parse(); + schedule(); + glue(); + } catch (const std::exception& error) { + std::fprintf(stderr, "FAILED: %s\n", error.what()); + return 1; + } + std::printf("runtime switch tests passed\n"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index fa67366d65..4068a2ce41 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -445,6 +445,9 @@ void testMultiAcbs() { check(sceAgcDriverSubmitMultiAcbs(0, nullptr, nullptr, 0) == 0, "empty multi-ACB submit checked its queue"); expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x20, nullptr, sizes.data(), 1); }); expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x20, addresses.data(), nullptr, 1); }); + std::array nullAcbs{nullptr}; + std::array nonZeroAcbSizes{4}; + expectFailure([&] { sceAgcDriverSubmitMultiAcbs(0x20, nullAcbs.data(), nonZeroAcbSizes.data(), 1); }); check(value == 0, "a rejected multi-ACB submit ran a command buffer"); for (std::uint32_t queue : {0x20u, 0x57u}) { value = 0; @@ -500,6 +503,11 @@ void testMultiSubmissions() { expectFailure([&] { sceAgcDriverAgrSubmitMultiDcbs(invalidAddresses.data(), nullptr, 1); }); expectFailure([&] { sceAgcDriverSubmitMultiDcbs(invalidAddresses.data(), invalidSizes.data(), 1); }); expectFailure([&] { sceAgcDriverAgrSubmitMultiDcbs(invalidAddresses.data(), invalidSizes.data(), 1); }); + std::array nullAddresses{nullptr}; + std::array nonZeroSizes{4}; + expectFailure([&] { sceAgcDriverSubmitMultiDcbs(nullAddresses.data(), nonZeroSizes.data(), 1); }); + expectFailure([&] { sceAgcDriverAgrSubmitMultiDcbs(nullAddresses.data(), nonZeroSizes.data(), 1); }); + expectFailure([&] { sceAgcDriverSubmitMultiDcbs(invalidAddresses.data(), invalidSizes.data(), std::numeric_limits::max()); }); for (bool agr : {false, true}) { value = 0; done = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp new file mode 100644 index 0000000000..bef64fd6b2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreFloatRounding.cpp @@ -0,0 +1,210 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.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 Width = 32; +constexpr std::uint32_t Height = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::uint32_t TwoChannelSwizzle = 0x22cu; +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t TexelBytes = 8192; + +alignas(256) std::array Input{}; +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, + 0x7e3e0281, 0xf0201508, 0x0001141e, 0xbf810000, +}; + +struct Format { + const char* name; + std::uint32_t value; + std::uint32_t components; +}; + +constexpr std::array Formats{{ + {"16_FLOAT", 13u, 1u}, + {"16_16_FLOAT", 29u, 2u}, + {"16_16_16_16_FLOAT", 71u, 4u}, +}}; + +struct Write { + std::uint32_t row; + std::uint32_t dmask; +}; + +constexpr std::array Writes{{{0u, 0xfu}, {1u, 0x5u}}}; +constexpr std::array EdgeBits{ + 0x00000000u, 0x80000000u, 0x3f800000u, 0xbf800000u, 0x3f801000u, 0x3f803000u, + 0x3f800fffu, 0x3f801001u, 0x477fe000u, 0x477ff000u, 0x47800000u, 0xc7800000u, + 0x7f800000u, 0xff800000u, 0x7fc12345u, 0x7f812345u, 0x38800000u, 0x33800000u, + 0x33000000u, 0x000116c2u, 0x3eaaaaabu, 0x40490fdbu, 0x00000001u, 0x80000001u, +}; + +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(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "image store float rounding: 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 + } + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::uint16_t HalfTowardZero(std::uint32_t bits) { + const auto sign = static_cast((bits >> 16u) & 0x8000u); + const auto exponent = (bits >> 23u) & 0xffu; + const auto mantissa = bits & 0x7fffffu; + if (exponent == 0xffu) { + auto payload = static_cast(mantissa >> 13u); + if (mantissa != 0u && payload == 0u) payload = 1u; + return static_cast(sign | 0x7c00u | payload); + } + if (exponent == 0u) return sign; + const int unbiased = static_cast(exponent) - 127; + if (unbiased > 15) return static_cast(sign | 0x7bffu); + if (unbiased < -14) { + const int shift = -14 - unbiased + 13; + if (shift >= 32) return sign; + const auto significand = 0x800000u | mantissa; + return static_cast(sign | (significand >> static_cast(shift))); + } + return static_cast(sign | (static_cast(unbiased + 15) << 10u) | (mantissa >> 13u)); +} + +std::uint32_t Value(std::uint32_t tid, std::uint32_t component) { + const auto slot = tid * 4u + component; + if (slot < EdgeBits.size()) return EdgeBits[slot]; + const auto seed = (slot + 1u) * 0x9e3779b1u; + const auto mixed = seed ^ (seed >> 16u); + return (mixed & 0x807fffffu) | (((mixed >> 23u) % 31u + 110u) << 23u); +} + +std::uint64_t TexelOffset(const Format& format, std::uint32_t x, std::uint32_t y) { + const auto mip = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format.value, Width, Height, 1u).at(0); + return mip.tiledOffset + static_cast(y) * mip.pitchBytes + static_cast(x) * format.components * sizeof(std::uint16_t); +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data, const Format& format) { + const auto address = static_cast(reinterpret_cast(data)); + return {static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (format.value << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), (format.components == 2u ? TwoChannelSwizzle : IdentitySwizzle) | (Type2D << 28u), 0u, 0u, 0u, 0u}; +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) Input[tid * 4u + component] = Value(tid, component); + } + std::fill(texels, texels + TexelBytes, 0x5au); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Input.data(), static_cast(Input.size() * sizeof(Input[0]))); + const auto texture = TextureDescriptor(texels, format); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(StoreCode); + const std::array memory{{{reinterpret_cast(StoreCode.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(StoreCode.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(StoreCode.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), TexelBytes, nullptr, "test"); + device.WaitIdle(); +} + +std::uint16_t ExpectedComponent(std::uint32_t tid, std::uint32_t dmask, std::uint32_t component) { + if (((dmask >> component) & 1u) == 0u) return 0u; + std::uint32_t source = 0u; + for (std::uint32_t lower = 0u; lower < component; ++lower) source += (dmask >> lower) & 1u; + return HalfTowardZero(Value(tid, source)); +} + +void Check(const std::uint8_t* texels, const Format& format) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = TexelOffset(format, x, y); + for (std::uint32_t component = 0; component < format.components; ++component) { + std::uint16_t actual = 0u; + std::memcpy(&actual, texels + offset + component * sizeof(actual), sizeof(actual)); + const auto dmask = y == Writes[0].row ? Writes[0].dmask : y == Writes[1].row ? Writes[1].dmask : 0u; + const auto expected = dmask != 0u ? ExpectedComponent(x, dmask, component) : 0x5a5au; + Require(actual == expected, std::string("image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + for (const auto& format : Formats) { + Store(*device, block.Data(), format); + Check(block.Data(), format); + } + std::puts("image store float rounding tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp index 943f962b6e..0ddce118db 100644 --- a/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp @@ -35,6 +35,11 @@ #ifdef _WIN32 #include #endif +#ifdef __linux__ +#include "prx/libc/include/specifics/linux/HostMapping.hpp" +#include +#include +#endif namespace { @@ -142,6 +147,13 @@ bool committedWritable(std::uint64_t address, std::uint64_t bytes) { at = reinterpret_cast(info.BaseAddress) + info.RegionSize; } return true; +#elif defined(__linux__) + auto cursor = address; + HostMapping::Walk(address, address + bytes, "frame replay", [&](const HostMapping::Mapping& mapping) { + if (mapping.start > cursor || !mapping.readable || !mapping.writable) return; + cursor = std::max(cursor, mapping.end); + }); + return cursor >= address + bytes; #else static_cast(address); static_cast(bytes); @@ -149,6 +161,21 @@ bool committedWritable(std::uint64_t address, std::uint64_t bytes) { #endif } +void commit(const FrameMemoryChunk& chunk) { + auto* const pointer = reinterpret_cast(static_cast(chunk.address)); + const auto bytes = static_cast(chunk.bytes); +#ifdef __linux__ + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + const auto begin = chunk.address & ~(page - 1); + const auto end = (chunk.address + chunk.bytes + page - 1) & ~(page - 1); + if (begin != chunk.address || end != chunk.address + chunk.bytes) { + GuestArena::GuestArenaSetProtection_nid_postfix(static_cast(begin), static_cast(end - begin), 0x04); + return; + } +#endif + GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, 0x04, bytes); +} + std::pair progress(); struct Skipped { @@ -189,6 +216,31 @@ void mapHostRanges(const FrameCaptureReader& reader, Skipped& skipped) { if ((chunk->access & FrameRangeAccess::Gpu) != 0) throw std::runtime_error("frame replay: cannot map the GPU-visible host range at " + std::to_string(chunk->address)); skipped.Add(*chunk); } +#elif defined(__linux__) + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + std::sort(host.begin(), host.end(), [](const auto* left, const auto* right) { return left->address < right->address; }); + std::vector> envelopes; + for (const auto* chunk : host) { + const auto begin = chunk->address & ~(page - 1); + const auto end = (chunk->address + chunk->bytes + page - 1) & ~(page - 1); + if (!envelopes.empty() && begin < envelopes.back().second) envelopes.back().second = std::max(envelopes.back().second, end); + else envelopes.emplace_back(begin, end); + } + std::vector> refused; + for (const auto& [begin, end] : envelopes) { + auto* const wanted = reinterpret_cast(static_cast(begin)); + const auto bytes = static_cast(end - begin); + void* const placed = mmap(wanted, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + if (placed == wanted) continue; + if (placed != MAP_FAILED) munmap(placed, bytes); + refused.emplace_back(begin, end); + } + for (const auto* chunk : host) { + const bool blocked = std::any_of(refused.begin(), refused.end(), [&](const auto& range) { return chunk->address < range.second && range.first < chunk->address + chunk->bytes; }); + if (!blocked) continue; + if ((chunk->access & FrameRangeAccess::Gpu) != 0) throw std::runtime_error("frame replay: cannot map the GPU-visible host range at " + std::to_string(chunk->address)); + skipped.Add(*chunk); + } #else for (const auto* chunk : host) skipped.Add(*chunk); #endif @@ -202,12 +254,15 @@ Skipped restoreMemory(const FrameCaptureReader& reader) { progress(); Skipped skipped; mapHostRanges(reader, skipped); + std::vector owned; + for (const auto& chunk : reader.Chunks()) owned.push_back(chunk.kind == FrameMemoryKind::Range && chunk.rangeKind != FrameRangeKind::Host && committedWritable(chunk.address, chunk.bytes)); { GuestAllocations::Mutation mutation; - for (const auto& chunk : reader.Chunks()) { + for (std::size_t index = 0; index < reader.Chunks().size(); ++index) { + const auto& chunk = reader.Chunks()[index]; if (chunk.kind != FrameMemoryKind::Range || chunk.rangeKind == FrameRangeKind::Host) continue; const bool gpu = (chunk.access & FrameRangeAccess::Gpu) != 0; - if (committedWritable(chunk.address, chunk.bytes)) { + if (owned[index]) { if (!gpu) skipped.Add(chunk); else std::memset(reinterpret_cast(static_cast(chunk.address)), 0, static_cast(chunk.bytes)); continue; @@ -221,7 +276,7 @@ Skipped restoreMemory(const FrameCaptureReader& reader) { skipped.Add(chunk); continue; } - GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, 0x04, bytes); + commit(chunk); mutation.Add(pointer, bytes, (chunk.access & FrameRangeAccess::Readable) != 0, (chunk.access & FrameRangeAccess::Writable) != 0, gpu); } } diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index 8ca7ca551d..5dc10dec26 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -88,20 +88,17 @@ static int AppendRead(Apr::CommandBufferObject* buffer, Apr::Opcode opcode, std: } static std::uint64_t NopBytes(std::uint32_t dwords) { - return sizeof(Apr::CommandHeader) + std::uint64_t{dwords} * 4u; + return std::uint64_t{dwords} * 4u; } -static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { - if (!buffer) return SCE_KERNEL_ERROR_EINVAL; - const std::uint64_t bytes = NopBytes(dwords); - if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; - if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; - const Apr::CommandHeader header{Apr::Opcode::Nop, static_cast(bytes)}; - std::uint8_t* destination = buffer->base + buffer->offset; +static int AppendNop(Apr::CommandBufferObject& buffer, std::uint32_t header, const std::uint32_t* data) { + const std::uint64_t bytes = NopBytes(Apr::NopDwords(header)); + if (bytes > RemainingCommandBytes(buffer)) return SCE_KERNEL_ERROR_EBUSY; + std::uint8_t* destination = buffer.base + buffer.offset; std::memcpy(destination, &header, sizeof(header)); - if (data) std::memcpy(destination + sizeof(header), data, std::uint64_t{dwords} * 4u); - buffer->offset += static_cast(bytes); - ++buffer->numCommands; + if (data) std::memcpy(destination + sizeof(header), data, bytes - sizeof(header)); + buffer.offset += static_cast(bytes); + ++buffer.numCommands; return 0; } @@ -353,19 +350,21 @@ int APS5_VABI sceAmprCommandBufferConstructMarker(Apr::CommandBufferObject* buff } int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject* buffer, std::int16_t type, const void* payload, std::uint32_t payloadBytes, const std::uint32_t* word) { - (void)type; + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; if (payloadBytes > (word ? 56u : 60u)) return SCE_KERNEL_ERROR_EINVAL; if (payloadBytes != 0u && !payload) throw std::invalid_argument("sceAmprCommandBufferConstructNop: null payload"); const std::uint32_t dwords = (payloadBytes + 3u) / 4u + (word ? 1u : 0u); - const std::uint32_t offset = buffer ? buffer->offset : 0u; - const int result = AppendNop(buffer, dwords, nullptr); + const std::uint32_t offset = buffer->offset; + const int result = AppendNop(*buffer, Apr::NopHeader(dwords, static_cast(type)), nullptr); if (result != 0) return result; - std::uint8_t* data = buffer->base + offset + sizeof(Apr::CommandHeader); + std::uint8_t* data = buffer->base + offset + Apr::NopHeaderBytes; if (word) { std::memcpy(data, word, sizeof(*word)); data += sizeof(*word); } if (payloadBytes != 0u) std::memcpy(data, payload, payloadBytes); + std::memset(data + payloadBytes, 0, (4u - payloadBytes % 4u) % 4u); return 0; } @@ -401,13 +400,17 @@ uint32_t APS5_VABI sceAmprCommandBufferGetType(const Apr::CommandBufferObject* b } int APS5_VABI sceAmprCommandBufferNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; if (dwords == 0u || dwords > 16u) return SCE_KERNEL_ERROR_EINVAL; - return AppendNop(buffer, dwords, nullptr); + return AppendNop(*buffer, Apr::NopHeader(dwords - 1u), nullptr); } int APS5_VABI sceAmprCommandBufferNopWithData(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; if (dwords > 15u) return SCE_KERNEL_ERROR_EINVAL; - return AppendNop(buffer, dwords, data); + return AppendNop(*buffer, Apr::NopHeader(dwords), data); } int APS5_VABI sceAmprCommandBufferPopMarker(Apr::CommandBufferObject* buffer) { @@ -513,7 +516,7 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNop(std::uint32_t dwords) { std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNopWithData(std::uint32_t dwords) { if (dwords == 0u || dwords > 16u) return MeasureInvalid; - return NopBytes(dwords - 1u); + return NopBytes(dwords); } std::uint64_t APS5_VABI sceAmprMeasureCommandSizePopMarker() { diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 3485cd7b86..96a00cca78 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -228,24 +228,34 @@ int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t service_label, co if (!validEntitlementLabel(*entitlement_label, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; return SCE_APP_CONTENT_ERROR_NOT_FOUND; } -int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(const char* entitlementId) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + if (!entitlementId) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } -int APS5_VABI sceAppContentAddcontEnqueueDownloadSp(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentAddcontEnqueueDownloadSp(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)serviceLabel; + if (!entitlementLabel) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0)) throw std::runtime_error("sceAppContentAddcontEnqueueDownloadSp: padded entitlement label before SDK 0x03500000"); + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } -int APS5_VABI sceAppContentAddcontMountByEntitlemetId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentAddcontMountByEntitlemetId(const char* entitlementId, AppContentMountPoint* mountPoint) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + if (!entitlementId || !mountPoint) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } -int APS5_VABI sceAppContentAddcontShrink(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentAddcontShrink(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)serviceLabel; + if (!entitlementLabel) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0x03500000)) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!validEntitlementLabel(*entitlementLabel, 0)) throw std::runtime_error("sceAppContentAddcontShrink: padded entitlement label before SDK 0x03500000"); + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } int APS5_VABI sceAppContentAppParamGetString(void) { @@ -273,13 +283,17 @@ int APS5_VABI sceAppContentDownload1Shrink(void) { return 0; } -int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(const char* entitlementId, AppContentAddcontInfo* info) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + if (!entitlementId || !info) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT; } -int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(uint32_t iroTag, AppContentAddcontInfo* list, uint32_t listNum, uint32_t* hitNum) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + (void)iroTag; + if ((!list || listNum == 0) && !hitNum) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (hitNum) *hitNum = 0; return 0; } @@ -296,8 +310,10 @@ int APS5_VABI sceAppContentGetEntitlementKey(uint32_t serviceLabel, const NpUnif return SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT; } -int APS5_VABI sceAppContentGetPftFlag(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAppContentGetPftFlag(uint32_t* pftFlag) { + if (!initialized.load()) return SCE_APP_CONTENT_ERROR_NOT_INITIALIZED; + if (!pftFlag) return SCE_APP_CONTENT_ERROR_PARAMETER; + *pftFlag = 0; return 0; } diff --git a/core/libs/prx/libSceAppContent/include/Api.hpp b/core/libs/prx/libSceAppContent/include/Api.hpp index cd0a799e37..a48561a66e 100644 --- a/core/libs/prx/libSceAppContent/include/Api.hpp +++ b/core/libs/prx/libSceAppContent/include/Api.hpp @@ -28,20 +28,20 @@ int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* moun int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel); int APS5_VABI sceAppContentAddcontDelete(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel); int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, void* progress); -int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(void); -int APS5_VABI sceAppContentAddcontEnqueueDownloadSp(void); -int APS5_VABI sceAppContentAddcontMountByEntitlemetId(void); -int APS5_VABI sceAppContentAddcontShrink(void); +int APS5_VABI sceAppContentAddcontEnqueueDownloadByEntitlemetId(const char* entitlementId); +int APS5_VABI sceAppContentAddcontEnqueueDownloadSp(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel); +int APS5_VABI sceAppContentAddcontMountByEntitlemetId(const char* entitlementId, AppContentMountPoint* mountPoint); +int APS5_VABI sceAppContentAddcontShrink(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel); int APS5_VABI sceAppContentAppParamGetString(void); int APS5_VABI sceAppContentDownload0Expand(void); int APS5_VABI sceAppContentDownload0Shrink(void); int APS5_VABI sceAppContentDownload1Expand(void); int APS5_VABI sceAppContentDownload1Shrink(void); -int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(void); -int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(void); +int APS5_VABI sceAppContentGetAddcontInfoByEntitlementId(const char* entitlementId, AppContentAddcontInfo* info); +int APS5_VABI sceAppContentGetAddcontInfoListByIroTag(uint32_t iroTag, AppContentAddcontInfo* list, uint32_t listNum, uint32_t* hitNum); int APS5_VABI sceAppContentGetDownloadedStoreCountry(void); int APS5_VABI sceAppContentGetEntitlementKey(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, AppContentEntitlementKey* key); -int APS5_VABI sceAppContentGetPftFlag(void); +int APS5_VABI sceAppContentGetPftFlag(uint32_t* pftFlag); int APS5_VABI sceAppContentGetRegion(void); int APS5_VABI sceAppContentRequestPatchInstall(void); int APS5_VABI sceAppContentTemporaryDataMount(AppContentMountPoint* mountPoint); diff --git a/core/libs/prx/libSceAvSetting/Export.cpp b/core/libs/prx/libSceAvSetting/Export.cpp index 1861a92646..64d53713fc 100644 --- a/core/libs/prx/libSceAvSetting/Export.cpp +++ b/core/libs/prx/libSceAvSetting/Export.cpp @@ -6,8 +6,6 @@ extern "C" { -APS5_DUMMY_FUN - int APS5_VABI sceAvControlGetPuTypeByMonitorInfo(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -128,4 +126,524 @@ int APS5_VABI sceAvControlDebugAddCallbacks(void) { return 0; } +int APS5_VABI sceAvControlChangeOutputLayout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlChangeOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlChangeOutputModeForDiag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlChangeProcessAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlChangeScanInMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetCurrentDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetCurrentOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetCurrentOutputModeWithSync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetDetailHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetNativeMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetPuStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlGetPuType() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsAudioOutModeSupportedByMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsColorimetryHdr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsDeepColorSupportedByMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsDeviceConnected() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsHdrSupportedByMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsModeGen1Vr() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsModeSystemDefault() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsSuspendedProcessOutputModeAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlIsVideoOutModeSupportedByMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSet2dVrMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetAudioMuteForShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetAvOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetColorEffect() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetDefaultAudioOutMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetDefaultVideoOutMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetDispclk() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetGamutMetadata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetHdcpEncryption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetHdmiGamutMetadata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetHdrMetadata() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSetVideoOutSource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvControlSimulateProcessOutputModeArbitration() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingAddCallbacks() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingAddCallbacksForLnc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingAddCallbacksForSocialScreen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingCallbackFuncsInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingCallbackFuncsInitForLnc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingCallbackFuncsInitForLnc_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingCallbackFuncsInit_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingChangeOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingChangeOutputMode2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingChangeOutputMode3() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingChangeOutputModeForDiag() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingChangeProcessAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingCheckCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingControlHdcpEncryption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDebugAddCallbacks() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDebugClearDiagCommand() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDebugSetDiagState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDebugSetHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDebugSetProcessAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDriverChangeConnectionStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingDriverUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingEnterAudioMuteForShutdown() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetCurrentDeviceInfo_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetCurrentHdmiDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetCurrentOutputMode2_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetCurrentOutputMode_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetDetailedHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetHdmiConnectDisconnectNum() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetHdmiKsvList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetHdmiRawEdid() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetMorpheusPuStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetNativeHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingGetRawHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsSupportedAudioOutModeByHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsSupportedHdcpVersionByHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsSupportedVideoOutModeByHdmiMonitorInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsSuspendedProcessOutputModeAvailable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsSuspendedProcessOutputModeAvailable2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingIsVrSupportedByHdmiMonitorInfo2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingNotifyAudioOutMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingNotifyResponseForSocialScreen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingNotifyUmdEvent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingNotifyVideoOutMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingRemoveCallbacks() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingRemoveProcessOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSet2dVrMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetAudioOutModeAny_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetAudioOutModeInvalid_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetAvOutputMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetDispclk() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetDisplayBlackLevel() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetHdcpMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetHdcpStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetHdmiGamutMetadata_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetVideoOutModeAny_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetVideoOutModeInvalid_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetVideoOutSource() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSetVideoOutputColorEffect() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingSimulateProcessOutputModeArbitration_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingTerm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAvSettingWaitCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAvSetting/include/Api.hpp b/core/libs/prx/libSceAvSetting/include/Api.hpp index 6ce470cc12..d038e47803 100644 --- a/core/libs/prx/libSceAvSetting/include/Api.hpp +++ b/core/libs/prx/libSceAvSetting/include/Api.hpp @@ -28,5 +28,109 @@ int APS5_VABI sceAvControlCheckVideoOutMode(void); int APS5_VABI sceAvControlSetCheckoutputMode(void); int APS5_VABI sceAvControlSetSystemVideoMode(void); int APS5_VABI sceAvControlDebugAddCallbacks(void); +int APS5_VABI sceAvControlChangeOutputLayout(void); +int APS5_VABI sceAvControlChangeOutputMode(void); +int APS5_VABI sceAvControlChangeOutputModeForDiag(void); +int APS5_VABI sceAvControlChangeProcessAttribute(void); +int APS5_VABI sceAvControlChangeScanInMode(void); +int APS5_VABI sceAvControlGetCurrentDeviceId(void); +int APS5_VABI sceAvControlGetCurrentOutputMode(void); +int APS5_VABI sceAvControlGetCurrentOutputModeWithSync(void); +int APS5_VABI sceAvControlGetDetailHdcpStatus(void); +int APS5_VABI sceAvControlGetDeviceInfo(void); +int APS5_VABI sceAvControlGetHdcpStatus(void); +int APS5_VABI sceAvControlGetMonitorInfo(void); +int APS5_VABI sceAvControlGetNativeMonitorInfo(void); +int APS5_VABI sceAvControlGetPuStatus(void); +int APS5_VABI sceAvControlGetPuType(void); +int APS5_VABI sceAvControlInit(void); +int APS5_VABI sceAvControlIsAudioOutModeSupportedByMonitorInfo(void); +int APS5_VABI sceAvControlIsColorimetryHdr(void); +int APS5_VABI sceAvControlIsDeepColorSupportedByMonitorInfo(void); +int APS5_VABI sceAvControlIsDeviceConnected(void); +int APS5_VABI sceAvControlIsHdrSupportedByMonitorInfo(void); +int APS5_VABI sceAvControlIsModeGen1Vr(void); +int APS5_VABI sceAvControlIsModeSystemDefault(void); +int APS5_VABI sceAvControlIsSuspendedProcessOutputModeAvailable(void); +int APS5_VABI sceAvControlIsVideoOutModeSupportedByMonitorInfo(void); +int APS5_VABI sceAvControlSet2dVrMode(void); +int APS5_VABI sceAvControlSetAudioMuteForShutdown(void); +int APS5_VABI sceAvControlSetAvOutputMode(void); +int APS5_VABI sceAvControlSetColorEffect(void); +int APS5_VABI sceAvControlSetDefaultAudioOutMode(void); +int APS5_VABI sceAvControlSetDefaultVideoOutMode(void); +int APS5_VABI sceAvControlSetDispclk(void); +int APS5_VABI sceAvControlSetGamutMetadata(void); +int APS5_VABI sceAvControlSetHdcpEncryption(void); +int APS5_VABI sceAvControlSetHdcpStatus(void); +int APS5_VABI sceAvControlSetHdmiGamutMetadata(void); +int APS5_VABI sceAvControlSetHdrMetadata(void); +int APS5_VABI sceAvControlSetVideoOutSource(void); +int APS5_VABI sceAvControlSimulateProcessOutputModeArbitration(void); +int APS5_VABI sceAvSettingAddCallbacks(void); +int APS5_VABI sceAvSettingAddCallbacksForLnc(void); +int APS5_VABI sceAvSettingAddCallbacksForSocialScreen(void); +int APS5_VABI sceAvSettingCallbackFuncsInit(void); +int APS5_VABI sceAvSettingCallbackFuncsInitForLnc(void); +int APS5_VABI sceAvSettingCallbackFuncsInitForLnc_(void); +int APS5_VABI sceAvSettingCallbackFuncsInit_(void); +int APS5_VABI sceAvSettingChangeOutputMode(void); +int APS5_VABI sceAvSettingChangeOutputMode2(void); +int APS5_VABI sceAvSettingChangeOutputMode3(void); +int APS5_VABI sceAvSettingChangeOutputModeForDiag(void); +int APS5_VABI sceAvSettingChangeProcessAttribute(void); +int APS5_VABI sceAvSettingCheckCallback(void); +int APS5_VABI sceAvSettingControlHdcpEncryption(void); +int APS5_VABI sceAvSettingDebugAddCallbacks(void); +int APS5_VABI sceAvSettingDebugClearDiagCommand(void); +int APS5_VABI sceAvSettingDebugSetDiagState(void); +int APS5_VABI sceAvSettingDebugSetHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingDebugSetProcessAttribute(void); +int APS5_VABI sceAvSettingDriverChangeConnectionStatus(void); +int APS5_VABI sceAvSettingDriverUpdateStatus(void); +int APS5_VABI sceAvSettingEnterAudioMuteForShutdown(void); +int APS5_VABI sceAvSettingGetCurrentDeviceInfo_(void); +int APS5_VABI sceAvSettingGetCurrentHdmiDeviceId(void); +int APS5_VABI sceAvSettingGetCurrentOutputMode2_(void); +int APS5_VABI sceAvSettingGetCurrentOutputMode_(void); +int APS5_VABI sceAvSettingGetDetailedHdcpStatus(void); +int APS5_VABI sceAvSettingGetDeviceInfo(void); +int APS5_VABI sceAvSettingGetHdcpStatus(void); +int APS5_VABI sceAvSettingGetHdmiConnectDisconnectNum(void); +int APS5_VABI sceAvSettingGetHdmiKsvList(void); +int APS5_VABI sceAvSettingGetHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingGetHdmiRawEdid(void); +int APS5_VABI sceAvSettingGetMorpheusPuStatus(void); +int APS5_VABI sceAvSettingGetNativeHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingGetRawHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingInit(void); +int APS5_VABI sceAvSettingIsSupportedAudioOutModeByHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingIsSupportedHdcpVersionByHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingIsSupportedVideoOutModeByHdmiMonitorInfo(void); +int APS5_VABI sceAvSettingIsSuspendedProcessOutputModeAvailable(void); +int APS5_VABI sceAvSettingIsSuspendedProcessOutputModeAvailable2(void); +int APS5_VABI sceAvSettingIsVrSupportedByHdmiMonitorInfo2(void); +int APS5_VABI sceAvSettingNotifyAudioOutMode(void); +int APS5_VABI sceAvSettingNotifyResponseForSocialScreen(void); +int APS5_VABI sceAvSettingNotifyUmdEvent(void); +int APS5_VABI sceAvSettingNotifyVideoOutMode(void); +int APS5_VABI sceAvSettingRemoveCallbacks(void); +int APS5_VABI sceAvSettingRemoveProcessOutputMode(void); +int APS5_VABI sceAvSettingSet2dVrMode(void); +int APS5_VABI sceAvSettingSetAudioOutModeAny_(void); +int APS5_VABI sceAvSettingSetAudioOutModeInvalid_(void); +int APS5_VABI sceAvSettingSetAvOutputMode(void); +int APS5_VABI sceAvSettingSetDispclk(void); +int APS5_VABI sceAvSettingSetDisplayBlackLevel(void); +int APS5_VABI sceAvSettingSetHdcpMode(void); +int APS5_VABI sceAvSettingSetHdcpStatus(void); +int APS5_VABI sceAvSettingSetHdmiGamutMetadata_(void); +int APS5_VABI sceAvSettingSetVideoOutModeAny_(void); +int APS5_VABI sceAvSettingSetVideoOutModeInvalid_(void); +int APS5_VABI sceAvSettingSetVideoOutSource(void); +int APS5_VABI sceAvSettingSetVideoOutputColorEffect(void); +int APS5_VABI sceAvSettingSimulateProcessOutputModeArbitration_(void); +int APS5_VABI sceAvSettingTerm(void); +int APS5_VABI sceAvSettingWaitCallback(void); } diff --git a/core/libs/prx/libSceBluetoothHid/Export.cpp b/core/libs/prx/libSceBluetoothHid/Export.cpp index e1e00be946..61bcb98b50 100644 --- a/core/libs/prx/libSceBluetoothHid/Export.cpp +++ b/core/libs/prx/libSceBluetoothHid/Export.cpp @@ -4,6 +4,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include #include #include @@ -118,7 +119,9 @@ int APS5_VABI sceBluetoothHidInterruptOutput(std::uint64_t deviceHandle, std::ui } int APS5_VABI sceBluetoothHidParamInitialize(BluetoothHidParam* param, const BluetoothHidThreadParam* threadParam) { - NotImplemented_nid_no_patch(__func__); + if (param == nullptr) return InvalidArgument; + param->threadParam = threadParam; + param->reserved = 0; return 0; } diff --git a/core/libs/prx/libSceCdlgUtilServer/Export.cpp b/core/libs/prx/libSceCdlgUtilServer/Export.cpp index d81f4a5e32..25cb1446d7 100644 --- a/core/libs/prx/libSceCdlgUtilServer/Export.cpp +++ b/core/libs/prx/libSceCdlgUtilServer/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceCdlgUtilServer/include/Api.hpp" #include #include #include "SceTypes.hpp" @@ -5,6 +6,59 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI _ZN3sce16CommonDialogUtil6Server13setFinishDataEPvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6Server6setRunEPvm() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6Server9setFinishEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6Server9setResultEi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerC2EPKcmmis() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD0Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server10getVersionERj() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server10isCloseReqEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server14getServerStateEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceCdlgUtilServer/include/Api.hpp b/core/libs/prx/libSceCdlgUtilServer/include/Api.hpp new file mode 100644 index 0000000000..9b327a4d20 --- /dev/null +++ b/core/libs/prx/libSceCdlgUtilServer/include/Api.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZN3sce16CommonDialogUtil6Server13setFinishDataEPvm(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6Server6setRunEPvm(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6Server9setFinishEv(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6Server9setResultEi(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerC2EPKcmmis(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD0Ev(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD1Ev(void); +int APS5_VABI _ZN3sce16CommonDialogUtil6ServerD2Ev(void); +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server10getVersionERj(void); +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server10isCloseReqEv(void); +int APS5_VABI _ZNK3sce16CommonDialogUtil6Server14getServerStateEv(void); + +} diff --git a/core/libs/prx/libSceCdlgUtilServer/libSceCdlgUtilServer.hpp b/core/libs/prx/libSceCdlgUtilServer/libSceCdlgUtilServer.hpp index 6f70f09bee..29b94d8e37 100644 --- a/core/libs/prx/libSceCdlgUtilServer/libSceCdlgUtilServer.hpp +++ b/core/libs/prx/libSceCdlgUtilServer/libSceCdlgUtilServer.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceCdlgUtilServer/include/Api.hpp" diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index d36d477c1f..5b02cb28c7 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -371,11 +371,17 @@ constexpr uint32_t GB_CELLS = 126 * 190; struct Gb18030Tables { std::span twoByte; std::span fourByte; - std::span fallback; + bool edition2005; }; -constexpr Gb18030Tables GB18030_2000{Gb18030::TWO_BYTE_2000, Gb18030::FOUR_BYTE_2000, {}}; -constexpr Gb18030Tables GB18030_2005{Gb18030::TWO_BYTE_2005, Gb18030::FOUR_BYTE_2005, Gb18030::FALLBACK_2005}; +constexpr Gb18030Tables GB18030_2000{Gb18030::TWO_BYTE_2000, Gb18030::FOUR_BYTE_2000, false}; +constexpr Gb18030Tables GB18030_2005{Gb18030::TWO_BYTE_2000, Gb18030::FOUR_BYTE_2000, true}; +constexpr uint32_t GB18030_SWAPPED_CELL = (0xA8 - 0x81) * 190 + (0xBC - 0x41); +constexpr uint32_t GB18030_SWAPPED_LINEAR = (5 * 126 + (0xF4 - 0x81)) * 10 + 7; + +bool mapsGb18030Supplementary(const Gb18030Tables& tables, uint32_t code) { + return tables.edition2005 && code >= 0x20000 && code <= 0x2FFFF; +} const Gb18030Tables* gb18030Tables(const void* profile) { if (profile == nullptr) return nullptr; @@ -400,10 +406,20 @@ const uint32_t* findRun(std::span runs, uint32_t key) { } uint32_t gb18030Cell(const Gb18030Tables& tables, uint32_t cell) { + if (tables.edition2005 && cell == GB18030_SWAPPED_CELL) return 0x1E3F; const uint32_t* run = findRun(tables.twoByte, cell); return run ? run[1] + cell - run[0] : Cp936::TO_UCS2[cell]; } +void putGbLinear(uint32_t linear, uint8_t* mbcs) { + mbcs[3] = static_cast(0x30 + linear % 10); + linear /= 10; + mbcs[2] = static_cast(0x81 + linear % 126); + linear /= 126; + mbcs[1] = static_cast(0x30 + linear % 10); + mbcs[0] = static_cast(0x81 + linear / 10); +} + void putGbCell(uint32_t cell, uint8_t* mbcs) { const uint32_t column = cell % 190; mbcs[0] = static_cast(0x81 + cell / 190); @@ -427,8 +443,9 @@ int decodeGb18030(const Gb18030Tables& tables, const uint8_t* mbcs, uint32_t mbc const uint32_t linear = (((lead - 0x81) * 10 + (second - 0x30)) * 126 + (mbcs[2] - 0x81)) * 10 + (mbcs[3] - 0x30); const uint32_t* run = findRun(tables.fourByte, linear); if (!run) return unassigned(4, length); + code = tables.edition2005 && linear == GB18030_SWAPPED_LINEAR ? 0xE7C7 : run[1] + linear - run[0]; + if (code > 0xFFFF && !mapsGb18030Supplementary(tables, code)) return unassigned(4, length); length = 4; - code = run[1] + linear - run[0]; return 0; } if (second < 0x40 || second == 0x7F || second == 0xFF) return rejectTrail(second, 2, length); @@ -463,22 +480,16 @@ int encodeGb18030(const Gb18030Tables& tables, uint32_t code, uint8_t* mbcs, uin length = 2; return 0; } - for (const auto& run : tables.fourByte) { - if (code - run[1] >= run[2]) continue; - uint32_t linear = run[0] + code - run[1]; - mbcs[3] = static_cast(0x30 + linear % 10); - linear /= 10; - mbcs[2] = static_cast(0x81 + linear % 126); - linear /= 126; - mbcs[1] = static_cast(0x30 + linear % 10); - mbcs[0] = static_cast(0x81 + linear / 10); + if (code > 0xFFFF && !mapsGb18030Supplementary(tables, code)) return CES_ERROR_UNASSIGNED_CODE; + if (tables.edition2005 && code == 0xE7C7) { + putGbLinear(GB18030_SWAPPED_LINEAR, mbcs); length = 4; return 0; } - for (const auto& entry : tables.fallback) { - if (entry[0] != code) continue; - putGbCell(entry[1], mbcs); - length = 2; + for (const auto& run : tables.fourByte) { + if (code - run[1] >= run[2]) continue; + putGbLinear(run[0] + code - run[1], mbcs); + length = 4; return 0; } return CES_ERROR_UNASSIGNED_CODE; diff --git a/core/libs/prx/libSceCesCs/Gb18030.hpp b/core/libs/prx/libSceCesCs/Gb18030.hpp index 5f13d60813..2927db6663 100644 --- a/core/libs/prx/libSceCesCs/Gb18030.hpp +++ b/core/libs/prx/libSceCesCs/Gb18030.hpp @@ -64,72 +64,4 @@ inline constexpr uint32_t FOUR_BYTE_2000[207][3] = { {0x9961, 0xFF5F, 0x81}, {0x99E2, 0xFFE6, 0x1A}, {0x2E248, 0x10000, 0x100000}, }; -inline constexpr uint32_t TWO_BYTE_2005[118][3] = { - {0x17C0, 0xE4C6, 0x60}, {0x187E, 0xE526, 0x60}, {0x18E8, 0xE766, 0x6}, {0x1920, 0x20AC, 0x1}, {0x1921, 0xE76D, 0x1}, {0x192C, 0xE76E, 0x2}, - {0x193A, 0xE770, 0x2}, {0x193C, 0xE586, 0x60}, {0x19FA, 0xE5E6, 0x60}, {0x1AAD, 0xE772, 0xB}, {0x1AB8, 0xE646, 0x60}, {0x1B6E, 0xE77D, 0x8}, - {0x1B76, 0xE6A6, 0x60}, {0x1BEE, 0xE785, 0x8}, {0x1C0E, 0xFE10, 0x1}, {0x1C0F, 0xFE12, 0x1}, {0x1C10, 0xFE11, 0x1}, {0x1C11, 0xFE13, 0x4}, - {0x1C21, 0xFE17, 0x2}, {0x1C28, 0xFE19, 0x1}, {0x1C2B, 0xE797, 0x9}, {0x1C34, 0xE706, 0x60}, {0x1CB5, 0xE7A0, 0xF}, {0x1CE5, 0xE7AF, 0xD}, - {0x1D47, 0xE7BC, 0xB}, {0x1D6D, 0x1E3F, 0x1}, {0x1D70, 0x1F9, 0x1}, {0x1D72, 0xE7C9, 0x4}, {0x1D9B, 0xE7CD, 0x15}, {0x1DC8, 0xE7E2, 0x1}, - {0x1DCB, 0xE7E3, 0x1}, {0x1DCD, 0xE7E4, 0x3}, {0x1DF8, 0x303E, 0x1}, {0x1DF9, 0x2FF0, 0xC}, {0x1E06, 0xE7F4, 0xD}, {0x1E5F, 0xE801, 0xF}, - {0x1ECE, 0xE000, 0x5E}, {0x1F8C, 0xE05E, 0x5E}, {0x204A, 0xE0BC, 0x5E}, {0x2108, 0xE11A, 0x5E}, {0x21C6, 0xE178, 0x5E}, {0x2284, 0xE1D6, 0x5E}, - {0x408D, 0xE810, 0x5}, {0x58B2, 0xE234, 0x5E}, {0x5970, 0xE292, 0x5E}, {0x5A2E, 0xE2F0, 0x5E}, {0x5AEC, 0xE34E, 0x5E}, {0x5BAA, 0xE3AC, 0x5E}, - {0x5C68, 0xE40A, 0x5E}, {0x5CD6, 0x2E81, 0x1}, {0x5CD7, 0x20087, 0x1}, {0x5CD8, 0x20089, 0x1}, {0x5CD9, 0x200CC, 0x1}, {0x5CDA, 0x2E84, 0x1}, - {0x5CDB, 0x3473, 0x1}, {0x5CDC, 0x3447, 0x1}, {0x5CDD, 0x2E88, 0x1}, {0x5CDE, 0x2E8B, 0x1}, {0x5CDF, 0x9FB4, 0x1}, {0x5CE0, 0x359E, 0x1}, - {0x5CE1, 0x361A, 0x1}, {0x5CE2, 0x360E, 0x1}, {0x5CE3, 0x2E8C, 0x1}, {0x5CE4, 0x2E97, 0x1}, {0x5CE5, 0x396E, 0x1}, {0x5CE6, 0x3918, 0x1}, - {0x5CE7, 0x9FB5, 0x1}, {0x5CE8, 0x39CF, 0x1}, {0x5CE9, 0x39DF, 0x1}, {0x5CEA, 0x3A73, 0x1}, {0x5CEB, 0x39D0, 0x1}, {0x5CEC, 0x9FB6, 0x2}, - {0x5CEE, 0x3B4E, 0x1}, {0x5CEF, 0x3C6E, 0x1}, {0x5CF0, 0x3CE0, 0x1}, {0x5CF1, 0x2EA7, 0x1}, {0x5CF2, 0x215D7, 0x1}, {0x5CF3, 0x9FB8, 0x1}, - {0x5CF4, 0x2EAA, 0x1}, {0x5CF5, 0x4056, 0x1}, {0x5CF6, 0x415F, 0x1}, {0x5CF7, 0x2EAE, 0x1}, {0x5CF8, 0x4337, 0x1}, {0x5CF9, 0x2EB3, 0x1}, - {0x5CFA, 0x2EB6, 0x2}, {0x5CFC, 0x2298F, 0x1}, {0x5CFD, 0x43B1, 0x1}, {0x5CFE, 0x43AC, 0x1}, {0x5CFF, 0x2EBB, 0x1}, {0x5D00, 0x43DD, 0x1}, - {0x5D01, 0x44D6, 0x1}, {0x5D02, 0x4661, 0x1}, {0x5D03, 0x464C, 0x1}, {0x5D04, 0x9FB9, 0x1}, {0x5D05, 0x4723, 0x1}, {0x5D06, 0x4729, 0x1}, - {0x5D07, 0x477C, 0x1}, {0x5D08, 0x478D, 0x1}, {0x5D09, 0x2ECA, 0x1}, {0x5D0A, 0x4947, 0x1}, {0x5D0B, 0x497A, 0x1}, {0x5D0C, 0x497D, 0x1}, - {0x5D0D, 0x4982, 0x2}, {0x5D0F, 0x4985, 0x2}, {0x5D11, 0x499F, 0x1}, {0x5D12, 0x499B, 0x1}, {0x5D13, 0x49B7, 0x1}, {0x5D14, 0x49B6, 0x1}, - {0x5D15, 0x9FBA, 0x1}, {0x5D16, 0x241FE, 0x1}, {0x5D17, 0x4CA3, 0x1}, {0x5D18, 0x4C9F, 0x3}, {0x5D1B, 0x4C77, 0x1}, {0x5D1C, 0x4CA2, 0x1}, - {0x5D1D, 0x4D13, 0x7}, {0x5D24, 0x4DAE, 0x1}, {0x5D25, 0x9FBB, 0x1}, {0x5D26, 0xE468, 0x5E}, -}; - -inline constexpr uint32_t FOUR_BYTE_2005[209][3] = { - {0x0, 0x80, 0x24}, {0x24, 0xA5, 0x2}, {0x26, 0xA9, 0x7}, {0x2D, 0xB2, 0x5}, {0x32, 0xB8, 0x1F}, {0x51, 0xD8, 0x8}, - {0x59, 0xE2, 0x6}, {0x5F, 0xEB, 0x1}, {0x60, 0xEE, 0x4}, {0x64, 0xF4, 0x3}, {0x67, 0xF8, 0x1}, {0x68, 0xFB, 0x1}, - {0x69, 0xFD, 0x4}, {0x6D, 0x102, 0x11}, {0x7E, 0x114, 0x7}, {0x85, 0x11C, 0xF}, {0x94, 0x12C, 0x18}, {0xAC, 0x145, 0x3}, - {0xAF, 0x149, 0x4}, {0xB3, 0x14E, 0x1D}, {0xD0, 0x16C, 0x62}, {0x132, 0x1CF, 0x1}, {0x133, 0x1D1, 0x1}, {0x134, 0x1D3, 0x1}, - {0x135, 0x1D5, 0x1}, {0x136, 0x1D7, 0x1}, {0x137, 0x1D9, 0x1}, {0x138, 0x1DB, 0x1}, {0x139, 0x1DD, 0x1C}, {0x155, 0x1FA, 0x57}, - {0x1AC, 0x252, 0xF}, {0x1BB, 0x262, 0x65}, {0x220, 0x2C8, 0x1}, {0x221, 0x2CC, 0xD}, {0x22E, 0x2DA, 0xB7}, {0x2E5, 0x3A2, 0x1}, - {0x2E6, 0x3AA, 0x7}, {0x2ED, 0x3C2, 0x1}, {0x2EE, 0x3CA, 0x37}, {0x325, 0x402, 0xE}, {0x333, 0x450, 0x1}, {0x334, 0x452, 0x19ED}, - {0x1D21, 0xE7C7, 0x1}, {0x1D22, 0x1E40, 0x1D0}, {0x1EF2, 0x2011, 0x2}, {0x1EF4, 0x2017, 0x1}, {0x1EF5, 0x201A, 0x2}, {0x1EF7, 0x201E, 0x7}, - {0x1EFE, 0x2027, 0x9}, {0x1F07, 0x2031, 0x1}, {0x1F08, 0x2034, 0x1}, {0x1F09, 0x2036, 0x5}, {0x1F0E, 0x203C, 0x70}, {0x1F7E, 0x20AD, 0x56}, - {0x1FD4, 0x2104, 0x1}, {0x1FD5, 0x2106, 0x3}, {0x1FD8, 0x210A, 0xC}, {0x1FE4, 0x2117, 0xA}, {0x1FEE, 0x2122, 0x3E}, {0x202C, 0x216C, 0x4}, - {0x2030, 0x217A, 0x16}, {0x2046, 0x2194, 0x2}, {0x2048, 0x219A, 0x6E}, {0x20B6, 0x2209, 0x6}, {0x20BC, 0x2210, 0x1}, {0x20BD, 0x2212, 0x3}, - {0x20C0, 0x2216, 0x4}, {0x20C4, 0x221B, 0x2}, {0x20C6, 0x2221, 0x2}, {0x20C8, 0x2224, 0x1}, {0x20C9, 0x2226, 0x1}, {0x20CA, 0x222C, 0x2}, - {0x20CC, 0x222F, 0x5}, {0x20D1, 0x2238, 0x5}, {0x20D6, 0x223E, 0xA}, {0x20E0, 0x2249, 0x3}, {0x20E3, 0x224D, 0x5}, {0x20E8, 0x2253, 0xD}, - {0x20F5, 0x2262, 0x2}, {0x20F7, 0x2268, 0x6}, {0x20FD, 0x2270, 0x25}, {0x2122, 0x2296, 0x3}, {0x2125, 0x229A, 0xB}, {0x2130, 0x22A6, 0x19}, - {0x2149, 0x22C0, 0x52}, {0x219B, 0x2313, 0x14D}, {0x22E8, 0x246A, 0xA}, {0x22F2, 0x249C, 0x64}, {0x2356, 0x254C, 0x4}, {0x235A, 0x2574, 0xD}, - {0x2367, 0x2590, 0x3}, {0x236A, 0x2596, 0xA}, {0x2374, 0x25A2, 0x10}, {0x2384, 0x25B4, 0x8}, {0x238C, 0x25BE, 0x8}, {0x2394, 0x25C8, 0x3}, - {0x2397, 0x25CC, 0x2}, {0x2399, 0x25D0, 0x12}, {0x23AB, 0x25E6, 0x1F}, {0x23CA, 0x2607, 0x2}, {0x23CC, 0x260A, 0x36}, {0x2402, 0x2641, 0x1}, - {0x2403, 0x2643, 0x83E}, {0x2C41, 0x2E82, 0x2}, {0x2C43, 0x2E85, 0x3}, {0x2C46, 0x2E89, 0x2}, {0x2C48, 0x2E8D, 0xA}, {0x2C52, 0x2E98, 0xF}, - {0x2C61, 0x2EA8, 0x2}, {0x2C63, 0x2EAB, 0x3}, {0x2C66, 0x2EAF, 0x4}, {0x2C6A, 0x2EB4, 0x2}, {0x2C6C, 0x2EB8, 0x3}, {0x2C6F, 0x2EBC, 0xE}, - {0x2C7D, 0x2ECB, 0x125}, {0x2DA2, 0x2FFC, 0x4}, {0x2DA6, 0x3004, 0x1}, {0x2DA7, 0x3018, 0x5}, {0x2DAC, 0x301F, 0x2}, {0x2DAE, 0x302A, 0x14}, - {0x2DC2, 0x303F, 0x2}, {0x2DC4, 0x3094, 0x7}, {0x2DCB, 0x309F, 0x2}, {0x2DCD, 0x30F7, 0x5}, {0x2DD2, 0x30FF, 0x6}, {0x2DD8, 0x312A, 0xF6}, - {0x2ECE, 0x322A, 0x7}, {0x2ED5, 0x3232, 0x71}, {0x2F46, 0x32A4, 0xEA}, {0x3030, 0x3390, 0xC}, {0x303C, 0x339F, 0x2}, {0x303E, 0x33A2, 0x22}, - {0x3060, 0x33C5, 0x9}, {0x3069, 0x33CF, 0x2}, {0x306B, 0x33D3, 0x2}, {0x306D, 0x33D6, 0x71}, {0x30DE, 0x3448, 0x2B}, {0x3109, 0x3474, 0x12A}, - {0x3233, 0x359F, 0x6F}, {0x32A2, 0x360F, 0xB}, {0x32AD, 0x361B, 0x2FD}, {0x35AA, 0x3919, 0x55}, {0x35FF, 0x396F, 0x60}, {0x365F, 0x39D1, 0xE}, - {0x366D, 0x39E0, 0x93}, {0x3700, 0x3A74, 0xDA}, {0x37DA, 0x3B4F, 0x11F}, {0x38F9, 0x3C6F, 0x71}, {0x396A, 0x3CE1, 0x375}, {0x3CDF, 0x4057, 0x108}, - {0x3DE7, 0x4160, 0x1D7}, {0x3FBE, 0x4338, 0x74}, {0x4032, 0x43AD, 0x4}, {0x4036, 0x43B2, 0x2B}, {0x4061, 0x43DE, 0xF8}, {0x4159, 0x44D7, 0x175}, - {0x42CE, 0x464D, 0x14}, {0x42E2, 0x4662, 0xC1}, {0x43A3, 0x4724, 0x5}, {0x43A8, 0x472A, 0x52}, {0x43FA, 0x477D, 0x10}, {0x440A, 0x478E, 0x1B9}, - {0x45C3, 0x4948, 0x32}, {0x45F5, 0x497B, 0x2}, {0x45F7, 0x497E, 0x4}, {0x45FB, 0x4984, 0x1}, {0x45FC, 0x4987, 0x14}, {0x4610, 0x499C, 0x3}, - {0x4613, 0x49A0, 0x16}, {0x4629, 0x49B8, 0x2BF}, {0x48E8, 0x4C78, 0x27}, {0x490F, 0x4CA4, 0x6F}, {0x497E, 0x4D1A, 0x94}, {0x4A12, 0x4DAF, 0x51}, - {0x4A63, 0x9FA6, 0x385A}, {0x82BD, 0xE76C, 0x1}, {0x82BE, 0xE7C8, 0x1}, {0x82BF, 0xE7E7, 0xD}, {0x82CC, 0xE815, 0x1}, {0x82CD, 0xE819, 0x5}, - {0x82D2, 0xE81F, 0x7}, {0x82D9, 0xE827, 0x4}, {0x82DD, 0xE82D, 0x4}, {0x82E1, 0xE833, 0x8}, {0x82E9, 0xE83C, 0x7}, {0x82F0, 0xE844, 0x10}, - {0x8300, 0xE856, 0xE}, {0x830E, 0xE865, 0x10C7}, {0x93D5, 0xF92D, 0x4C}, {0x9421, 0xF97A, 0x1B}, {0x943C, 0xF996, 0x51}, {0x948D, 0xF9E8, 0x9}, - {0x9496, 0xF9F2, 0x1A}, {0x94B0, 0xFA10, 0x1}, {0x94B1, 0xFA12, 0x1}, {0x94B2, 0xFA15, 0x3}, {0x94B5, 0xFA19, 0x6}, {0x94BB, 0xFA22, 0x1}, - {0x94BC, 0xFA25, 0x2}, {0x94BE, 0xFA2A, 0x406}, {0x98C4, 0xFE32, 0x1}, {0x98C5, 0xFE45, 0x4}, {0x98C9, 0xFE53, 0x1}, {0x98CA, 0xFE58, 0x1}, - {0x98CB, 0xFE67, 0x1}, {0x98CC, 0xFE6C, 0x95}, {0x9961, 0xFF5F, 0x81}, {0x99E2, 0xFFE6, 0x1A}, {0x2E248, 0x10000, 0x100000}, -}; - -inline constexpr uint32_t FALLBACK_2005[24][2] = { - {0xE78D, 0x1C0E}, {0xE78E, 0x1C0F}, {0xE78F, 0x1C10}, {0xE790, 0x1C11}, {0xE791, 0x1C12}, {0xE792, 0x1C13}, - {0xE793, 0x1C14}, {0xE794, 0x1C21}, {0xE795, 0x1C22}, {0xE796, 0x1C28}, {0xE816, 0x5CD7}, {0xE817, 0x5CD8}, - {0xE818, 0x5CD9}, {0xE81E, 0x5CDF}, {0xE826, 0x5CE7}, {0xE82B, 0x5CEC}, {0xE82C, 0x5CED}, {0xE831, 0x5CF2}, - {0xE832, 0x5CF3}, {0xE83B, 0x5CFC}, {0xE843, 0x5D04}, {0xE854, 0x5D15}, {0xE855, 0x5D16}, {0xE864, 0x5D25}, -}; - } diff --git a/core/libs/prx/libSceCommonDialog/Export.cpp b/core/libs/prx/libSceCommonDialog/Export.cpp index 8d090d7d35..7f5c42a321 100644 --- a/core/libs/prx/libSceCommonDialog/Export.cpp +++ b/core/libs/prx/libSceCommonDialog/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceCommonDialog/include/Api.hpp" +#include "prx/libSceCommonDialog/include/DialogUse.hpp" #include #include #include @@ -7,6 +8,7 @@ #include "prx/libc/include/General.hpp" static std::atomic g_initialized{false}; +static std::atomic g_used{false}; extern "C" { @@ -19,7 +21,20 @@ int APS5_VABI sceCommonDialogInitialize(void) { } bool APS5_VABI sceCommonDialogIsUsed(void) { - return false; + return g_used.load(); +} + +bool CommonDialogSystemInitialized_nid_no_patch() { + return g_initialized.load(); +} + +bool CommonDialogAcquire_nid_no_patch() { + bool expected = false; + return g_used.compare_exchange_strong(expected, true); +} + +void CommonDialogRelease_nid_no_patch() { + g_used.store(false); } } diff --git a/core/libs/prx/libSceCommonDialog/include/DialogUse.hpp b/core/libs/prx/libSceCommonDialog/include/DialogUse.hpp new file mode 100644 index 0000000000..19b3e4779b --- /dev/null +++ b/core/libs/prx/libSceCommonDialog/include/DialogUse.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBSCECOMMONDIALOG_INCLUDE_DIALOGUSE_HPP +#define CORE_LIBS_PRX_LIBSCECOMMONDIALOG_INCLUDE_DIALOGUSE_HPP + +extern "C" bool CommonDialogSystemInitialized_nid_no_patch(); +extern "C" bool CommonDialogAcquire_nid_no_patch(); +extern "C" void CommonDialogRelease_nid_no_patch(); + +#endif diff --git a/core/libs/prx/libSceContentDelete/Export.cpp b/core/libs/prx/libSceContentDelete/Export.cpp index ecd7c66372..f0e72c1b2e 100644 --- a/core/libs/prx/libSceContentDelete/Export.cpp +++ b/core/libs/prx/libSceContentDelete/Export.cpp @@ -28,12 +28,14 @@ int APS5_VABI sceContentDeleteByPath(const char* contentPath) { if (!g_initialized.load()) return NotInitialized; if (contentPath == nullptr) APS5_INVALID_ARG_EX; if (*contentPath == 0) return InvalidArgument; - throw std::logic_error(std::string(__func__) + ": " + contentPath + " is not in the host content library, which is empty, and the console's error for it is unknown"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentDeleteById(std::int64_t contentId) { if (!g_initialized.load()) return NotInitialized; - throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceContentDeleteByIdListForSf(const std::int64_t* contentIds, std::size_t count) { diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index da4a70b28a..a4ff9e01ef 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -121,6 +121,9 @@ int StyleStateGetSlantRatio(const StyleStateBlock* style, float* slantRatio); int StyleStateSetWeightScale(StyleStateBlock* style, float weightXScale, float weightYScale); int StyleStateGetWeightScale(const StyleStateBlock* style, float* weightXScale, float* weightYScale, std::uint32_t* mode); bool ValidStyleFrame(const FontStyleFrame* frame); +const FontStyleFrame* SurfaceStyleFrame(const FontRenderSurface* surface); +const StyleStateBlock* ApplyStyleFrame(const StyleStateBlock* base, const FontStyleFrame* frame, StyleStateBlock& temporary); +int RenderCodeWithStyle(FontHandle handle, std::uint32_t code, const StyleStateBlock* style, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); std::uint8_t CachedStyleCacheFlags(const CachedStyle& style); void CachedStyleSetCacheFlags(CachedStyle& style, std::uint8_t flags); diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index d297840675..d52a854c55 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -68,28 +68,7 @@ void ClearVerticalLayout(std::uint8_t* outWords) { } const StyleStateBlock* ResolveSurfaceStyleState(const StyleStateBlock* cachedState, const FontRenderSurface* surface, StyleStateBlock& temporary) { - const auto* systemUse = reinterpret_cast(surface->reserved_q); - if ((surface->styleFlag & 0x1) == 0 || !ValidStyleFrame(systemUse->styleframe)) return cachedState; - const FontStyleFrame* frame = systemUse->styleframe; - temporary = *cachedState; - if ((frame->flags1 & STYLE_FRAME_FLAG_SCALE) != 0) { - temporary.scale_unit = frame->scaleUnit; - temporary.scale_w = frame->scalePixelW; - temporary.scale_h = frame->scalePixelH; - temporary.dpi_x = frame->hDpi ? frame->hDpi : 0x48; - temporary.dpi_y = frame->vDpi ? frame->vDpi : 0x48; - } else { - const std::uint32_t wantX = frame->hDpi == 0 ? temporary.dpi_x : frame->hDpi; - const std::uint32_t wantY = frame->vDpi == 0 ? temporary.dpi_y : frame->vDpi; - temporary.dpi_x = wantX != 0 ? 0x48 : 0; - temporary.dpi_y = wantY != 0 ? 0x48 : 0; - } - if ((frame->flags1 & STYLE_FRAME_FLAG_SLANT) != 0) temporary.slant_ratio = frame->slantRatio; - if ((frame->flags1 & STYLE_FRAME_FLAG_WEIGHT) != 0) { - temporary.effect_weight_x = frame->effectWeightX; - temporary.effect_weight_y = frame->effectWeightY; - } - return &temporary; + return ApplyStyleFrame(cachedState, SurfaceStyleFrame(surface), temporary); } void FillImageMetrics(FontRenderOutput* result, const FontGlyphMetrics* metrics, float x, float y) { @@ -440,6 +419,35 @@ int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMe return finish(SCE_FONT_OK); } +const FontStyleFrame* Font::SurfaceStyleFrame(const FontRenderSurface* surface) { + const auto* systemUse = reinterpret_cast(surface->reserved_q); + if ((surface->styleFlag & 0x1) == 0 || !ValidStyleFrame(systemUse->styleframe)) return nullptr; + return systemUse->styleframe; +} + +const StyleStateBlock* Font::ApplyStyleFrame(const StyleStateBlock* base, const FontStyleFrame* frame, StyleStateBlock& temporary) { + if (!frame) return base; + temporary = *base; + if ((frame->flags1 & STYLE_FRAME_FLAG_SCALE) != 0) { + temporary.scale_unit = frame->scaleUnit; + temporary.scale_w = frame->scalePixelW; + temporary.scale_h = frame->scalePixelH; + temporary.dpi_x = frame->hDpi ? frame->hDpi : 0x48; + temporary.dpi_y = frame->vDpi ? frame->vDpi : 0x48; + } else { + const std::uint32_t wantX = frame->hDpi == 0 ? temporary.dpi_x : frame->hDpi; + const std::uint32_t wantY = frame->vDpi == 0 ? temporary.dpi_y : frame->vDpi; + temporary.dpi_x = wantX != 0 ? 0x48 : 0; + temporary.dpi_y = wantY != 0 ? 0x48 : 0; + } + if ((frame->flags1 & STYLE_FRAME_FLAG_SLANT) != 0) temporary.slant_ratio = frame->slantRatio; + if ((frame->flags1 & STYLE_FRAME_FLAG_WEIGHT) != 0) { + temporary.effect_weight_x = frame->effectWeightX; + temporary.effect_weight_y = frame->effectWeightY; + } + return &temporary; +} + int Font::RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result) { const auto fail = [&](int rc) { ClearRenderOutputs(metrics, result); @@ -451,13 +459,21 @@ int Font::RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRe auto* font = GetNativeFont(handle); if (font->magic != HANDLE_MAGIC) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); if (!font->renderer) return fail(SCE_FONT_ERROR_NOT_BOUND_RENDERER); + StyleStateBlock surfaceStyle{}; + return RenderCodeWithStyle(handle, code, ResolveSurfaceStyleState(&font->cached_style.state, surface, surfaceStyle), surface, x, y, metrics, result); +} + +int Font::RenderCodeWithStyle(FontHandle handle, std::uint32_t code, const StyleStateBlock* style, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result) { + const auto fail = [&](int rc) { + ClearRenderOutputs(metrics, result); + return rc; + }; + auto* font = GetNativeFont(handle); auto* library = static_cast(font->library); if (!library || library->magic != LIBRARY_MAGIC) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); const SysDriver* driver = library->sys_driver; if (!driver || !driver->glyph_index || !driver->set_char_with_dpi || !driver->set_char_default_dpi) return fail(SCE_FONT_ERROR_FATAL); const std::uint32_t modeLow = font->flags & 0x0Fu; - StyleStateBlock surfaceStyle{}; - const StyleStateBlock* style = ResolveSurfaceStyleState(&font->cached_style.state, surface, surfaceStyle); FontObj* obj = nullptr; std::uint32_t lockWord = 0; auto* entry = AcquireFontCtxEntry(FontContext(library, font), font->open_info.ctx_entry_index, modeLow, &obj, &lockWord); diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 0647f2192e..bdc370105e 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -13,6 +13,7 @@ #include FT_FREETYPE_H #include "prx/libSceFont/include/FontInternal.hpp" +#include "prx/libc/include/General.hpp" namespace { @@ -121,6 +122,45 @@ int RenderDirectional(FontHandle fontHandle, std::uint32_t code, FontRenderSurfa return rc; } +int RenderGlyph(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result, std::uint16_t direction) { + const auto fail = [&](int rc) { + ClearRenderOutputs(metrics, result); + return rc; + }; + auto* generated = TryGetGeneratedGlyph(glyph); + if (!generated) return fail(SCE_FONT_ERROR_INVALID_GLYPH); + if (!renderer || static_cast(renderer)->magic != RENDERER_MAGIC) return fail(SCE_FONT_ERROR_INVALID_RENDERER); + if (!surface || !metrics || !result) return fail(SCE_FONT_ERROR_INVALID_PARAMETER); + if (direction == 2) NotImplemented_nid_no_patch("sceFontGlyphRenderImageVertical"); + const FontHandle owner = generated->owner; + auto* font = TryGetState(owner) ? GetNativeFont(owner) : nullptr; + std::uint32_t fontLock = 0; + if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + StyleStateBlock glyphStyle{}; + glyphStyle.scale_w = generated->glyph.scale_x; + glyphStyle.scale_h = generated->glyph.base_scale; + StyleStateBlock framed{}; + const FontStyleFrame* surfaceFrame = SurfaceStyleFrame(surface); + const StyleStateBlock* style = ApplyStyleFrame(&glyphStyle, surfaceFrame ? surfaceFrame : styleFrame, framed); + float yUsed = y; + int rc = SCE_FONT_OK; + if (direction == 0) { + if (static_cast(font->flags) < 0) { + ReleaseFontLock(font, fontLock); + NotImplemented_nid_no_patch("sceFontGlyphRenderImage: a vertical font"); + } + std::uint8_t layout[HORIZONTAL_LAYOUT_SIZE] = {}; + rc = ComputeHorizontalLayout(owner, style, layout); + yUsed = y + LoadFloat(layout, HORIZONTAL_BASELINE); + direction = 1; + } + CachedStyleSetDirectionWord(font->cached_style, direction); + if (rc == SCE_FONT_OK) rc = RenderCodeWithStyle(owner, generated->codepoint, style, surface, x, yUsed, metrics, result); + ReleaseFontLock(font, fontLock); + if (rc != SCE_FONT_OK) ClearRenderOutputs(metrics, result); + return rc; +} + } #pragma GCC visibility push(default) @@ -559,6 +599,18 @@ FontGlyphOutline* APS5_VABI sceFontGlyphRefersOutline(FontGlyph glyph) { return &generated->outline; } +int APS5_VABI sceFontGlyphRenderImage(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { + return RenderGlyph(glyph, styleFrame, renderer, surface, x, y, metrics, output, 0); +} + +int APS5_VABI sceFontGlyphRenderImageHorizontal(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { + return RenderGlyph(glyph, styleFrame, renderer, surface, x, y, metrics, output, 1); +} + +int APS5_VABI sceFontGlyphRenderImageVertical(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { + return RenderGlyph(glyph, styleFrame, renderer, surface, x, y, metrics, output, 2); +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 2ff7bd7da3..fa5b689e1e 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -106,21 +106,6 @@ int APS5_VABI sceFontGetGlyphExpandBufferState(FontHandle font, FontGlyphExpandB return 0; } -int APS5_VABI sceFontGlyphRenderImage(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGlyphRenderImageHorizontal(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGlyphRenderImageVertical(FontGlyph glyph, const FontStyleFrame* styleFrame, FontRenderer renderer, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* output) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGraphicsBeginFrame(FontGraphicsFrame* frame, std::uint64_t flags, FontGraphicsResource** resource) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceFreeType/CMakeLists.txt b/core/libs/prx/libSceFreeType/CMakeLists.txt index fb0affb25c..9682b1e0b3 100644 --- a/core/libs/prx/libSceFreeType/CMakeLists.txt +++ b/core/libs/prx/libSceFreeType/CMakeLists.txt @@ -31,6 +31,18 @@ if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") endif() configure_windows_unwind(libSceFreeType) +target_sources(libSceFreeType PRIVATE src/Sfnt.cpp) +if(BUILD_TESTING) + add_test_executable(guest_freetype_sfnt_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeSfnt.cpp) + target_include_directories(guest_freetype_sfnt_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_sfnt_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_sfnt_tests) + add_test(NAME guest_freetype_sfnt COMMAND guest_freetype_sfnt_tests) + if(WIN32) + set_tests_properties(guest_freetype_sfnt PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() if(BUILD_TESTING) add_test_executable(guest_freetype_bitmap_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeBitmap.cpp) target_include_directories(guest_freetype_bitmap_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -67,6 +79,15 @@ if(BUILD_TESTING) target_link_libraries(guest_freetype_face_info_tests PRIVATE libSceFreeType freetype libc) configure_windows_unwind(guest_freetype_face_info_tests) add_test(NAME guest_freetype_face_info COMMAND guest_freetype_face_info_tests) + add_test_executable(guest_freetype_bitmap_init_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeBitmapInit.cpp) + target_include_directories(guest_freetype_bitmap_init_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_bitmap_init_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_bitmap_init_tests) + add_test(NAME guest_freetype_bitmap_init COMMAND guest_freetype_bitmap_init_tests) + if(WIN32) + set_tests_properties(guest_freetype_bitmap_init PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_freetype_charmap_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCharmap.cpp) target_include_directories(guest_freetype_charmap_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_freetype_charmap_tests PRIVATE libSceFreeType freetype libc) @@ -114,3 +135,5 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +include("${CMAKE_CURRENT_LIST_DIR}/ModuleProperties.cmake") diff --git a/core/libs/prx/libSceFreeType/CurveBounds.cmake b/core/libs/prx/libSceFreeType/CurveBounds.cmake new file mode 100644 index 0000000000..462dec7223 --- /dev/null +++ b/core/libs/prx/libSceFreeType/CurveBounds.cmake @@ -0,0 +1,13 @@ +target_sources(libSceFreeType PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/CurveBounds.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_curve_bounds_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeCurveBounds.cpp) + target_include_directories(guest_freetype_curve_bounds_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_curve_bounds_tests PRIVATE libSceFreeType freetype libc) + configure_windows_unwind(guest_freetype_curve_bounds_tests) + add_test(NAME guest_freetype_curve_bounds COMMAND guest_freetype_curve_bounds_tests) + if(WIN32) + set_tests_properties(guest_freetype_curve_bounds PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceFreeType/ModuleProperties.cmake b/core/libs/prx/libSceFreeType/ModuleProperties.cmake new file mode 100644 index 0000000000..b4ff4952da --- /dev/null +++ b/core/libs/prx/libSceFreeType/ModuleProperties.cmake @@ -0,0 +1,21 @@ +target_sources(libSceFreeType PRIVATE ${LIBS_INCLUDE_DIR}/prx/libSceFreeType/src/ModuleProperties.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_freetype_module_properties_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeModuleProperties.cpp) + target_include_directories(guest_freetype_module_properties_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_module_properties_tests PRIVATE freetype ${CMAKE_DL_LIBS}) + if(WIN32) + target_link_libraries(guest_freetype_module_properties_tests PRIVATE libc) + endif() + configure_windows_unwind(guest_freetype_module_properties_tests) + add_dependencies(guest_freetype_module_properties_tests libs) + add_test(NAME guest_freetype_module_properties COMMAND guest_freetype_module_properties_tests $ unpatched) + add_test(NAME guest_freetype_module_properties_nid COMMAND guest_freetype_module_properties_tests ${PATCHED_DIR}/libSceFreeType.prx patched) + set_tests_properties(guest_freetype_module_properties guest_freetype_module_properties_nid PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_module_properties PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_freetype_module_properties_nid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") + endif() +endif() diff --git a/core/libs/prx/libSceFreeType/include/Api.hpp b/core/libs/prx/libSceFreeType/include/Api.hpp index 926be2864a..048fa59647 100644 --- a/core/libs/prx/libSceFreeType/include/Api.hpp +++ b/core/libs/prx/libSceFreeType/include/Api.hpp @@ -58,13 +58,22 @@ std::uint32_t APS5_VABI FT_Get_Name_Index_nid_postfix(FreeType::GuestFaceRec* fa const char* APS5_VABI FT_Get_Postscript_Name_nid_postfix(FreeType::GuestFaceRec* face); std::uint32_t APS5_VABI FT_Get_Sfnt_Name_Count_nid_postfix(FreeType::GuestFaceRec* face); int APS5_VABI FT_Get_Sfnt_Name_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t index, FreeType::GuestSfntName* name); +int APS5_VABI FT_Sfnt_Table_Info_nid_postfix(FreeType::GuestFaceRec* face, std::uint32_t tableIndex, std::uint64_t* tag, std::uint64_t* length); +int APS5_VABI FT_Load_Sfnt_Table_nid_postfix(FreeType::GuestFaceRec* face, std::uint64_t tag, std::int64_t offset, unsigned char* buffer, std::uint64_t* length); int APS5_VABI FT_Outline_Check_nid_postfix(FreeType::GuestOutline* outline); void APS5_VABI FT_Outline_Get_CBox_nid_postfix(const FreeType::GuestOutline* outline, FreeType::GuestBBox* box); +int APS5_VABI FT_Outline_Get_BBox_nid_postfix(FreeType::GuestOutline* outline, FreeType::GuestBBox* boundingBox); +int APS5_VABI sceFreeTypeOtOutlineGetBBox_nid_no_patch(); void APS5_VABI FT_Outline_Translate_nid_postfix(const FreeType::GuestOutline* outline, std::int64_t xOffset, std::int64_t yOffset); void APS5_VABI FT_Outline_Reverse_nid_postfix(FreeType::GuestOutline* outline); int APS5_VABI FT_Request_Size_nid_postfix(FreeType::GuestFaceRec* face, const FreeType::GuestSizeRequestRec* request); +int APS5_VABI FT_Property_Get_nid_postfix(FreeType::GuestLibrary* library, const char* moduleName, const char* propertyName, void* value); +int APS5_VABI sceFreeTypeOtPropertyGet_nid_no_patch(); +int APS5_VABI FT_Property_Set_nid_postfix(FreeType::GuestLibrary* library, const char* moduleName, const char* propertyName, const void* value); +int APS5_VABI sceFreeTypeOtPropertySet_nid_no_patch(); void APS5_VABI FT_Bitmap_New_nid_postfix(FreeType::GuestBitmap* bitmap); +void APS5_VABI FT_Bitmap_Init_nid_postfix(FreeType::GuestBitmap* bitmap); int APS5_VABI FT_Bitmap_Copy_nid_postfix(FreeType::GuestLibrary* library, const FreeType::GuestBitmap* source, FreeType::GuestBitmap* target); int APS5_VABI FT_Bitmap_Convert_nid_postfix(FreeType::GuestLibrary* library, const FreeType::GuestBitmap* source, FreeType::GuestBitmap* target, std::int32_t alignment); int APS5_VABI FT_Bitmap_Done_nid_postfix(FreeType::GuestLibrary* library, FreeType::GuestBitmap* bitmap); diff --git a/core/libs/prx/libSceFreeType/src/Bitmap.cpp b/core/libs/prx/libSceFreeType/src/Bitmap.cpp index 2e8a5d750e..c0392d1e97 100644 --- a/core/libs/prx/libSceFreeType/src/Bitmap.cpp +++ b/core/libs/prx/libSceFreeType/src/Bitmap.cpp @@ -58,6 +58,10 @@ void APS5_VABI FT_Bitmap_New_nid_postfix(GuestBitmap* bitmap) { if (bitmap) SyncBitmap(*bitmap, {}); } +void APS5_VABI FT_Bitmap_Init_nid_postfix(GuestBitmap* bitmap) { + FT_Bitmap_New_nid_postfix(bitmap); +} + int APS5_VABI FT_Bitmap_Copy_nid_postfix(GuestLibrary* library, const GuestBitmap* source, GuestBitmap* target) { if (!library) return FT_Err_Invalid_Library_Handle; if (!source || !target) return FT_Err_Invalid_Argument; diff --git a/core/libs/prx/libSceFreeType/src/CurveBounds.cpp b/core/libs/prx/libSceFreeType/src/CurveBounds.cpp new file mode 100644 index 0000000000..3ae90d4f60 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/CurveBounds.cpp @@ -0,0 +1,58 @@ +#include "prx/libSceFreeType/include/Api.hpp" +#include "prx/libSceFreeType/include/GuestFreeType.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include FT_BBOX_H + +#include +#include + +namespace { + +FT_Pos NativePosition(std::int64_t value) { + if (value < std::numeric_limits::min() || value > std::numeric_limits::max()) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: coordinate outside the native FT_Pos range"); + return static_cast(value); +} + +} + +extern "C" int APS5_VABI FT_Outline_Get_BBox_nid_postfix(FreeType::GuestOutline* outline, FreeType::GuestBBox* boundingBox) { + if (!boundingBox) return FT_Outline_Get_BBox(nullptr, nullptr); + FT_BBox result{}; + if (!outline) return FT_Outline_Get_BBox(nullptr, &result); + FT_Outline native{outline->n_contours, outline->n_points, nullptr, outline->tags, outline->contours, outline->flags}; + std::vector points; + if (outline->n_points != 0 && outline->n_contours != 0) { + if (!outline->points || !outline->tags || !outline->contours) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: missing nonempty outline arrays"); + if (outline->n_contours > outline->n_points) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: inconsistent contour bounds"); + std::size_t first = 0; + for (std::size_t contour = 0; contour < outline->n_contours; ++contour) { + const std::size_t last = outline->contours[contour]; + if (last < first || last >= outline->n_points) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: inconsistent contour bounds"); + first = last + 1; + } + if (first != outline->n_points) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: inconsistent contour bounds"); + points.reserve(outline->n_points); + constexpr std::int64_t curveLimit = std::numeric_limits::max() / 4; + bool curved = false; + bool wide = false; + for (std::size_t index = 0; index < outline->n_points; ++index) { + points.push_back({NativePosition(outline->points[index].x), NativePosition(outline->points[index].y)}); + curved |= (outline->tags[index] & 3) != FT_CURVE_TAG_ON; + wide |= outline->points[index].x < -curveLimit || outline->points[index].x > curveLimit || + outline->points[index].y < -curveLimit || outline->points[index].y > curveLimit; + } + if (curved && wide) + NotImplemented_nid_no_patch("FT_Outline_Get_BBox: curved coordinates exceed the portable arithmetic range"); + native.points = points.data(); + } + const FT_Error error = FT_Outline_Get_BBox(&native, &result); + if (error == 0) *boundingBox = {result.xMin, result.yMin, result.xMax, result.yMax}; + return error; +} diff --git a/core/libs/prx/libSceFreeType/src/ModuleProperties.cpp b/core/libs/prx/libSceFreeType/src/ModuleProperties.cpp new file mode 100644 index 0000000000..53d4890916 --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/ModuleProperties.cpp @@ -0,0 +1,79 @@ +#include "prx/libSceFreeType/include/Api.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/General.hpp" + +#include FT_MODULE_H + +#include +#include + +using namespace FreeType; + +static_assert(sizeof(FT_UInt) == sizeof(std::uint32_t)); +static_assert(sizeof(FT_Int32) == sizeof(std::int32_t)); +static_assert(sizeof(FT_Int) == sizeof(std::int32_t)); +static_assert(sizeof(FT_Bool) == sizeof(std::uint8_t)); + +namespace { + +enum class PropertyType { Unsigned, Signed, Boolean, Darkening, Unsupported }; +using DarkeningParameters = FT_Int[8]; + +PropertyType TypeOf(const char* moduleName, const char* propertyName) { + const bool postScript = std::strcmp(moduleName, "cff") == 0 || std::strcmp(moduleName, "type1") == 0 || + std::strcmp(moduleName, "t1cid") == 0; + const bool autoHinter = std::strcmp(moduleName, "autofitter") == 0; + if (std::strcmp(moduleName, "truetype") == 0 && std::strcmp(propertyName, "interpreter-version") == 0) { + return PropertyType::Unsigned; + } + if (postScript && std::strcmp(propertyName, "hinting-engine") == 0) return PropertyType::Unsigned; + if (postScript && std::strcmp(propertyName, "random-seed") == 0) return PropertyType::Signed; + if (postScript || autoHinter) { + if (std::strcmp(propertyName, "no-stem-darkening") == 0) return PropertyType::Boolean; + if (std::strcmp(propertyName, "darkening-parameters") == 0) return PropertyType::Darkening; + } + return PropertyType::Unsupported; +} + +template +int Read(FT_Library library, const char* moduleName, const char* propertyName, void* output) { + TValue value{}; + const auto error = FT_Property_Get(library, moduleName, propertyName, &value); + if (!error) std::memcpy(output, &value, sizeof(value)); + return error; +} + +template +int Write(FT_Library library, const char* moduleName, const char* propertyName, const void* input) { + TValue value{}; + std::memcpy(&value, input, sizeof(value)); + return FT_Property_Set(library, moduleName, propertyName, &value); +} + +} + +extern "C" int APS5_VABI FT_Property_Get_nid_postfix(GuestLibrary* library, const char* moduleName, + const char* propertyName, void* value) { + const auto host = library ? library->host : nullptr; + if (!host || !moduleName || !propertyName || !value) return FT_Property_Get(host, moduleName, propertyName, value); + switch (TypeOf(moduleName, propertyName)) { + case PropertyType::Unsigned: return Read(host, moduleName, propertyName, value); + case PropertyType::Signed: return Read(host, moduleName, propertyName, value); + case PropertyType::Boolean: return Read(host, moduleName, propertyName, value); + case PropertyType::Darkening: return Read(host, moduleName, propertyName, value); + default: NotImplemented_nid_no_patch(__func__); + } +} + +extern "C" int APS5_VABI FT_Property_Set_nid_postfix(GuestLibrary* library, const char* moduleName, + const char* propertyName, const void* value) { + const auto host = library ? library->host : nullptr; + if (!host || !moduleName || !propertyName || !value) return FT_Property_Set(host, moduleName, propertyName, value); + switch (TypeOf(moduleName, propertyName)) { + case PropertyType::Unsigned: return Write(host, moduleName, propertyName, value); + case PropertyType::Signed: return Write(host, moduleName, propertyName, value); + case PropertyType::Boolean: return Write(host, moduleName, propertyName, value); + case PropertyType::Darkening: return Write(host, moduleName, propertyName, value); + default: NotImplemented_nid_no_patch(__func__); + } +} diff --git a/core/libs/prx/libSceFreeType/src/Sfnt.cpp b/core/libs/prx/libSceFreeType/src/Sfnt.cpp new file mode 100644 index 0000000000..c16abcb99b --- /dev/null +++ b/core/libs/prx/libSceFreeType/src/Sfnt.cpp @@ -0,0 +1,95 @@ +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/GuestArena.hpp" + +#include FT_TRUETYPE_TABLES_H + +#include +#include +#include +#include +#include + +using namespace FreeType; + +namespace { + +FT_Error QueryTableLength(FT_Face face, std::uint64_t tag, FT_ULong& length) { + length = 0; + if constexpr (sizeof(FT_ULong) < sizeof(tag)) { + if (tag > std::numeric_limits::max()) { + const auto error = FT_Load_Sfnt_Table(face, 0, 0, nullptr, &length); + return error != 0 ? error : FT_Err_Table_Missing; + } + } + return FT_Load_Sfnt_Table(face, static_cast(tag), 0, nullptr, &length); +} + +} + +extern "C" { + +int APS5_VABI FT_Sfnt_Table_Info_nid_postfix(GuestFaceRec* face, std::uint32_t tableIndex, std::uint64_t* tag, std::uint64_t* length) { + const GuestFace* owner = FaceOf(face); + FT_ULong nativeTag = 0; + FT_ULong nativeLength = 0; + const auto error = FT_Sfnt_Table_Info(owner ? owner->host : nullptr, tableIndex, tag ? &nativeTag : nullptr, length ? &nativeLength : nullptr); + if (error == 0) { + if (tag) { + GuestArena::HostWrite write(tag, sizeof(*tag)); + *tag = nativeTag; + } + if (length) { + GuestArena::HostWrite write(length, sizeof(*length)); + *length = nativeLength; + } + } + return error; +} + +int APS5_VABI FT_Load_Sfnt_Table_nid_postfix(GuestFaceRec* face, std::uint64_t tag, std::int64_t offset, unsigned char* buffer, std::uint64_t* length) { + const GuestFace* owner = FaceOf(face); + const FT_Face native = owner ? owner->host : nullptr; + FT_ULong tableSize = 0; + const auto error = QueryTableLength(native, tag, tableSize); + if (error != 0) return error; + const std::uint64_t requested = length ? *length : tableSize; + if (length && requested == 0) { + GuestArena::HostWrite write(length, sizeof(*length)); + *length = tableSize; + return 0; + } + FT_ULong fileSize = tableSize; + if (tag != 0) { + const auto fileError = QueryTableLength(native, 0, fileSize); + if (fileError != 0) return fileError; + } + FT_ULong nativeTag = static_cast(tag); + FT_Long nativeOffset = 0; + if constexpr (sizeof(FT_Long) < sizeof(offset)) { + std::uint64_t absolute = static_cast(offset); + if (tag != 0) { + FT_Stream stream = native->stream; + if (stream->read) throw std::runtime_error("FT_Load_Sfnt_Table: tagged reads through a native stream callback are not implemented on this host"); + const auto position = stream->pos; + FT_ULong probeLength = 1; + unsigned char probe = 0; + const auto probeError = FT_Load_Sfnt_Table(native, nativeTag, 0, &probe, &probeLength); + const auto afterProbe = stream->pos; + stream->pos = position; + if (probeError != 0) return probeError; + absolute += static_cast(afterProbe - 1); + } + if (absolute >= fileSize) return FT_Err_Invalid_Stream_Operation; + nativeOffset = std::bit_cast(static_cast(absolute)); + nativeTag = 0; + } else { + nativeOffset = static_cast(offset); + } + FT_ULong nativeLength = static_cast(std::min(requested, static_cast(std::numeric_limits::max()))); + GuestArena::HostWrite write(buffer, static_cast(std::min(nativeLength, fileSize))); + const auto readError = FT_Load_Sfnt_Table(native, nativeTag, nativeOffset, buffer, &nativeLength); + if (readError == 0 && requested > std::numeric_limits::max()) return FT_Err_Invalid_Stream_Operation; + return readError; +} + +} diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index c4ec1cd9ca..e17ac66491 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceFreeType/include/Api.hpp" #include #include #include "SceTypes.hpp" @@ -206,10 +207,10 @@ int APS5_VABI sceFreeTypeOtUnknown_BgUMBGvhATU_nid_no_patch() { } APS5_EXPORT("BgUMBGvhATU", sceFreeTypeOtUnknown_BgUMBGvhATU_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_0Aal_TIC4so_nid_no_patch() { +int APS5_VABI sceFreeTypeOtOutlineGetBBox_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("0Aal-TIC4so", sceFreeTypeOtUnknown_0Aal_TIC4so_nid_no_patch); +APS5_EXPORT("0Aal-TIC4so", sceFreeTypeOtOutlineGetBBox_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_4RRjK_Wd1lU_nid_no_patch() { NotImplemented_nid_no_patch(__func__); @@ -241,15 +242,15 @@ int APS5_VABI sceFreeTypeOtUnknown_0Lv6K1d47jg_nid_no_patch() { } APS5_EXPORT("0Lv6K1d47jg", sceFreeTypeOtUnknown_0Lv6K1d47jg_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_8fAMdZCnewA_nid_no_patch() { +int APS5_VABI sceFreeTypeOtPropertyGet_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("8fAMdZCnewA", sceFreeTypeOtUnknown_8fAMdZCnewA_nid_no_patch); +APS5_EXPORT("8fAMdZCnewA", sceFreeTypeOtPropertyGet_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_LsB538_IPrE_nid_no_patch() { +int APS5_VABI sceFreeTypeOtPropertySet_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } -APS5_EXPORT("LsB538+IPrE", sceFreeTypeOtUnknown_LsB538_IPrE_nid_no_patch); +APS5_EXPORT("LsB538+IPrE", sceFreeTypeOtPropertySet_nid_no_patch); int APS5_VABI sceFreeTypeOtUnknown_hz_J3hohXW8_nid_no_patch() { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libSceGvMp4Parser/Export.cpp b/core/libs/prx/libSceGvMp4Parser/Export.cpp index d81f4a5e32..4d1c7ba5b3 100644 --- a/core/libs/prx/libSceGvMp4Parser/Export.cpp +++ b/core/libs/prx/libSceGvMp4Parser/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceGvMp4Parser/include/Api.hpp" #include #include #include "SceTypes.hpp" @@ -5,6 +6,29 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser19gvMp4ParserFinalizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser21gvMp4ParserInitializeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser23gvMp4ParserReadMetadataERKSsRKNS_8MetadataERS3_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser24gvMp4ParserWriteMetadataERKSsRKNS_8MetadataE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser27gvMp4ParserExtractThumbnailERKSsRNS_9ThumbnailE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceGvMp4Parser/include/Api.hpp b/core/libs/prx/libSceGvMp4Parser/include/Api.hpp new file mode 100644 index 0000000000..d27fe7f4be --- /dev/null +++ b/core/libs/prx/libSceGvMp4Parser/include/Api.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser19gvMp4ParserFinalizeEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser21gvMp4ParserInitializeEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser23gvMp4ParserReadMetadataERKSsRKNS_8MetadataERS3_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser24gvMp4ParserWriteMetadataERKSsRKNS_8MetadataE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter11gvMp4Parser27gvMp4ParserExtractThumbnailERKSsRNS_9ThumbnailE(void); + +} diff --git a/core/libs/prx/libSceGvMp4Parser/libSceGvMp4Parser.hpp b/core/libs/prx/libSceGvMp4Parser/libSceGvMp4Parser.hpp index 6f70f09bee..c639941f8d 100644 --- a/core/libs/prx/libSceGvMp4Parser/libSceGvMp4Parser.hpp +++ b/core/libs/prx/libSceGvMp4Parser/libSceGvMp4Parser.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceGvMp4Parser/include/Api.hpp" diff --git a/core/libs/prx/libSceHidControl/Export.cpp b/core/libs/prx/libSceHidControl/Export.cpp index bb7329027a..e53100bbdf 100644 --- a/core/libs/prx/libSceHidControl/Export.cpp +++ b/core/libs/prx/libSceHidControl/Export.cpp @@ -6,8 +6,6 @@ extern "C" { -APS5_DUMMY_FUN - int APS5_VABI sceHidControlEnableMicBeamForming(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -208,4 +206,119 @@ int APS5_VABI sceHidControlGetPswDeviceIdFromIdxAdd(void) { return 0; } +int APS5_VABI sceHidControlConnectHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlConnectPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlDisconnectDevice() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlDisconnectHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlDisconnectPort() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetAuthInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetBatteryState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetDeviceId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetDeviceInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetDeviceName() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetJediExtensionUnitInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetJediState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetSparkState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlGetVersionInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlInit() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetApplicationFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetHandleState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetJediAudioOutPath() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetJediVolumeGain() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetProcessFocus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetRevokeList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetRevokeList2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHidControlSetUserColor() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceHidControl/include/Api.hpp b/core/libs/prx/libSceHidControl/include/Api.hpp index ca9e12b806..d19c925d3d 100644 --- a/core/libs/prx/libSceHidControl/include/Api.hpp +++ b/core/libs/prx/libSceHidControl/include/Api.hpp @@ -44,5 +44,28 @@ int APS5_VABI sceHidControlGetPswDeviceInfo(void); int APS5_VABI sceHidControlSetPswCommand(void); int APS5_VABI sceHidControlGetPswCommand(void); int APS5_VABI sceHidControlGetPswDeviceIdFromIdxAdd(void); +int APS5_VABI sceHidControlConnectHandle(void); +int APS5_VABI sceHidControlConnectPort(void); +int APS5_VABI sceHidControlDisconnectDevice(void); +int APS5_VABI sceHidControlDisconnectHandle(void); +int APS5_VABI sceHidControlDisconnectPort(void); +int APS5_VABI sceHidControlGetAuthInformation(void); +int APS5_VABI sceHidControlGetBatteryState(void); +int APS5_VABI sceHidControlGetDeviceId(void); +int APS5_VABI sceHidControlGetDeviceInfo(void); +int APS5_VABI sceHidControlGetDeviceName(void); +int APS5_VABI sceHidControlGetJediExtensionUnitInfo(void); +int APS5_VABI sceHidControlGetJediState(void); +int APS5_VABI sceHidControlGetSparkState(void); +int APS5_VABI sceHidControlGetVersionInfo(void); +int APS5_VABI sceHidControlInit(void); +int APS5_VABI sceHidControlSetApplicationFocus(void); +int APS5_VABI sceHidControlSetHandleState(void); +int APS5_VABI sceHidControlSetJediAudioOutPath(void); +int APS5_VABI sceHidControlSetJediVolumeGain(void); +int APS5_VABI sceHidControlSetProcessFocus(void); +int APS5_VABI sceHidControlSetRevokeList(void); +int APS5_VABI sceHidControlSetRevokeList2(void); +int APS5_VABI sceHidControlSetUserColor(void); } diff --git a/core/libs/prx/libSceIcu/CMakeLists.txt b/core/libs/prx/libSceIcu/CMakeLists.txt index 5fcc2e7744..14fb9a0761 100644 --- a/core/libs/prx/libSceIcu/CMakeLists.txt +++ b/core/libs/prx/libSceIcu/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceIcu SHARED EXCLUDE_FROM_ALL Export.cpp Invariant.cpp Version.cpp) +add_library(libSceIcu SHARED EXCLUDE_FROM_ALL Export.cpp Invariant.cpp Version.cpp Unescape.cpp) target_include_directories(libSceIcu PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceIcu) @@ -14,3 +14,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_icu_unescape_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Unescape.cpp) + target_include_directories(guest_icu_unescape_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_icu_unescape_tests PRIVATE libSceIcu) + configure_windows_unwind(guest_icu_unescape_tests) + add_test(NAME guest_icu_unescape COMMAND guest_icu_unescape_tests) + set_tests_properties(guest_icu_unescape PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_icu_unescape PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceIcu/Unescape.cpp b/core/libs/prx/libSceIcu/Unescape.cpp new file mode 100644 index 0000000000..acf0b2abaf --- /dev/null +++ b/core/libs/prx/libSceIcu/Unescape.cpp @@ -0,0 +1,157 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include + +namespace { + +bool IsLead(std::int32_t value) { + return value >= 0xd800 && value <= 0xdbff; +} + +bool IsTrail(std::int32_t value) { + return value >= 0xdc00 && value <= 0xdfff; +} + +std::int32_t Supplementary(std::int32_t lead, std::int32_t trail) { + return 0x10000 + ((lead - 0xd800) << 10) + trail - 0xdc00; +} + +int Digit(char16_t value, int base) { + int digit = value >= u'0' && value <= u'9' ? value - u'0' + : value >= u'a' && value <= u'f' ? value - u'a' + 10 + : value >= u'A' && value <= u'F' ? value - u'A' + 10 : -1; + return digit < base ? digit : -1; +} + +std::int32_t Decode(Icu::UnescapeCharacterAt characterAt, std::int32_t& position, std::int32_t length, void* context) { + const auto start = position; + if (position < 0 || position >= length) return -1; + if (!characterAt) NotImplemented_nid_no_patch("u_unescapeAt: null callback"); + const auto initial = characterAt(position++, context); + bool braces = false; + int minimum = 0; + int maximum = 0; + int digits = 0; + int base = 16; + std::uint32_t value = 0; + switch (initial) { + case u'u': minimum = maximum = 4; break; + case u'U': minimum = maximum = 8; break; + case u'x': + minimum = 1; + braces = position < length && characterAt(position, context) == u'{'; + maximum = braces ? 8 : 2; + if (braces) ++position; + break; + default: + if (initial >= u'0' && initial <= u'7') { + minimum = 1; + maximum = 3; + digits = 1; + base = 8; + value = initial - u'0'; + } + break; + } + if (minimum) { + char16_t last = initial; + while (position < length && digits < maximum) { + last = characterAt(position, context); + const int digit = Digit(last, base); + if (digit < 0) break; + value = value * base + digit; + ++position; + ++digits; + } + if (digits < minimum || (braces && last != u'}') || value > 0x10ffff) { + position = start; + return -1; + } + if (braces) ++position; + if (IsLead(value) && position < length) { + auto ahead = position + 1; + std::int32_t tail = characterAt(position, context); + if (tail == u'\\' && ahead < length) { + const auto limit = ahead + std::min(11, length - ahead); + tail = Decode(characterAt, ahead, limit, context); + } + if (IsTrail(tail)) { + position = ahead; + return Supplementary(value, tail); + } + } + return static_cast(value); + } + switch (initial) { + case u'a': return 7; + case u'b': return 8; + case u'e': return 27; + case u'f': return 12; + case u'n': return 10; + case u'r': return 13; + case u't': return 9; + case u'v': return 11; + default: break; + } + const bool control = initial == u'c' && position < length; + std::int32_t result = control ? characterAt(position++, context) : initial; + if (IsLead(result) && position < length) { + const auto tail = characterAt(position, context); + if (IsTrail(tail)) { + ++position; + result = Supplementary(result, tail); + } + } + return control ? result & 0x1f : result; +} + +char16_t APS5_VABI ByteAt(std::int32_t position, void* context) { + return static_cast(context)[position]; +} + +} // namespace + +extern "C" { + +std::int32_t APS5_VABI u_unescapeAt_nid_postfix(Icu::UnescapeCharacterAt characterAt, std::int32_t* offset, std::int32_t length, void* context) { + if (!offset) NotImplemented_nid_no_patch(__func__); + return Decode(characterAt, *offset, length, context); +} + +std::int32_t APS5_VABI u_unescape_nid_postfix(const char* source, char16_t* destination, std::int32_t capacity) { + if (!source || (destination && capacity < 0)) NotImplemented_nid_no_patch(__func__); + const auto bytes = std::strlen(source); + if (bytes > static_cast(std::numeric_limits::max())) + NotImplemented_nid_no_patch(__func__); + const auto length = static_cast(bytes); + std::int32_t written = 0; + for (std::int32_t position = 0; position < length;) { + const auto byte = static_cast(source[position++]); + if (byte != '\\') { + if (destination && written < capacity) destination[written] = byte; + ++written; + continue; + } + const auto value = Decode(ByteAt, position, length, const_cast(source)); + if (value < 0) { + if (destination && capacity > 0) destination[0] = 0; + return 0; + } + const int units = value > 0xffff ? 2 : 1; + if (destination && written <= capacity && units <= capacity - written) { + if (units == 1) destination[written] = static_cast(value); + else { + destination[written] = static_cast(0xd800 + ((value - 0x10000) >> 10)); + destination[written + 1] = static_cast(0xdc00 + ((value - 0x10000) & 0x3ff)); + } + } + written += units; + } + if (destination && written < capacity) destination[written] = 0; + return written; +} + +} diff --git a/core/libs/prx/libSceIcu/Unescape.hpp b/core/libs/prx/libSceIcu/Unescape.hpp new file mode 100644 index 0000000000..a91eb17ed6 --- /dev/null +++ b/core/libs/prx/libSceIcu/Unescape.hpp @@ -0,0 +1,12 @@ +#pragma once +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +namespace Icu { +using UnescapeCharacterAt = char16_t (APS5_VABI*)(std::int32_t, void*); +} // namespace Icu + +extern "C" { +std::int32_t APS5_VABI u_unescape_nid_postfix(const char*, char16_t*, std::int32_t); +std::int32_t APS5_VABI u_unescapeAt_nid_postfix(Icu::UnescapeCharacterAt, std::int32_t*, std::int32_t, void*); +} diff --git a/core/libs/prx/libSceIcu/libSceIcu.hpp b/core/libs/prx/libSceIcu/libSceIcu.hpp index 7b35c6d5fb..d8c66857ef 100644 --- a/core/libs/prx/libSceIcu/libSceIcu.hpp +++ b/core/libs/prx/libSceIcu/libSceIcu.hpp @@ -1,3 +1,4 @@ #pragma once #include "Invariant.hpp" #include "Version.hpp" +#include "Unescape.hpp" diff --git a/core/libs/prx/libSceIcu/tests/Unescape.cpp b/core/libs/prx/libSceIcu/tests/Unescape.cpp new file mode 100644 index 0000000000..1dc7152940 --- /dev/null +++ b/core/libs/prx/libSceIcu/tests/Unescape.cpp @@ -0,0 +1,189 @@ +#include "prx/libSceIcu/libSceIcu.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace { + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "ICU unescape check failed at line %u\n", location.line()); + std::abort(); +} + +template +void RequireThrows(TAction action) { + try { action(); } catch (const std::exception&) { return; } + Require(false); +} + +struct Input { + std::u16string text; + std::vector positions; + std::int32_t* offset = nullptr; + std::int32_t base = 0; + std::vector> observations; +}; + +char16_t APS5_VABI Read_nid_no_patch(std::int32_t position, void* context) { + auto& input = *static_cast(context); + const auto relative = static_cast(position) - input.base; + Require(relative >= 0 && static_cast(relative) < input.text.size()); + input.positions.push_back(position); + if (input.offset) input.observations.emplace_back(static_cast(relative), *input.offset - input.base); + return input.text[relative]; +} + +void At(std::u16string text, std::int32_t expected, std::int32_t consumed) { + Input input{std::move(text), {}}; + std::int32_t offset = 0; + Require(u_unescapeAt_nid_postfix(Read_nid_no_patch, &offset, input.text.size(), &input) == expected); + Require(offset == consumed); +} + +void CallbackOrder() { + const auto check = [](std::u16string text, std::int32_t expected, std::int32_t consumed, + std::vector> observations, std::int32_t base = 0) { + auto offset = base; + Input input{std::move(text), {}, &offset, base, {}}; + const auto length = static_cast(static_cast(base) + input.text.size()); + Require(u_unescapeAt_nid_postfix(Read_nid_no_patch, &offset, length, &input) == expected); + Require(offset - base == consumed && input.observations == observations); + }; + check(u"u0041!", 0x41, 5, {{0, 1}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}); + check(u"x{41}", 0x41, 5, {{0, 1}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}); + check(u"u12", -1, 0, {{0, 1}, {1, 1}, {2, 2}}); + check(u"uD83D\\uDE00", 0x1f600, 11, + {{0, 1}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 5}, {7, 5}, {8, 5}, {9, 5}, {10, 5}}); + check(u"uD800\\u12", 0xd800, 5, + {{0, 1}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}, {6, 5}, {7, 5}, {8, 5}}); + check(std::u16string(u"uD83D") + char16_t{0xde00}, 0x1f600, 6, + {{0, 1}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}}, std::numeric_limits::max() - 6); + check(u"u", -1, 0, {{0, 1}}, std::numeric_limits::max() - 1); +} + +void SingleEscapes() { + At(u"u0041!", 0x41, 5); + At(u"U0010ffff!", 0x10ffff, 9); + At(u"xF!", 15, 2); + At(u"xFF0", 255, 3); + At(u"x{10FfFf}!", 0x10ffff, 9); + At(u"x{010ffff}!", 0x10ffff, 10); + At(u"x{0010ffff}!", -1, 0); + At(u"7777", 511, 3); + At(u"078", 7, 2); + At(u"8", u'8', 1); + At(u"cA", 1, 2); + At(u"c", u'c', 1); + At(u"c\U0001f600", 0, 3); + At(u"\U0001f600", 0x1f600, 2); + At(u"uD83D\\uDE00!", 0x1f600, 11); + At(std::u16string(u"uD83D") + char16_t{0xde00} + u"!", 0x1f600, 6); + At(u"uD83D\\x{0DE00}!", 0x1f600, 14); + At(u"uD800\\uD801\\uDC00", 0xd800, 5); + At(u"uD800\\u12", 0xd800, 5); + At(u"uDC00", 0xdc00, 5); + At(u"\\", u'\\', 1); + At(u"-", u'-', 1); + At(u"a", 7, 1); + At(u"b", 8, 1); + At(u"e", 27, 1); + At(u"f", 12, 1); + At(u"n", 10, 1); + At(u"r", 13, 1); + At(u"t", 9, 1); + At(u"v", 11, 1); + for (const auto* text : {u"", u"u", u"u123", u"U0000000", u"U00110000", u"Uffffffff", u"x", u"x{}", u"x{1", u"x{110000}"}) + At(text, -1, 0); + Input input{u"prefixu0041suffix", {}}; + std::int32_t offset = 6; + Require(u_unescapeAt_nid_postfix(Read_nid_no_patch, &offset, input.text.size(), &input) == 0x41 && offset == 11); + for (const auto invalid : {-1, 17, std::numeric_limits::max()}) { + offset = invalid; + const auto calls = input.positions.size(); + Require(u_unescapeAt_nid_postfix(Read_nid_no_patch, &offset, input.text.size(), &input) == -1); + Require(offset == invalid && input.positions.size() == calls); + } + offset = 0; + Require(u_unescapeAt_nid_postfix(nullptr, &offset, -1, nullptr) == -1 && offset == 0); + RequireThrows([&] { u_unescapeAt_nid_postfix(nullptr, &offset, 1, nullptr); }); + RequireThrows([] { u_unescapeAt_nid_postfix(Read_nid_no_patch, nullptr, 1, nullptr); }); +} + +void String(const char* escaped, std::u16string_view expected) { + Require(u_unescape_nid_postfix(escaped, nullptr, std::numeric_limits::min()) == static_cast(expected.size())); + for (std::int32_t capacity = 0; capacity <= static_cast(expected.size()) + 2; ++capacity) { + std::vector output(expected.size() + 4, 0xa55a); + Require(u_unescape_nid_postfix(escaped, output.data() + 1, capacity) == static_cast(expected.size())); + auto prefix = std::min(capacity, expected.size()); + if (prefix > 0 && prefix < expected.size() && expected[prefix - 1] >= 0xd800 && expected[prefix - 1] <= 0xdbff + && expected[prefix] >= 0xdc00 && expected[prefix] <= 0xdfff) --prefix; + Require(std::equal(expected.begin(), expected.begin() + prefix, output.begin() + 1)); + for (auto index = prefix; index < std::min(capacity, expected.size()); ++index) + Require(output[index + 1] == 0xa55a); + if (capacity > static_cast(expected.size())) Require(output[expected.size() + 1] == 0); + Require(output.front() == 0xa55a); + for (std::size_t index = capacity + 1; index < output.size(); ++index) Require(output[index] == 0xa55a); + } +} + +void Strings() { + String("", u""); + String("plain text", u"plain text"); + String("A\\u0042", u"AB"); + String("\\a\\b\\t\\n\\v\\f\\r\\e\\\"\\'\\?\\\\", u"\a\b\t\n\v\f\r\x1b\"'?\\"); + String("[a\\-z]\\cA\\777", u"[a-z]\x01\u01ff"); + String("A\\u0042\\U0001F600Z", u"AB\U0001f600Z"); + String("\\uD83D\\uDE00", u"\U0001f600"); + const char16_t unpaired[] = {0xd800, 0xd801, 0xdc00}; + String("\\uD800\\uD801\\uDC00", std::u16string_view(unpaired, 3)); + String("\\x{10ffff}\\xA\\123", u"\U0010ffff\nS"); + const char16_t embedded[] = {u'A', 0, u'B'}; + String("A\\0B", std::u16string_view(embedded, 3)); + for (unsigned int byte = 1; byte <= 255; ++byte) { + if (byte == '\\') continue; + const char source[] = {static_cast(byte), 0}; + const char16_t expected[] = {static_cast(byte)}; + String(source, std::u16string_view(expected, 1)); + } + char16_t destination[] = {0xa55a, 0xa55a, 0xa55a}; + Require(u_unescape_nid_postfix("\\U0001f600Z", destination, 1) == 3); + Require(destination[0] == 0xa55a && destination[1] == 0xa55a); + Require(u_unescape_nid_postfix("a\\u12", destination, 3) == 0 && destination[0] == 0 && destination[1] == 0xa55a); + destination[0] = 0xa55a; + Require(u_unescape_nid_postfix("abc\\u12", destination, 3) == 0 && destination[0] == 0 && destination[1] == u'b' && destination[2] == u'c'); + for (auto capacity : {-1, std::numeric_limits::min()}) { + std::fill(std::begin(destination), std::end(destination), 0xa55a); + RequireThrows([&] { u_unescape_nid_postfix("a\\U0001f600", destination, capacity); }); + Require(destination[0] == 0xa55a && destination[1] == 0xa55a && destination[2] == 0xa55a); + } + RequireThrows([] { u_unescape_nid_postfix(nullptr, nullptr, 0); }); + for (std::uint32_t value = 0; value <= 0x10ffff; value += 17) { + char escaped[16]; + std::snprintf(escaped, sizeof(escaped), "\\U%08x", value); + std::array output; + output.fill(0xa55a); + const auto size = u_unescape_nid_postfix(escaped, output.data(), output.size()); + if (value <= 0xffff) Require(size == 1 && output[0] == value && output[1] == 0); + else Require(size == 2 && output[0] == 0xd800 + ((value - 0x10000) >> 10) && output[1] == 0xdc00 + ((value - 0x10000) & 0x3ff) && output[2] == 0); + } +} + +} // namespace + +int main() { + SingleEscapes(); + CallbackOrder(); + Strings(); + std::puts("ICU escape decoding tests passed"); +} diff --git a/core/libs/prx/libSceInvitationDialog/CMakeLists.txt b/core/libs/prx/libSceInvitationDialog/CMakeLists.txt new file mode 100644 index 0000000000..56b44db4a6 --- /dev/null +++ b/core/libs/prx/libSceInvitationDialog/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceInvitationDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceInvitationDialog PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceInvitationDialog PRIVATE libSceCommonDialog libc) +configure_windows_unwind(libSceInvitationDialog) + +if(BUILD_TESTING) + add_test_executable(guest_invitation_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestInvitationDialog.cpp) + target_include_directories(guest_invitation_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_invitation_dialog_tests PRIVATE libSceInvitationDialog libSceCommonDialog libc) + configure_windows_unwind(guest_invitation_dialog_tests) + add_test(NAME guest_invitation_dialog COMMAND guest_invitation_dialog_tests) + if(WIN32) + set_tests_properties(guest_invitation_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceInvitationDialog/Export.cpp b/core/libs/prx/libSceInvitationDialog/Export.cpp index 9053e853ee..e198f7e1b0 100644 --- a/core/libs/prx/libSceInvitationDialog/Export.cpp +++ b/core/libs/prx/libSceInvitationDialog/Export.cpp @@ -1,37 +1,114 @@ #include "prx/libSceInvitationDialog/include/Api.hpp" +#include "prx/libSceCommonDialog/include/DialogUse.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_SYSTEM_INITIALIZED = static_cast(0x80B80001u); +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80008u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +constexpr std::int32_t INVITATION_DIALOG_MODE_SEND = 1; +constexpr std::int32_t INVITATION_DIALOG_MODE_RECV = 2; + +struct DialogState { + std::mutex mutex; + int status = COMMON_DIALOG_STATUS_NONE; + void* callbackArg = nullptr; +}; + +DialogState& Dialog() { + static DialogState state; + return state; +} + +[[noreturn]] void Unsettled(const char* function, const std::string& what) { + throw std::runtime_error(std::string(function) + ": " + what); +} + +} extern "C" { -int APS5_VABI sceInvitationDialogGetResult(InvitationDialogResult* result) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceInvitationDialogInitialize(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (!CommonDialogSystemInitialized_nid_no_patch()) return COMMON_DIALOG_ERROR_NOT_SYSTEM_INITIALIZED; + if (dialog.status != COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + if (!CommonDialogAcquire_nid_no_patch()) return COMMON_DIALOG_ERROR_BUSY; + dialog.status = COMMON_DIALOG_STATUS_INITIALIZED; + dialog.callbackArg = nullptr; + return COMMON_DIALOG_OK; } -int APS5_VABI sceInvitationDialogInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceInvitationDialogTerminate(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + dialog.status = COMMON_DIALOG_STATUS_NONE; + dialog.callbackArg = nullptr; + CommonDialogRelease_nid_no_patch(); + return COMMON_DIALOG_OK; } -int APS5_VABI sceInvitationDialogOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceInvitationDialogUpdateStatus(void) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + return dialog.status; } -int APS5_VABI sceInvitationDialogTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceInvitationDialogUpdateStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceInvitationDialogOpen(const InvitationDialogParam* param) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status != COMMON_DIALOG_STATUS_INITIALIZED && dialog.status != COMMON_DIALOG_STATUS_FINISHED) { + Unsettled(__func__, "status " + std::to_string(dialog.status) + " is neither INITIALIZED nor FINISHED, and the error for it is unsettled"); + } + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (param->size != sizeof(InvitationDialogParam)) { + Unsettled(__func__, "param size " + std::to_string(param->size) + " is not the " + std::to_string(sizeof(InvitationDialogParam)) + "-byte layout this library reads"); + } + if (param->mode != INVITATION_DIALOG_MODE_SEND && param->mode != INVITATION_DIALOG_MODE_RECV) { + Unsettled(__func__, "mode " + std::to_string(param->mode) + " is neither SEND nor RECV, and the error for it is unsettled"); + } + dialog.callbackArg = param->callbackArg; + dialog.status = COMMON_DIALOG_STATUS_FINISHED; + return COMMON_DIALOG_OK; } int APS5_VABI sceInvitationDialogClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + Unsettled(__func__, "status " + std::to_string(dialog.status) + " is not RUNNING, and the error for closing a dialog that is not running is unsettled"); +} + +int APS5_VABI sceInvitationDialogGetResult(InvitationDialogResult* result) { + DialogState& dialog = Dialog(); + std::lock_guard lock(dialog.mutex); + if (dialog.status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (dialog.status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (std::any_of(std::begin(result->reserved), std::end(result->reserved), [](std::uint8_t value) { return value != 0; })) { + Unsettled(__func__, "the module checks the reserved bytes 0x18..0x37 of the result, and the error for a non-zero one is unsettled"); + } + result->callbackArg = dialog.callbackArg; + result->errorCode = COMMON_DIALOG_OK; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; + if (result->recipients != nullptr) result->recipients->count = 0; + return COMMON_DIALOG_OK; } } diff --git a/core/libs/prx/libSceInvitationDialog/include/Api.hpp b/core/libs/prx/libSceInvitationDialog/include/Api.hpp index 884ec82465..7e53014e52 100644 --- a/core/libs/prx/libSceInvitationDialog/include/Api.hpp +++ b/core/libs/prx/libSceInvitationDialog/include/Api.hpp @@ -1,27 +1,47 @@ #pragma once +#include "prx/libSceCommonDialog/CommonDialog.hpp" #include "SceTypes.hpp" #include #include +struct InvitationDialogParam { + CommonDialogBaseParam baseParam; + std::uint32_t size; + std::int32_t mode; + std::int32_t userId; + std::uint32_t padding; + void* callbackArg; + const void* dataParam; + std::uint8_t reserved[64]; +}; + struct InvitationDialogRecipients { std::uint32_t count; NpOnlineId onlineIds[16]; }; struct InvitationDialogResult { - std::uint8_t opaque00[8]; + void* callbackArg; std::int32_t errorCode; - std::uint32_t opaque0C; + std::int32_t result; InvitationDialogRecipients* recipients; std::uint8_t reserved[32]; }; +static_assert(sizeof(CommonDialogBaseParam) == 48); +static_assert(offsetof(InvitationDialogParam, size) == 48); +static_assert(offsetof(InvitationDialogParam, mode) == 52); +static_assert(offsetof(InvitationDialogParam, userId) == 56); +static_assert(offsetof(InvitationDialogParam, callbackArg) == 64); +static_assert(offsetof(InvitationDialogParam, dataParam) == 72); +static_assert(offsetof(InvitationDialogParam, reserved) == 80); +static_assert(sizeof(InvitationDialogParam) == 144); static_assert(sizeof(NpOnlineId) == 20); static_assert(offsetof(InvitationDialogRecipients, onlineIds) == 4); static_assert(sizeof(InvitationDialogRecipients) == 324); static_assert(offsetof(InvitationDialogResult, errorCode) == 0x08); -static_assert(offsetof(InvitationDialogResult, opaque0C) == 0x0C); +static_assert(offsetof(InvitationDialogResult, result) == 0x0C); static_assert(offsetof(InvitationDialogResult, recipients) == 0x10); static_assert(offsetof(InvitationDialogResult, reserved) == 0x18); static_assert(sizeof(InvitationDialogResult) == 0x38); @@ -29,10 +49,10 @@ static_assert(sizeof(InvitationDialogResult) == 0x38); extern "C" { int APS5_VABI sceInvitationDialogGetResult(InvitationDialogResult* result); -int APS5_VABI sceInvitationDialogInitialize(); -int APS5_VABI sceInvitationDialogOpen(); -int APS5_VABI sceInvitationDialogTerminate(); -int APS5_VABI sceInvitationDialogUpdateStatus(); +int APS5_VABI sceInvitationDialogInitialize(void); +int APS5_VABI sceInvitationDialogOpen(const InvitationDialogParam* param); +int APS5_VABI sceInvitationDialogTerminate(void); +int APS5_VABI sceInvitationDialogUpdateStatus(void); int APS5_VABI sceInvitationDialogClose(void); } diff --git a/core/libs/prx/libSceJpegParser/Export.cpp b/core/libs/prx/libSceJpegParser/Export.cpp index d81f4a5e32..d42156fe9b 100644 --- a/core/libs/prx/libSceJpegParser/Export.cpp +++ b/core/libs/prx/libSceJpegParser/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceJpegParser/include/Api.hpp" #include #include #include "SceTypes.hpp" @@ -5,6 +6,29 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser18jpegParserFinalizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser20jpegParserInitializeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser22jpegParserReadMetadataERKSsRKNS_8MetadataERS3_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser23jpegParserWriteMetadataERKSsRKNS_8MetadataE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser26jpegParserExtractThumbnailERKSsRNS_9ThumbnailE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceJpegParser/include/Api.hpp b/core/libs/prx/libSceJpegParser/include/Api.hpp new file mode 100644 index 0000000000..a46c4d5364 --- /dev/null +++ b/core/libs/prx/libSceJpegParser/include/Api.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser18jpegParserFinalizeEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser20jpegParserInitializeEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser22jpegParserReadMetadataERKSsRKNS_8MetadataERS3_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser23jpegParserWriteMetadataERKSsRKNS_8MetadataE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter10jpegParser26jpegParserExtractThumbnailERKSsRNS_9ThumbnailE(void); + +} diff --git a/core/libs/prx/libSceJpegParser/libSceJpegParser.hpp b/core/libs/prx/libSceJpegParser/libSceJpegParser.hpp index 6f70f09bee..9b5e8e9385 100644 --- a/core/libs/prx/libSceJpegParser/libSceJpegParser.hpp +++ b/core/libs/prx/libSceJpegParser/libSceJpegParser.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceJpegParser/include/Api.hpp" diff --git a/core/libs/prx/libSceJsc/CMakeLists.txt b/core/libs/prx/libSceJsc/CMakeLists.txt new file mode 100644 index 0000000000..73915bc4ff --- /dev/null +++ b/core/libs/prx/libSceJsc/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceJsc SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceJsc PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceJsc) + +if(BUILD_TESTING) + add_test_executable(guest_jsc_string_tests ${LIBS_INCLUDE_DIR}/tests/GuestJscString.cpp) + target_include_directories(guest_jsc_string_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_jsc_string_tests PRIVATE libSceJsc libc) + configure_windows_unwind(guest_jsc_string_tests) + add_test(NAME guest_jsc_string COMMAND guest_jsc_string_tests) + if(WIN32) + set_tests_properties(guest_jsc_string PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceJsc/Export.cpp b/core/libs/prx/libSceJsc/Export.cpp index d81f4a5e32..05794b5e68 100644 --- a/core/libs/prx/libSceJsc/Export.cpp +++ b/core/libs/prx/libSceJsc/Export.cpp @@ -1,10 +1,154 @@ +#include "prx/libSceJsc/include/Api.hpp" +#include "prx/libc/include/General.hpp" + +#include #include #include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" +#include +#include +#include +#include + + + +namespace { + +std::unique_ptr Decode(const char* text) { + auto result = std::make_unique(); + const auto* cursor = reinterpret_cast(text); + while (*cursor) { + std::uint32_t codepoint = *cursor++; + unsigned following = 0; + std::uint32_t minimum = 0; + if (codepoint >= 0xC2 && codepoint <= 0xDF) { + codepoint &= 0x1F; + following = 1; + minimum = 0x80; + } else if (codepoint >= 0xE0 && codepoint <= 0xEF) { + codepoint &= 0x0F; + following = 2; + minimum = 0x800; + } else if (codepoint >= 0xF0 && codepoint <= 0xF4) { + codepoint &= 7; + following = 3; + minimum = 0x10000; + } else if (codepoint >= 0x80) { + result->nullString = true; + break; + } + while (following > 0) { + --following; + if ((*cursor & 0xC0) != 0x80) { + result->nullString = true; + break; + } + codepoint = codepoint << 6 | (*cursor++ & 0x3F); + } + if (result->nullString || codepoint < minimum || codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) { + result->nullString = true; + break; + } + if (codepoint > 0xFFFF) { + const auto supplementary = codepoint - 0x10000; + result->characters.push_back(static_cast(0xD800 + (supplementary >> 10))); + result->characters.push_back(static_cast(0xDC00 + (supplementary & 0x3FF))); + } else result->characters.push_back(static_cast(codepoint)); + } + if (result->nullString) result->characters.clear(); + result->characters.push_back(0); + return result; +} + +bool Equal(const GuestJSString* first, const GuestJSString* second) { + if (first == second) return true; + if (!first || !second) return false; + return first->nullString == second->nullString && first->characters == second->characters; +} + +} extern "C" { -APS5_DUMMY_FUN +GuestJSString* APS5_VABI JSStringCreateWithCharacters(const std::uint16_t* characters, std::size_t count) { + if (!characters && count) NotImplemented_nid_no_patch(__func__); + if (count > static_cast(std::numeric_limits::max())) NotImplemented_nid_no_patch(__func__); + auto string = std::make_unique(); + string->nullString = !characters; + if (count) string->characters.assign(characters, characters + count); + string->characters.push_back(0); + return string.release(); +} + +GuestJSString* APS5_VABI JSStringCreateWithUTF8CString(const char* text) { + if (!text) NotImplemented_nid_no_patch(__func__); + return Decode(text).release(); +} + +GuestJSString* APS5_VABI JSStringRetain(GuestJSString* string) { + if (!string) NotImplemented_nid_no_patch(__func__); + auto references = string->references.load(std::memory_order_relaxed); + for (;;) { + if (references == std::numeric_limits::max()) NotImplemented_nid_no_patch(__func__); + if (string->references.compare_exchange_weak(references, references + 1, std::memory_order_relaxed)) return string; + } +} + +void APS5_VABI JSStringRelease(GuestJSString* string) { + if (!string) NotImplemented_nid_no_patch(__func__); + if (string->references.fetch_sub(1, std::memory_order_acq_rel) == 1) delete string; +} + +std::size_t APS5_VABI JSStringGetLength(const GuestJSString* string) { + return string ? string->characters.size() - 1 : 0; +} + +const std::uint16_t* APS5_VABI JSStringGetCharactersPtr(const GuestJSString* string) { + return string && !string->nullString ? string->characters.data() : nullptr; +} + +std::size_t APS5_VABI JSStringGetMaximumUTF8CStringSize(const GuestJSString* string) { + if (!string) NotImplemented_nid_no_patch(__func__); + const auto length = string->characters.size() - 1; + if (length > (std::numeric_limits::max() - 1) / 3) NotImplemented_nid_no_patch(__func__); + return length * 3 + 1; +} + +std::size_t APS5_VABI JSStringGetUTF8CString(const GuestJSString* string, char* buffer, std::size_t bufferSize) { + if (!string || !buffer || !bufferSize) return 0; + std::size_t written = 0; + const auto count = string->characters.size() - 1; + for (std::size_t index = 0; index < count; ++index) { + std::uint32_t codepoint = string->characters[index]; + if (codepoint >= 0xD800 && codepoint <= 0xDBFF) { + if (index + 1 == count) break; + const auto low = string->characters[index + 1]; + if (low < 0xDC00 || low > 0xDFFF) break; + codepoint = 0x10000 + ((codepoint - 0xD800) << 10) + low - 0xDC00; + ++index; + } else if (codepoint >= 0xDC00 && codepoint <= 0xDFFF) break; + const std::size_t bytes = codepoint < 0x80 ? 1 : codepoint < 0x800 ? 2 : codepoint < 0x10000 ? 3 : 4; + if (bytes > bufferSize - 1 - written) break; + if (bytes == 1) buffer[written++] = static_cast(codepoint); + else { + const unsigned prefix = bytes == 2 ? 0xC0 : bytes == 3 ? 0xE0 : 0xF0; + buffer[written++] = static_cast(prefix | (codepoint >> (6 * (bytes - 1)))); + for (auto remaining = bytes - 1; remaining > 0; --remaining) { + buffer[written++] = static_cast(0x80 | ((codepoint >> (6 * (remaining - 1))) & 0x3F)); + } + } + } + buffer[written] = '\0'; + return written + 1; +} + +bool APS5_VABI JSStringIsEqual(const GuestJSString* first, const GuestJSString* second) { + return Equal(first, second); +} + +bool APS5_VABI JSStringIsEqualToUTF8CString(const GuestJSString* string, const char* text) { + if (!text) NotImplemented_nid_no_patch(__func__); + const auto other = Decode(text); + return Equal(string, other.get()); +} } diff --git a/core/libs/prx/libSceJsc/include/Api.hpp b/core/libs/prx/libSceJsc/include/Api.hpp new file mode 100644 index 0000000000..43b06b1625 --- /dev/null +++ b/core/libs/prx/libSceJsc/include/Api.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +struct GuestJSString { + std::atomic references{1}; + std::vector characters; + bool nullString = false; +}; + +extern "C" { + +GuestJSString* APS5_VABI JSStringCreateWithCharacters(const std::uint16_t* characters, std::size_t count); +GuestJSString* APS5_VABI JSStringCreateWithUTF8CString(const char* text); +GuestJSString* APS5_VABI JSStringRetain(GuestJSString* string); +void APS5_VABI JSStringRelease(GuestJSString* string); +std::size_t APS5_VABI JSStringGetLength(const GuestJSString* string); +const std::uint16_t* APS5_VABI JSStringGetCharactersPtr(const GuestJSString* string); +std::size_t APS5_VABI JSStringGetMaximumUTF8CStringSize(const GuestJSString* string); +std::size_t APS5_VABI JSStringGetUTF8CString(const GuestJSString* string, char* buffer, std::size_t bufferSize); +bool APS5_VABI JSStringIsEqual(const GuestJSString* first, const GuestJSString* second); +bool APS5_VABI JSStringIsEqualToUTF8CString(const GuestJSString* string, const char* text); + +} diff --git a/core/libs/prx/libSceJsc/libSceJsc.hpp b/core/libs/prx/libSceJsc/libSceJsc.hpp index 6f70f09bee..28a1869c2c 100644 --- a/core/libs/prx/libSceJsc/libSceJsc.hpp +++ b/core/libs/prx/libSceJsc/libSceJsc.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceJsc/include/Api.hpp" diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 3621baad39..7f27debea6 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ocsp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Stack.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ui.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rsa.cpp) +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Aes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ocsp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Stack.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Ui.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rsa.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Pkey.cpp) target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) @@ -121,6 +121,17 @@ if(BUILD_TESTING) set_tests_properties(guest_libressl_rsa PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_libressl_pkey_type_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslPkeyType.cpp) + target_include_directories(guest_libressl_pkey_type_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_pkey_type_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_pkey_type_tests) + add_test(NAME guest_libressl_pkey_type COMMAND guest_libressl_pkey_type_tests) + set_tests_properties(guest_libressl_pkey_type PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_pkey_type PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Cipher.cpp) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 00ace935b0..4ac6e5f647 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -639,11 +639,6 @@ int APS5_VABI EVP_PKEY_set1_RSA_nid_postfix(void) { return 0; } -int APS5_VABI EVP_PKEY_type_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_aes_128_cbc_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1304,11 +1299,6 @@ int APS5_VABI SSL_read_nid_postfix(void) { return 0; } -int APS5_VABI SSL_select_next_proto_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI SSL_set_SSL_CTX_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake b/core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake new file mode 100644 index 0000000000..111f818aec --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/ProtocolSelection.cmake @@ -0,0 +1,22 @@ +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/ProtocolSelection.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_protocol_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/GuestLibreSslProtocol.cpp) + target_include_directories(guest_libressl_protocol_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_protocol_tests PRIVATE ${CMAKE_DL_LIBS}) + if(WIN32) + target_link_libraries(guest_libressl_protocol_tests PRIVATE libc) + endif() + add_dependencies(guest_libressl_protocol_tests libs) + configure_windows_unwind(guest_libressl_protocol_tests) + add_test(NAME guest_libressl_protocol COMMAND guest_libressl_protocol_tests + $ SSL_select_next_proto_nid_postfix) + add_test(NAME guest_libressl_protocol_nid COMMAND guest_libressl_protocol_tests + ${PATCHED_DIR}/libSceLibreSsl.prx CpIzEs1Q1Ug) + if(WIN32) + set_tests_properties(guest_libressl_protocol PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_libressl_protocol_nid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index a114265d0c..1cbfc7d759 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -246,7 +246,7 @@ int APS5_VABI EVP_PKEY_get1_RSA_nid_postfix(void); int APS5_VABI EVP_PKEY_id_nid_postfix(void); int APS5_VABI EVP_PKEY_new_nid_postfix(void); int APS5_VABI EVP_PKEY_set1_RSA_nid_postfix(void); -int APS5_VABI EVP_PKEY_type_nid_postfix(void); +int APS5_VABI EVP_PKEY_type_nid_postfix(int type); int APS5_VABI EVP_aes_128_cbc_nid_postfix(void); int APS5_VABI EVP_aes_128_cfb8_nid_postfix(void); int APS5_VABI EVP_aes_128_ctr_nid_postfix(void); @@ -388,7 +388,7 @@ int APS5_VABI SSL_new_nid_postfix(void); int APS5_VABI SSL_peek_nid_postfix(void); int APS5_VABI SSL_pending_nid_postfix(void); int APS5_VABI SSL_read_nid_postfix(void); -int APS5_VABI SSL_select_next_proto_nid_postfix(void); +int APS5_VABI SSL_select_next_proto_nid_postfix(unsigned char** output, unsigned char* outputLength, const unsigned char* peer, std::uint32_t peerLength, const unsigned char* supported, std::uint32_t supportedLength); int APS5_VABI SSL_set_SSL_CTX_nid_postfix(void); int APS5_VABI SSL_set_bio_nid_postfix(void); int APS5_VABI SSL_set_connect_state_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp index 409df8adac..1773149e73 100644 --- a/core/libs/prx/libSceLibreSsl/src/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Asn1.cpp @@ -1,12 +1,13 @@ #include "prx/libSceLibreSsl/libSceLibreSsl.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceLibreSsl/include/Error.hpp" #include #include #include #include #include -#include +#include namespace { @@ -14,6 +15,19 @@ static_assert(sizeof(Asn1String) == 24); static_assert(offsetof(Asn1String, data) == 8); static_assert(offsetof(Asn1String, flags) == 16); +constexpr int Asn1Library = 13; +constexpr int AnyFunction = 0xfff; +constexpr int MallocFailure = 65; +constexpr int IllegalCharacters = 124; +constexpr int InvalidBmpStringLength = 129; +constexpr int InvalidUniversalStringLength = 133; +constexpr int InvalidUtf8String = 134; + +int Asn1Error(int reason) { + LibreSslPutError_nid_no_patch(Asn1Library, AnyFunction, reason); + return -1; +} + int CharacterWidth(int type) { switch (type) { case 12: return 0; @@ -52,18 +66,20 @@ bool ReadCharacter(std::span& input, int width, std::uint32 return value <= 0x10ffff && (value < 0xd800 || value > 0xdfff); } -void AppendCharacter(std::vector& output, std::uint32_t value) { - const int bytes = value < 0x80 ? 1 : value < 0x800 ? 2 : value < 0x10000 ? 3 : 4; - if (output.size() >= static_cast(std::numeric_limits::max() - bytes)) - NotImplemented_nid_no_patch("ASN1_STRING_to_UTF8: output length overflow"); +int CharacterBytes(std::uint32_t value) { + return value < 0x80 ? 1 : value < 0x800 ? 2 : value < 0x10000 ? 3 : 4; +} + +void WriteCharacter(unsigned char*& output, std::uint32_t value) { + const int bytes = CharacterBytes(value); if (bytes == 1) { - output.push_back(static_cast(value)); + *output++ = static_cast(value); return; } constexpr unsigned char prefix[] = {0, 0, 0xc0, 0xe0, 0xf0}; - output.push_back(static_cast(prefix[bytes] | (value >> (6 * (bytes - 1))))); + *output++ = static_cast(prefix[bytes] | (value >> (6 * (bytes - 1)))); for (int index = bytes - 2; index >= 0; --index) - output.push_back(static_cast(0x80 | ((value >> (6 * index)) & 0x3f))); + *output++ = static_cast(0x80 | ((value >> (6 * index)) & 0x3f)); } } // namespace @@ -106,23 +122,36 @@ int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, Asn1String const auto& string = *value; const int width = CharacterWidth(string.type); if (width < 0) return -1; - if (!output || (!string.data && string.length != 0)) NotImplemented_nid_no_patch(__func__); - std::size_t length = string.length < 0 ? std::strlen(reinterpret_cast(string.data)) + if (!string.data && string.length < 0) throw std::invalid_argument("ASN1_STRING_to_UTF8: null terminated-string data"); + const std::size_t length = string.length < 0 ? std::strlen(reinterpret_cast(string.data)) : static_cast(string.length); - if (length > static_cast(std::numeric_limits::max())) NotImplemented_nid_no_patch(__func__); - std::span input(string.data, length); - std::vector converted; - while (!input.empty()) { + if (length > static_cast(std::numeric_limits::max())) throw std::length_error("ASN1_STRING_to_UTF8: input length overflow"); + if (width == 2 && length % 2 != 0) return Asn1Error(InvalidBmpStringLength); + if (width == 4 && length % 4 != 0) return Asn1Error(InvalidUniversalStringLength); + if (!string.data && length != 0) throw std::invalid_argument("ASN1_STRING_to_UTF8: null nonempty string data"); + const std::span characters(string.data, length); + std::size_t size = 0; + for (auto input = characters; !input.empty();) { std::uint32_t character; - if (!ReadCharacter(input, width, character)) NotImplemented_nid_no_patch(__func__); - AppendCharacter(converted, character); + if (!ReadCharacter(input, width, character)) return Asn1Error(width == 0 ? InvalidUtf8String : IllegalCharacters); + size += static_cast(CharacterBytes(character)); + if (size > static_cast(std::numeric_limits::max() - 1)) throw std::length_error("ASN1_STRING_to_UTF8: output length overflow"); } - auto* result = static_cast(malloc_nid_postfix(converted.size() + 1)); - if (!result) NotImplemented_nid_no_patch(__func__); - if (!converted.empty()) std::memcpy(result, converted.data(), converted.size()); - result[converted.size()] = 0; + if (!output) throw std::invalid_argument("ASN1_STRING_to_UTF8: null output"); + auto* result = static_cast(malloc_nid_postfix(size + 1)); + if (!result) { + if (width == 0) Asn1Error(MallocFailure); + return Asn1Error(MallocFailure); + } + unsigned char* cursor = result; + for (auto input = characters; !input.empty();) { + std::uint32_t character; + ReadCharacter(input, width, character); + WriteCharacter(cursor, character); + } + *cursor = 0; *output = result; - return static_cast(converted.size()); + return static_cast(size); } } diff --git a/core/libs/prx/libSceLibreSsl/src/Pkey.cpp b/core/libs/prx/libSceLibreSsl/src/Pkey.cpp new file mode 100644 index 0000000000..244f5e5d36 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Pkey.cpp @@ -0,0 +1,44 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NidUndef = 0; +constexpr int NidRsaEncryption = 6; +constexpr int NidRsa = 19; +constexpr int NidDhKeyAgreement = 28; +constexpr int NidDsaWithSha = 66; +constexpr int NidDsa2 = 67; +constexpr int NidDsaWithSha1Old = 70; +constexpr int NidDsaWithSha1 = 113; +constexpr int NidDsa = 116; +constexpr int NidEcPublicKey = 408; +constexpr int NidHmac = 855; +constexpr int NidCmac = 894; + +} + +extern "C" { + +int APS5_VABI EVP_PKEY_type_nid_postfix(int type) { + switch (type) { + case NidRsaEncryption: + case NidRsa: + return NidRsaEncryption; + case NidDhKeyAgreement: + case NidEcPublicKey: + case NidHmac: + case NidCmac: + return type; + case NidDsaWithSha: + case NidDsa2: + case NidDsaWithSha1Old: + case NidDsaWithSha1: + case NidDsa: + return NidDsa; + default: + return NidUndef; + } +} + +} diff --git a/core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp b/core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp new file mode 100644 index 0000000000..537988df72 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp @@ -0,0 +1,37 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include + +namespace { + +bool ValidProtocols(const unsigned char* protocols, std::size_t size) { + if (!protocols || size == 0) return false; + for (std::size_t offset = 0; offset < size;) { + const std::size_t length = protocols[offset++]; + if (length == 0 || length > size - offset) return false; + offset += length; + } + return true; +} + +} + +extern "C" int APS5_VABI SSL_select_next_proto_nid_postfix(unsigned char** output, unsigned char* outputLength, + const unsigned char* peer, std::uint32_t peerLength, const unsigned char* supported, std::uint32_t supportedLength) { + if (!output || !outputLength || !ValidProtocols(peer, peerLength) || !ValidProtocols(supported, supportedLength)) + NotImplemented_nid_no_patch("SSL_select_next_proto: null output or invalid protocol list"); + for (std::size_t offered = 0; offered < peerLength; offered += 1 + peer[offered]) { + for (std::size_t local = 0; local < supportedLength; local += 1 + supported[local]) { + if (peer[offered] != supported[local] || std::memcmp(peer + offered + 1, supported + local + 1, peer[offered]) != 0) continue; + *output = const_cast(peer + offered + 1); + *outputLength = peer[offered]; + return 1; + } + } + *output = const_cast(supported + 1); + *outputLength = supported[0]; + return 2; +} diff --git a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp index e99142e9ad..d4e65b4303 100644 --- a/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/Asn1.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -63,6 +64,45 @@ void CheckConversion(int type, std::span input, std::span(reason); +} + +void RequireConversionError(unsigned char** output, int type, const unsigned char* data, int length, int reason, const char* message) { + ERR_clear_error_nid_postfix(); + Asn1String value{length, type, const_cast(data), 0}; + const unsigned char* before = output ? *output : nullptr; + Require(ASN1_STRING_to_UTF8_nid_postfix(output, &value) == -1, message); + Require(!output || *output == before, "a failed conversion leaves the output alone"); + Require(ERR_get_error_nid_postfix() == Asn1Error(reason) && ERR_get_error_nid_postfix() == 0, message); +} + +void MalformedStrings() { + unsigned char sentinel = 0xab; + unsigned char* result = &sentinel; + constexpr unsigned char oddBmp[] = {0, 0x41, 0}; + RequireConversionError(&result, 30, oddBmp, 3, 129, "odd BMPString length"); + RequireConversionError(&result, 30, nullptr, 1, 129, "BMPString length is checked before the data"); + constexpr unsigned char bmpSurrogate[] = {0, 0x41, 0xd8, 0}; + RequireConversionError(&result, 30, bmpSurrogate, 4, 124, "BMPString surrogate"); + constexpr unsigned char shortUniversal[] = {0, 0, 0, 0x41, 0}; + RequireConversionError(&result, 28, shortUniversal, 5, 133, "UniversalString length not a multiple of four"); + RequireConversionError(&result, 28, nullptr, 2, 133, "UniversalString length is checked before the data"); + constexpr unsigned char universalBeyond[] = {0, 0x11, 0, 0}; + RequireConversionError(&result, 28, universalBeyond, 4, 124, "UniversalString above U+10FFFF"); + constexpr unsigned char universalSurrogate[] = {0, 0, 0xdc, 0}; + RequireConversionError(&result, 28, universalSurrogate, 4, 124, "UniversalString surrogate"); + constexpr unsigned char overlong[] = {0xc0, 0x80}; + constexpr unsigned char truncated[] = {'A', 0xe2, 0x82}; + constexpr unsigned char surrogate[] = {0xed, 0xa0, 0x80}; + constexpr unsigned char beyond[] = {0xf5, 0x80, 0x80, 0x80}; + constexpr unsigned char continuation[] = {0x80}; + for (const auto& [data, length] : {std::pair{overlong, 2}, std::pair{truncated, 3}, std::pair{surrogate, 3}, + std::pair{beyond, 4}, std::pair{continuation, 1}}) + RequireConversionError(&result, 12, data, length, 134, "malformed UTF8String"); + RequireConversionError(nullptr, 30, oddBmp, 3, 129, "a malformed string fails before the output is used"); +} + void Accessors() { unsigned char data[] = {'a', 0, 'b'}; Asn1String value{3, 12, data, std::numeric_limits::max()}; @@ -185,12 +225,13 @@ void Strings() { std::size_t lastAllocationSize = 0; int allocationCount = 0; +bool failAllocation = false; int freeCount = 0; void* APS5_VABI Allocate_nid_no_patch(std::size_t size) { lastAllocationSize = size; ++allocationCount; - return std::malloc(size); + return failAllocation ? nullptr : std::malloc(size); } void APS5_VABI Free_nid_no_patch(void* pointer) { @@ -214,6 +255,32 @@ void Allocator() { constexpr unsigned char expected[] = {0xc3, 0xbf}; CheckConversion(20, input, expected); Require(allocationCount == 1 && lastAllocationSize == 3 && freeCount == 1, "conversion uses the application allocator"); + constexpr unsigned char universal[] = {0, 0, 0, 'A', 0, 0, 0x03, 0xa9, 0, 0, 0x20, 0xac, 0, 0x01, 0xf6, 0x00}; + constexpr unsigned char mixed[] = {'A', 0xce, 0xa9, 0xe2, 0x82, 0xac, 0xf0, 0x9f, 0x98, 0x80}; + CheckConversion(28, universal, mixed); + Require(allocationCount == 2 && lastAllocationSize == sizeof(mixed) + 1 && freeCount == 2, + "a conversion with every UTF-8 length makes one allocation of the exact size"); + unsigned char badUtf8[] = {'A', 0xc0, 0x80}; + Asn1String malformed{3, 12, badUtf8, 0}; + unsigned char* untouched = nullptr; + ERR_clear_error_nid_postfix(); + Require(ASN1_STRING_to_UTF8_nid_postfix(&untouched, &malformed) == -1 && untouched == nullptr && allocationCount == 2, + "a malformed string allocates nothing"); + ERR_clear_error_nid_postfix(); + failAllocation = true; + unsigned char sentinel = 0xab; + unsigned char* result = &sentinel; + unsigned char latin[] = {0xff}; + Asn1String converted{1, 20, latin, 0}; + ERR_clear_error_nid_postfix(); + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &converted) == -1 && result == &sentinel, "allocation failure of a converted string"); + Require(ERR_get_error_nid_postfix() == Asn1Error(65) && ERR_get_error_nid_postfix() == 0, "one malloc failure for a converted string"); + unsigned char utf8[] = {'A'}; + Asn1String copied{1, 12, utf8, 0}; + Require(ASN1_STRING_to_UTF8_nid_postfix(&result, &copied) == -1 && result == &sentinel, "allocation failure of a UTF8String copy"); + Require(ERR_get_error_nid_postfix() == Asn1Error(65) && ERR_get_error_nid_postfix() == Asn1Error(65) && ERR_get_error_nid_postfix() == 0, + "ASN1_STRING_set and ASN1_mbstring_ncopy both report the malloc failure of a UTF8String copy"); + failAllocation = false; } } // namespace @@ -228,4 +295,5 @@ int main(int argc, char**) { Accessors(); Integers(); Strings(); + MalformedStrings(); } diff --git a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt index 9f5ecac040..c787bccfea 100644 --- a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibreSsl3 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/IntegerDecoder.cpp) +add_library(libSceLibreSsl3 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/IntegerDecoder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Key.cpp) target_include_directories(libSceLibreSsl3 PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl3 PRIVATE libSceLibreSsl libc) configure_windows_unwind(libSceLibreSsl3) @@ -21,8 +21,14 @@ if(BUILD_TESTING) configure_windows_unwind(guest_libressl3_asn1_tests) add_test(NAME guest_libressl3_asn1 COMMAND guest_libressl3_asn1_tests) set_tests_properties(guest_libressl3_asn1 PROPERTIES TIMEOUT 30) + add_test_executable(guest_libressl3_key_reference_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSsl3KeyReference.cpp) + target_include_directories(guest_libressl3_key_reference_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl3_key_reference_tests PRIVATE libSceLibreSsl3) + configure_windows_unwind(guest_libressl3_key_reference_tests) + add_test(NAME guest_libressl3_key_reference COMMAND guest_libressl3_key_reference_tests) + set_tests_properties(guest_libressl3_key_reference PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_crypto_memcmp guest_libressl3_digest guest_libressl3_asn1 PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_crypto_memcmp guest_libressl3_digest guest_libressl3_asn1 guest_libressl3_key_reference PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceLibreSsl3/Export.cpp b/core/libs/prx/libSceLibreSsl3/Export.cpp index e2fa6b0ef4..0f7cb0e894 100644 --- a/core/libs/prx/libSceLibreSsl3/Export.cpp +++ b/core/libs/prx/libSceLibreSsl3/Export.cpp @@ -61,11 +61,6 @@ int APS5_VABI DSA_get0_pqg_nid_postfix(void) { return 0; } -int APS5_VABI EC_KEY_up_ref_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI ERR_load_crypto_strings_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -181,11 +176,6 @@ int APS5_VABI EVP_PKEY_size_nid_postfix(void) { return 0; } -int APS5_VABI EVP_PKEY_up_ref_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_SignFinal_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl3/include/Api.hpp b/core/libs/prx/libSceLibreSsl3/include/Api.hpp index 396e514cae..49ba411188 100644 --- a/core/libs/prx/libSceLibreSsl3/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl3/include/Api.hpp @@ -1,6 +1,7 @@ #pragma once #include "SceTypes.hpp" +#include "prx/libSceLibreSsl3/include/Key.hpp" #include "prx/libSceLibreSsl/include/Api.hpp" extern "C" { @@ -15,7 +16,7 @@ int APS5_VABI DH_get0_key_nid_postfix(void); int APS5_VABI DH_get0_pqg_nid_postfix(void); int APS5_VABI DSA_get0_key_nid_postfix(void); int APS5_VABI DSA_get0_pqg_nid_postfix(void); -int APS5_VABI EC_KEY_up_ref_nid_postfix(void); +int APS5_VABI EC_KEY_up_ref_nid_postfix(LibreSsl3EcKey* key); int APS5_VABI ERR_load_crypto_strings_nid_postfix(void); int APS5_VABI ERR_peek_last_error_nid_postfix(void); int APS5_VABI ERR_print_errors_cb_nid_postfix(void); @@ -39,7 +40,7 @@ int APS5_VABI EVP_PKEY_get0_RSA_nid_postfix(void); int APS5_VABI EVP_PKEY_new_mac_key_nid_postfix(void); int APS5_VABI EVP_PKEY_set1_EC_KEY_nid_postfix(void); int APS5_VABI EVP_PKEY_size_nid_postfix(void); -int APS5_VABI EVP_PKEY_up_ref_nid_postfix(void); +int APS5_VABI EVP_PKEY_up_ref_nid_postfix(LibreSsl3EvpPkey* key); int APS5_VABI EVP_SignFinal_nid_postfix(void); int APS5_VABI EVP_VerifyFinal_nid_postfix(void); int APS5_VABI EVP_aes_128_wrap_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl3/include/Key.hpp b/core/libs/prx/libSceLibreSsl3/include/Key.hpp new file mode 100644 index 0000000000..bc0a301686 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/include/Key.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +struct LibreSsl3EvpPkey { + std::int32_t type; + std::int32_t references; + const void* method; + void* key; + std::int32_t saveParameters; +}; + +struct LibreSsl3EcKey { + const void* method; + std::int32_t version; + void* group; + void* publicKey; + void* privateKey; + std::uint32_t encodingFlags; + std::int32_t conversionForm; + std::int32_t references; + std::int32_t flags; + void* extraData; +}; + +static_assert(sizeof(LibreSsl3EvpPkey) == 32); +static_assert(offsetof(LibreSsl3EvpPkey, references) == 4); +static_assert(sizeof(LibreSsl3EcKey) == 64); +static_assert(offsetof(LibreSsl3EcKey, references) == 48); diff --git a/core/libs/prx/libSceLibreSsl3/src/Key.cpp b/core/libs/prx/libSceLibreSsl3/src/Key.cpp new file mode 100644 index 0000000000..d5c4b7a856 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/src/Key.cpp @@ -0,0 +1,38 @@ +#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp" + +#include +#include +#include + +namespace { + +static_assert(std::atomic_ref::required_alignment <= alignof(std::int32_t)); + +int Retain_nid_no_patch(std::int32_t& references) { + std::atomic_ref count(references); + auto previous = count.load(); + for (;;) { + if (previous == std::numeric_limits::max()) + throw std::overflow_error("LibreSSL3 key reference count overflow"); + if (count.compare_exchange_weak(previous, previous + 1)) + return previous > 0; + } +} + +} // namespace + +extern "C" { + +int APS5_VABI EVP_PKEY_up_ref_nid_postfix(LibreSsl3EvpPkey* key) { + if (!key) + throw std::invalid_argument("EVP_PKEY_up_ref requires a key"); + return Retain_nid_no_patch(key->references); +} + +int APS5_VABI EC_KEY_up_ref_nid_postfix(LibreSsl3EcKey* key) { + if (!key) + throw std::invalid_argument("EC_KEY_up_ref requires a key"); + return Retain_nid_no_patch(key->references); +} + +} diff --git a/core/libs/prx/libSceMetadataReaderWriter/Export.cpp b/core/libs/prx/libSceMetadataReaderWriter/Export.cpp index d81f4a5e32..6cdea6e0d3 100644 --- a/core/libs/prx/libSceMetadataReaderWriter/Export.cpp +++ b/core/libs/prx/libSceMetadataReaderWriter/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceMetadataReaderWriter/include/Api.hpp" #include #include #include "SceTypes.hpp" @@ -5,6 +6,199 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI _ZN23sceMetadataReaderWriter10initializeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter11getMimeTypeERKSsRSt6vectorIjSaIjEE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter12getMimeTypesERKSsiPi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter12readMetadataERKSsRKNS_8MetadataEjRS2_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEj() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEjPS2_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter15ParserInterfaceC1ERKS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter15ParserInterfaceC2ERKS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter16extractThumbnailERKSsjRNS_9ThumbnailE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter16getNumOfMimeTypeERKSsPi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter17getParserInfoListERSt6vectorINS_10ParserInfoESaIS1_EE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter18registerParserInfoERKSt6vectorINS_10ParserInfoESaIS1_EE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter20unregisterParserInfoEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter23registerParserInterfaceEjRKNS_15ParserInterfaceE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter5ValueaSERS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata11DeserializeERN3sce4Json5ValueERS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata11deleteFieldERKSs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata15iterateAllFieldEPFiRKSsRKNS_5ValueEbE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueERb() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSs() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSsRKNS_5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata9SerializeERS0_RN3sce4Json5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1EPFPvmEPFvS1_E() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_PFPvmEPFvS3_E() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2EPFPvmEPFvS1_E() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_PFPvmEPFvS3_E() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD0Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataaSERKS0_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN23sceMetadataReaderWriter8finalizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata13getFieldCountEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata8getValueERKSsRNS_5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata9dumpValueEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceMetadataReaderWriter/include/Api.hpp b/core/libs/prx/libSceMetadataReaderWriter/include/Api.hpp new file mode 100644 index 0000000000..bcf8ad80e7 --- /dev/null +++ b/core/libs/prx/libSceMetadataReaderWriter/include/Api.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI _ZN23sceMetadataReaderWriter10initializeEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter11getMimeTypeERKSsRSt6vectorIjSaIjEE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter12getMimeTypesERKSsiPi(void); +int APS5_VABI _ZN23sceMetadataReaderWriter12readMetadataERKSsRKNS_8MetadataEjRS2_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEj(void); +int APS5_VABI _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEjPS2_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter15ParserInterfaceC1ERKS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter15ParserInterfaceC2ERKS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter16extractThumbnailERKSsjRNS_9ThumbnailE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter16getNumOfMimeTypeERKSsPi(void); +int APS5_VABI _ZN23sceMetadataReaderWriter17getParserInfoListERSt6vectorINS_10ParserInfoESaIS1_EE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter18registerParserInfoERKSt6vectorINS_10ParserInfoESaIS1_EE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter20unregisterParserInfoEv(void); +int APS5_VABI _ZN23sceMetadataReaderWriter23registerParserInterfaceEjRKNS_15ParserInterfaceE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter5ValueaSERS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata11DeserializeERN3sce4Json5ValueERS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata11deleteFieldERKSs(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata15iterateAllFieldEPFiRKSsRKNS_5ValueEbE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueERb(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSs(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSsRKNS_5ValueE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8Metadata9SerializeERS0_RN3sce4Json5ValueE(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1EPFPvmEPFvS1_E(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_PFPvmEPFvS3_E(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC1Ev(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2EPFPvmEPFvS1_E(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_PFPvmEPFvS3_E(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataC2Ev(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD0Ev(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD1Ev(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataD2Ev(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8MetadataaSERKS0_(void); +int APS5_VABI _ZN23sceMetadataReaderWriter8finalizeEv(void); +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata13getFieldCountEv(void); +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueE(void); +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata8getValueERKSsRNS_5ValueE(void); +int APS5_VABI _ZNK23sceMetadataReaderWriter8Metadata9dumpValueEv(void); + +} diff --git a/core/libs/prx/libSceMetadataReaderWriter/libSceMetadataReaderWriter.hpp b/core/libs/prx/libSceMetadataReaderWriter/libSceMetadataReaderWriter.hpp index 6f70f09bee..4e504bad70 100644 --- a/core/libs/prx/libSceMetadataReaderWriter/libSceMetadataReaderWriter.hpp +++ b/core/libs/prx/libSceMetadataReaderWriter/libSceMetadataReaderWriter.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libSceMetadataReaderWriter/include/Api.hpp" diff --git a/core/libs/prx/libSceNetCtl/CMakeLists.txt b/core/libs/prx/libSceNetCtl/CMakeLists.txt index 0083a712af..6907cd9fb7 100644 --- a/core/libs/prx/libSceNetCtl/CMakeLists.txt +++ b/core/libs/prx/libSceNetCtl/CMakeLists.txt @@ -3,6 +3,16 @@ target_include_directories(libSceNetCtl PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNetCtl) if(BUILD_TESTING) + add_test_executable(guest_net_ctl_v6_callbacks_tests tests/V6Callbacks.cpp) + target_include_directories(guest_net_ctl_v6_callbacks_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_ctl_v6_callbacks_tests PRIVATE libSceNetCtl libc) + configure_windows_unwind(guest_net_ctl_v6_callbacks_tests) + add_test(NAME guest_net_ctl_v6_callbacks COMMAND guest_net_ctl_v6_callbacks_tests) + set_tests_properties(guest_net_ctl_v6_callbacks PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_net_ctl_v6_callbacks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_net_ctl_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetCtl.cpp) target_include_directories(guest_net_ctl_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_net_ctl_tests PRIVATE libSceNetCtl libc) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index a5e469dfdd..c3cb3b3a08 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -9,8 +9,10 @@ #include #include #include +#include #include #include +#include #include #include #ifdef __linux__ @@ -21,18 +23,22 @@ #endif #endif #include +#include +#include #include #include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/HostEntropy.hpp" #include #include #include #include #include #include +#include #include // Network-control state reflects host interface addressing, not PSN sign-in status. @@ -49,6 +55,25 @@ static constexpr int NET_CTL_STATE_DISCONNECTED = 0; static constexpr int NET_CTL_STATE_IPOBTAINED = 3; static constexpr int NET_CTL_EVENT_DISCONNECTED = 1; static constexpr int NET_CTL_EVENT_IPOBTAINED = 3; +static constexpr int NET_CTL_STUN_STATUS_FAILED = 1; +static constexpr int NET_CTL_STUN_STATUS_OK = 2; +static constexpr int NET_CTL_NAT_TYPE_NONE = 0; +static constexpr int NET_CTL_NAT_TYPE_1 = 1; +static constexpr int NET_CTL_NAT_TYPE_2 = 2; +static constexpr int NET_CTL_NAT_TYPE_3 = 3; +static constexpr std::uint16_t STUN_BINDING_REQUEST = 0x0001; +static constexpr std::uint16_t STUN_BINDING_RESPONSE = 0x0101; +static constexpr std::uint32_t STUN_MAGIC_COOKIE = 0x2112A442u; +static constexpr std::uint16_t STUN_ATTR_MAPPED_ADDRESS = 0x0001; +static constexpr std::uint16_t STUN_ATTR_CHANGED_ADDRESS = 0x0005; +static constexpr std::uint16_t STUN_ATTR_XOR_MAPPED_ADDRESS = 0x0020; +static constexpr std::uint16_t STUN_ATTR_XOR_MAPPED_ADDRESS_LEGACY = 0x8020; +static constexpr std::uint16_t STUN_ATTR_OTHER_ADDRESS = 0x802b; +static constexpr std::size_t STUN_HEADER_SIZE = 20; +static constexpr std::size_t STUN_TRANSACTION_ID_SIZE = 12; +static constexpr int STUN_DEFAULT_TIMEOUT_MS = 300; +static constexpr int STUN_TOTAL_BUDGET_MS = 1000; +static constexpr int STUN_CACHE_TTL_MS = 60000; static constexpr int NET_CTL_INFO_DEVICE = 1; static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; static constexpr int NET_CTL_INFO_MTU = 3; @@ -457,6 +482,346 @@ extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, return host.dns.size(); } +#ifdef _WIN32 +using StunSocket = SOCKET; +static constexpr StunSocket INVALID_STUN_SOCKET = INVALID_SOCKET; +static void CloseStunSocket(StunSocket descriptor) { closesocket(descriptor); } +static bool EnsureSocketRuntime() { + static const int status = [] { + WSADATA data{}; + return WSAStartup(MAKEWORD(2, 2), &data); + }(); + return status == 0; +} +#else +using StunSocket = int; +static constexpr StunSocket INVALID_STUN_SOCKET = -1; +static void CloseStunSocket(StunSocket descriptor) { close(descriptor); } +static bool EnsureSocketRuntime() { return true; } +#endif + +struct StunEndpoint { + std::uint32_t address = 0; + std::uint16_t port = 0; + bool valid = false; +}; + +struct StunProbeResponse { + StunEndpoint mapped; + StunEndpoint other; +}; + +struct StunConfig { + std::mutex lock; + bool overrideActive = false; + std::vector overrideServers; + int overrideTimeoutMs = STUN_DEFAULT_TIMEOUT_MS; + std::uint64_t configGeneration = 1; + std::uint64_t cachedConfigGeneration = 0; + std::uint32_t cachedLocalAddr = 0; + std::chrono::steady_clock::time_point cachedUntil{}; + bool hasCachedResult = false; + NetCtlNatInfo cachedResult{}; +}; + +static StunConfig& GetStunConfig() { + static StunConfig config; + return config; +} + +extern "C" void NetCtlSetStunServers_nid_no_patch(const sockaddr_in* servers, std::size_t count, int timeoutMs) { + auto& config = GetStunConfig(); + std::lock_guard guard(config.lock); + ++config.configGeneration; + config.hasCachedResult = false; + config.overrideServers.clear(); + if (!servers && count == 0 && timeoutMs <= 0) { + config.overrideActive = false; + config.overrideTimeoutMs = STUN_DEFAULT_TIMEOUT_MS; + return; + } + config.overrideActive = true; + config.overrideTimeoutMs = timeoutMs > 0 ? timeoutMs : STUN_DEFAULT_TIMEOUT_MS; + if (servers && count > 0) config.overrideServers.assign(servers, servers + count); +} + +static void AppendUniqueStunServer(std::vector& servers, const sockaddr_in& candidate) { + if (candidate.sin_family != AF_INET || candidate.sin_addr.s_addr == 0 || candidate.sin_port == 0) return; + for (const auto& existing : servers) { + if (existing.sin_addr.s_addr == candidate.sin_addr.s_addr && existing.sin_port == candidate.sin_port) return; + } + servers.push_back(candidate); +} + +static std::vector ResolveStunServers(int& timeoutMs, std::uint64_t& generationOut, std::chrono::steady_clock::time_point totalDeadline) { + auto& config = GetStunConfig(); + { + std::lock_guard guard(config.lock); + generationOut = config.configGeneration; + if (config.overrideActive) { + timeoutMs = config.overrideTimeoutMs; + return config.overrideServers; + } + } + timeoutMs = STUN_DEFAULT_TIMEOUT_MS; + struct DefaultStunHost { + const char* host; + const char* port; + }; + static constexpr DefaultStunHost kDefaultHosts[] = { + {"stun.playstation.net", "3478"}, + {"stun.cloudflare.com", "3478"}, + {"stun.l.google.com", "19302"}, + {"stun1.l.google.com", "19302"}, + }; + struct DnsQueryState { + std::mutex lock; + std::condition_variable cv; + std::vector servers; + bool done = false; + }; + const auto now = std::chrono::steady_clock::now(); + if (!EnsureSocketRuntime() || now >= totalDeadline) return {}; + const auto dnsCap = now + std::chrono::milliseconds(STUN_DEFAULT_TIMEOUT_MS * 2); + const auto dnsDeadline = dnsCap < totalDeadline ? dnsCap : totalDeadline; + auto state = std::make_shared(); + std::thread([state, dnsDeadline] { + for (const auto& entry : kDefaultHosts) { + if (std::chrono::steady_clock::now() >= dnsDeadline) break; + addrinfo hints{}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + addrinfo* list = nullptr; + if (getaddrinfo(entry.host, entry.port, &hints, &list) != 0 || !list) continue; + { + std::lock_guard guard(state->lock); + for (auto* item = list; item; item = item->ai_next) { + if (item->ai_addr && item->ai_addrlen >= sizeof(sockaddr_in) && item->ai_addr->sa_family == AF_INET) { + AppendUniqueStunServer(state->servers, *reinterpret_cast(item->ai_addr)); + } + } + } + freeaddrinfo(list); + } + { + std::lock_guard guard(state->lock); + state->done = true; + } + state->cv.notify_one(); + }).detach(); + std::unique_lock lock(state->lock); + state->cv.wait_until(lock, dnsDeadline, [&] { return state->done; }); + return state->servers; +} + +static StunEndpoint DecodeStunIpv4Endpoint(const std::uint8_t* value, std::uint16_t length, bool xorMask) { + StunEndpoint endpoint{}; + if (length < 8 || value[1] != 0x01) return endpoint; + const auto rawPort = static_cast((static_cast(value[2]) << 8) | value[3]); + std::uint32_t rawAddress = 0; + std::memcpy(&rawAddress, value + 4, sizeof(rawAddress)); + endpoint.port = xorMask ? static_cast(rawPort ^ static_cast(STUN_MAGIC_COOKIE >> 16)) : rawPort; + endpoint.address = xorMask ? (rawAddress ^ htonl(STUN_MAGIC_COOKIE)) : rawAddress; + endpoint.valid = endpoint.address != 0 && endpoint.port != 0; + return endpoint; +} + +static bool ParseStunBindingResponse(const std::uint8_t* packet, std::size_t size, const std::uint8_t* transactionId, StunProbeResponse& response) { + if (size < STUN_HEADER_SIZE) return false; + const auto type = static_cast((static_cast(packet[0]) << 8) | packet[1]); + const auto length = static_cast((static_cast(packet[2]) << 8) | packet[3]); + if (type != STUN_BINDING_RESPONSE || (length & 3u) != 0 || STUN_HEADER_SIZE + length > size) return false; + const std::uint32_t expectedCookie = htonl(STUN_MAGIC_COOKIE); + if (std::memcmp(packet + 4, &expectedCookie, sizeof(expectedCookie)) != 0) return false; + if (std::memcmp(packet + 8, transactionId, STUN_TRANSACTION_ID_SIZE) != 0) return false; + response = {}; + bool hasXorMapped = false; + const std::size_t end = STUN_HEADER_SIZE + length; + for (std::size_t offset = STUN_HEADER_SIZE; offset < end;) { + if (offset + 4 > end) return false; + const auto attrType = static_cast((static_cast(packet[offset]) << 8) | packet[offset + 1]); + const auto attrLen = static_cast((static_cast(packet[offset + 2]) << 8) | packet[offset + 3]); + const std::size_t valueOffset = offset + 4; + if (valueOffset + attrLen > end) return false; + const std::uint8_t* value = packet + valueOffset; + if (attrType == STUN_ATTR_XOR_MAPPED_ADDRESS || attrType == STUN_ATTR_XOR_MAPPED_ADDRESS_LEGACY) { + const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, true); + if (endpoint.valid) { + response.mapped = endpoint; + hasXorMapped = true; + } + } else if (attrType == STUN_ATTR_MAPPED_ADDRESS && !hasXorMapped) { + const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, false); + if (endpoint.valid) response.mapped = endpoint; + } else if (attrType == STUN_ATTR_OTHER_ADDRESS || attrType == STUN_ATTR_CHANGED_ADDRESS) { + const auto endpoint = DecodeStunIpv4Endpoint(value, attrLen, false); + if (endpoint.valid) response.other = endpoint; + } + offset = valueOffset + ((static_cast(attrLen) + 3u) & ~std::size_t(3u)); + } + return response.mapped.valid; +} + +static bool ProbeStunServer(StunSocket descriptor, const sockaddr_in& server, int timeoutMs, std::chrono::steady_clock::time_point totalDeadline, StunProbeResponse& response) { + const auto start = std::chrono::steady_clock::now(); + if (start >= totalDeadline) return false; + std::uint8_t request[STUN_HEADER_SIZE] = {}; + request[0] = static_cast(STUN_BINDING_REQUEST >> 8); + request[1] = static_cast(STUN_BINDING_REQUEST & 0xFF); + const std::uint32_t cookie = htonl(STUN_MAGIC_COOKIE); + std::memcpy(request + 4, &cookie, sizeof(cookie)); + HostEntropy_nid_no_patch(request + 8, STUN_TRANSACTION_ID_SIZE); + const auto sent = sendto(descriptor, reinterpret_cast(request), sizeof(request), 0, + reinterpret_cast(&server), sizeof(server)); + if (sent != static_cast(sizeof(request))) return false; + const auto probeDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs > 0 ? timeoutMs : 1); + const auto deadline = probeDeadline < totalDeadline ? probeDeadline : totalDeadline; + for (;;) { + const auto now = std::chrono::steady_clock::now(); + if (now >= deadline) return false; + const auto remainingUs = std::chrono::duration_cast(deadline - now).count(); + fd_set readSet; + FD_ZERO(&readSet); + FD_SET(descriptor, &readSet); + timeval tv{}; + tv.tv_sec = static_cast(remainingUs / 1000000); + tv.tv_usec = static_cast(remainingUs % 1000000); + if (tv.tv_sec == 0 && tv.tv_usec == 0) tv.tv_usec = 1000; +#ifdef _WIN32 + const int ready = select(0, &readSet, nullptr, nullptr, &tv); +#else + const int ready = select(descriptor + 1, &readSet, nullptr, nullptr, &tv); +#endif + if (ready <= 0) return false; + std::uint8_t buffer[512]; + sockaddr_in from{}; +#ifdef _WIN32 + int fromLen = sizeof(from); +#else + socklen_t fromLen = sizeof(from); +#endif + const auto received = recvfrom(descriptor, reinterpret_cast(buffer), sizeof(buffer), 0, + reinterpret_cast(&from), &fromLen); +#ifdef _WIN32 + if (received < 0) { + if (WSAGetLastError() == WSAECONNRESET) continue; + return false; + } +#else + if (received < 0) return false; +#endif + if (received == 0) return false; + if (ParseStunBindingResponse(buffer, static_cast(received), request + 8, response)) return true; + } +} + +static NetCtlNatInfo DiscoverNatInfo(in_addr localAddress) { + NetCtlNatInfo result{}; + result.size = sizeof(NetCtlNatInfo); + result.stun_status = NET_CTL_STUN_STATUS_FAILED; + result.nat_type = NET_CTL_NAT_TYPE_NONE; + result.mapped_addr = 0; + + const auto totalDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(STUN_TOTAL_BUDGET_MS); + int timeoutMs = STUN_DEFAULT_TIMEOUT_MS; + std::uint64_t generation = 0; + const auto servers = ResolveStunServers(timeoutMs, generation, totalDeadline); + if (servers.empty() || !EnsureSocketRuntime()) return result; + + const StunSocket descriptor = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (descriptor == INVALID_STUN_SOCKET) return result; + +#ifdef _WIN32 + BOOL newBehavior = FALSE; + DWORD bytesReturned = 0; + WSAIoctl(descriptor, _WSAIOW(IOC_VENDOR, 12), &newBehavior, sizeof(newBehavior), nullptr, 0, &bytesReturned, nullptr, nullptr); +#endif + + sockaddr_in bindAddr{}; + bindAddr.sin_family = AF_INET; + bindAddr.sin_addr = (ntohl(servers[0].sin_addr.s_addr) >> 24) == 127 ? servers[0].sin_addr : localAddress; + bindAddr.sin_port = 0; + if (bind(descriptor, reinterpret_cast(&bindAddr), sizeof(bindAddr)) != 0) { + CloseStunSocket(descriptor); + return result; + } + + sockaddr_in boundAddr{}; +#ifdef _WIN32 + int boundLen = sizeof(boundAddr); +#else + socklen_t boundLen = sizeof(boundAddr); +#endif + if (getsockname(descriptor, reinterpret_cast(&boundAddr), &boundLen) != 0) { + CloseStunSocket(descriptor); + return result; + } + const std::uint16_t localPort = ntohs(boundAddr.sin_port); + + StunProbeResponse firstResponse{}; + sockaddr_in primaryServer{}; + std::size_t primaryIndex = servers.size(); + for (std::size_t i = 0; i < servers.size(); ++i) { + if (ProbeStunServer(descriptor, servers[i], timeoutMs, totalDeadline, firstResponse)) { + primaryServer = servers[i]; + primaryIndex = i; + break; + } + } + if (primaryIndex == servers.size()) { + CloseStunSocket(descriptor); + return result; + } + + if (firstResponse.mapped.address == localAddress.s_addr && firstResponse.mapped.port == localPort) { + CloseStunSocket(descriptor); + result.stun_status = NET_CTL_STUN_STATUS_OK; + result.nat_type = NET_CTL_NAT_TYPE_1; + result.mapped_addr = firstResponse.mapped.address; + return result; + } + + std::vector secondaryServers; + if (firstResponse.other.valid) { + sockaddr_in otherServer{}; + otherServer.sin_family = AF_INET; + otherServer.sin_addr.s_addr = firstResponse.other.address; + otherServer.sin_port = htons(firstResponse.other.port); + if (otherServer.sin_addr.s_addr != primaryServer.sin_addr.s_addr || otherServer.sin_port != primaryServer.sin_port) { + AppendUniqueStunServer(secondaryServers, otherServer); + } + if (otherServer.sin_port != primaryServer.sin_port) { + sockaddr_in sameIpOtherPort = primaryServer; + sameIpOtherPort.sin_port = otherServer.sin_port; + AppendUniqueStunServer(secondaryServers, sameIpOtherPort); + } + } + for (std::size_t i = 0; i < servers.size(); ++i) { + if (i == primaryIndex) continue; + if (servers[i].sin_addr.s_addr != primaryServer.sin_addr.s_addr || servers[i].sin_port != primaryServer.sin_port) { + AppendUniqueStunServer(secondaryServers, servers[i]); + } + } + + for (const auto& secondary : secondaryServers) { + StunProbeResponse secondResponse{}; + if (ProbeStunServer(descriptor, secondary, timeoutMs, totalDeadline, secondResponse)) { + CloseStunSocket(descriptor); + result.stun_status = NET_CTL_STUN_STATUS_OK; + result.nat_type = (secondResponse.mapped.address == firstResponse.mapped.address && + secondResponse.mapped.port == firstResponse.mapped.port) + ? NET_CTL_NAT_TYPE_2 + : NET_CTL_NAT_TYPE_3; + result.mapped_addr = firstResponse.mapped.address; + return result; + } + } + + CloseStunSocket(descriptor); + return result; +} + static void WriteInfoWord(NetCtlInfo* info, std::uint32_t value) { std::memcpy(info->opaque, &value, sizeof(value)); } @@ -504,28 +869,41 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { #endif } -static int CheckCallbacks(CallbackSlot (&callbacks)[MAX_CALLBACKS]) { +static int CheckCallbacks(CallbackSlot (&callbacks)[MAX_CALLBACKS], bool includeV6 = false) { if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; - if (!HasCallback(callbacks)) return 0; - const int event = HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED; - CallbackSlot due[MAX_CALLBACKS]; + const bool primary = HasCallback(callbacks); + const bool ipv6 = includeV6 && HasCallback(g_callbacksV6); + if (!primary && !ipv6) return 0; + const int primaryEvent = primary ? (HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED) : 0; + const int ipv6Event = ipv6 ? (HostHasAddress(AF_INET6) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED) : 0; + struct Delivery { + CallbackSlot callback; + int event; + }; + Delivery due[2 * MAX_CALLBACKS]; int count = 0; { std::lock_guard lock(g_callbackLock); - for (auto& slot : callbacks) { - if (!slot.func || slot.lastEvent == event) continue; - slot.lastEvent = event; - due[count++] = slot; - } + const auto collect = [&](CallbackSlot (&slots)[MAX_CALLBACKS], int event) { + if (event == 0) return; + for (auto& slot : slots) { + if (!slot.func || slot.lastEvent == event) continue; + slot.lastEvent = event; + due[count++] = {slot, event}; + } + }; + collect(callbacks, primaryEvent); + if (includeV6) collect(g_callbacksV6, ipv6Event); } - for (int index = 0; index < count; ++index) due[index].func(event, due[index].arg); + for (int index = 0; index < count; ++index) + due[index].callback.func(due[index].event, due[index].callback.arg); return 0; } extern "C" { int APS5_VABI sceNetCtlCheckCallback(void) { - return CheckCallbacks(g_callbacks); + return CheckCallbacks(g_callbacks, true); } int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { @@ -574,7 +952,33 @@ int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* info) { if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; if (info->size != sizeof(NetCtlNatInfo)) return SCE_NET_CTL_ERROR_INVALID_SIZE; - throw std::logic_error("sceNetCtlGetNatInfo: NAT discovery is not implemented"); + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + auto& config = GetStunConfig(); + std::uint64_t generation = 0; + const auto now = std::chrono::steady_clock::now(); + { + std::lock_guard guard(config.lock); + generation = config.configGeneration; + if (config.hasCachedResult && config.cachedConfigGeneration == generation && + config.cachedLocalAddr == host.address.s_addr && now < config.cachedUntil) { + *info = config.cachedResult; + return 0; + } + } + const NetCtlNatInfo discovered = DiscoverNatInfo(host.address); + if (discovered.stun_status == NET_CTL_STUN_STATUS_OK) { + std::lock_guard guard(config.lock); + if (config.configGeneration == generation) { + config.cachedConfigGeneration = generation; + config.cachedLocalAddr = host.address.s_addr; + config.cachedUntil = std::chrono::steady_clock::now() + std::chrono::milliseconds(STUN_CACHE_TTL_MS); + config.cachedResult = discovered; + config.hasCachedResult = true; + } + } + *info = discovered; + return 0; } int APS5_VABI sceNetCtlGetResult(int event, int* result) { diff --git a/core/libs/prx/libSceNetCtl/tests/V6Callbacks.cpp b/core/libs/prx/libSceNetCtl/tests/V6Callbacks.cpp new file mode 100644 index 0000000000..536e70a3a7 --- /dev/null +++ b/core/libs/prx/libSceNetCtl/tests/V6Callbacks.cpp @@ -0,0 +1,127 @@ +#include "prx/libSceNetCtl/libSceNetCtl.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "NetCtl IPv6 callback check failed at line %u\n", location.line()); + std::abort(); +} + +struct Probe { + int calls = 0; + int event = 0; + std::thread::id thread; +}; + +static void APS5_VABI Record(int event, void* argument) { + auto& probe = *static_cast(argument); + ++probe.calls; + probe.event = event; + probe.thread = std::this_thread::get_id(); +} + +static int ExpectedEvent(bool ipv6) { + int state = -1; + Require((ipv6 ? sceNetCtlGetStateV6(&state) : sceNetCtlGetState(&state)) == 0); + return state == 3 ? 3 : 1; +} + +struct RegisterDuringCheck { + Probe current; + Probe later; + int laterId = -1; +}; + +static void APS5_VABI RegisterV6(int event, void* argument) { + auto& request = *static_cast(argument); + Record(event, &request.current); + Require(sceNetCtlRegisterCallbackV6(Record, &request.later, &request.laterId) == 0); +} + +struct Reentrant { + Probe probe; + int id = -1; + int unregisterResult = -1; + int checkResult = -1; +}; + +static void APS5_VABI UnregisterAndReenter(int event, void* argument) { + auto& request = *static_cast(argument); + Record(event, &request.probe); + request.unregisterResult = sceNetCtlUnregisterCallbackV6(request.id); + request.checkResult = sceNetCtlCheckCallback(); +} + +int main() { + Require(sceNetCtlCheckCallback() == 0); + const auto checkingThread = std::this_thread::get_id(); + const int ipv6Event = ExpectedEvent(true); + Probe v6; + int v6Id = -1; + Require(sceNetCtlRegisterCallbackV6(Record, &v6, &v6Id) == 0); + Require(v6.calls == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(v6.calls == 1 && v6.event == ipv6Event && v6.thread == checkingThread); + Require(sceNetCtlCheckCallback() == 0 && v6.calls == 1); + Require(sceNetCtlUnregisterCallbackV6(v6Id) == 0); + Require(sceNetCtlCheckCallback() == 0 && v6.calls == 1); + + RegisterDuringCheck registration; + int v4Id = -1; + Require(sceNetCtlRegisterCallback(RegisterV6, ®istration, &v4Id) == 0); + Require(sceNetCtlRegisterCallbackV6(Record, &v6, &v6Id) == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(registration.current.calls == 1 && registration.current.event == ExpectedEvent(false)); + Require(registration.later.calls == 0 && v6.calls == 2); + Require(sceNetCtlCheckCallback() == 0); + Require(registration.later.calls == 1 && registration.later.event == ipv6Event && registration.later.thread == checkingThread); + Require(v6.calls == 2 && registration.current.calls == 1); + Require(sceNetCtlUnregisterCallback(v4Id) == 0); + Require(sceNetCtlUnregisterCallbackV6(v6Id) == 0); + Require(sceNetCtlUnregisterCallbackV6(registration.laterId) == 0); + + Reentrant reentrant; + Probe other; + Require(sceNetCtlRegisterCallbackV6(UnregisterAndReenter, &reentrant, &reentrant.id) == 0); + Require(sceNetCtlRegisterCallbackV6(Record, &other, &v6Id) == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(reentrant.probe.calls == 1 && reentrant.probe.event == ipv6Event); + Require(reentrant.unregisterResult == 0 && reentrant.checkResult == 0); + Require(other.calls == 1 && other.event == ipv6Event); + Require(sceNetCtlCheckCallback() == 0 && other.calls == 1); + Require(sceNetCtlUnregisterCallbackV6(v6Id) == 0); + + std::array probes{}; + std::array ids{}; + std::array ipv4Probes{}; + std::array ipv4Ids{}; + for (std::size_t index = 0; index < probes.size(); ++index) { + Require(sceNetCtlRegisterCallbackV6(Record, &probes[index], &ids[index]) == 0); + Require(sceNetCtlRegisterCallback(Record, &ipv4Probes[index], &ipv4Ids[index]) == 0); + } + Require(sceNetCtlCheckCallback() == 0); + for (std::size_t index = 0; index < probes.size(); ++index) { + Require(probes[index].calls == 1 && probes[index].event == ipv6Event && probes[index].thread == checkingThread); + Require(ipv4Probes[index].calls == 1 && ipv4Probes[index].event == ExpectedEvent(false) && ipv4Probes[index].thread == checkingThread); + Require(sceNetCtlUnregisterCallbackV6(ids[index]) == 0); + Require(sceNetCtlUnregisterCallback(ipv4Ids[index]) == 0); + } + Probe toolkit, isolatedV6; + int toolkitId = -1; + Require(sceNetCtlRegisterCallbackForNpToolkit(Record, &toolkit, &toolkitId) == 0); + Require(sceNetCtlRegisterCallbackV6(Record, &isolatedV6, &v6Id) == 0); + Require(sceNetCtlCheckCallbackForNpToolkit() == 0); + Require(toolkit.calls == 1 && toolkit.event == ExpectedEvent(false) && isolatedV6.calls == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(toolkit.calls == 1 && isolatedV6.calls == 1 && isolatedV6.event == ipv6Event); + Require(sceNetCtlCheckCallbackForNpToolkit() == 0 && toolkit.calls == 1); + Require(sceNetCtlUnregisterCallbackForNpToolkit(toolkitId) == 0); + Require(sceNetCtlUnregisterCallbackV6(v6Id) == 0); + +} diff --git a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp index dda42ea297..725e1aaffb 100644 --- a/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Atrac9.cpp @@ -21,7 +21,9 @@ static constexpr std::uint16_t WAVE_FORMAT_IEEE_FLOAT = 0x3; static constexpr std::uint16_t WAVE_FORMAT_EXTENSIBLE = 0xfffe; static constexpr std::uint8_t ATRAC9_GUID[16] = {0xd2, 0x42, 0xe1, 0x47, 0xba, 0x36, 0x8d, 0x4d, 0x88, 0xfc, 0x61, 0x65, 0x4f, 0x8c, 0x83, 0x6c}; static constexpr std::uint8_t PCM_GUID[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; +static constexpr std::uint8_t TYPE_43_GUID[16] = {0x1c, 0xe0, 0x10, 0x9e, 0xcc, 0x8b, 0xd9, 0x47, 0x8f, 0x3b, 0xe3, 0x0b, 0x8b, 0xdc, 0x35, 0x29}; static constexpr std::size_t FMT_ATRAC9_SIZE = 52; +static constexpr std::size_t FMT_EXTENSIBLE_SIZE = 40; static constexpr std::size_t FMT_GUID_OFFSET = 24; static constexpr std::size_t FMT_CONFIG_OFFSET = 44; static constexpr std::size_t FACT_ATRAC9_SIZE = 12; @@ -404,17 +406,18 @@ static int ParsePcm(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo static int ParseWaveform(const RiffChunks& chunks, std::size_t size, Ngs2WaveformInfo& info) { if (chunks.formatSize < 2) return SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT; const std::uint16_t tag = ReadLe16(chunks.format); - if (tag == WAVE_FORMAT_EXTENSIBLE && chunks.formatSize >= FMT_GUID_OFFSET + sizeof(ATRAC9_GUID) && - std::memcmp(chunks.format + FMT_GUID_OFFSET, ATRAC9_GUID, sizeof(ATRAC9_GUID)) == 0) { - return ParseAtrac9(chunks, info); - } - if (tag == WAVE_FORMAT_EXTENSIBLE && chunks.formatSize >= FMT_GUID_OFFSET + sizeof(PCM_GUID) && - std::memcmp(chunks.format + FMT_GUID_OFFSET, PCM_GUID, sizeof(PCM_GUID)) == 0) { - return ParsePcm(chunks, size, info, false); + if (tag == WAVE_FORMAT_EXTENSIBLE) { + if (chunks.formatSize < FMT_EXTENSIBLE_SIZE) return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; + const auto* guid = chunks.format + FMT_GUID_OFFSET; + if (std::memcmp(guid, PCM_GUID, sizeof(PCM_GUID)) == 0) return ParsePcm(chunks, size, info, false); + if (std::memcmp(guid, ATRAC9_GUID, sizeof(ATRAC9_GUID)) == 0) return ParseAtrac9(chunks, info); + if (chunks.formatSize == FMT_ATRAC9_SIZE) throw std::runtime_error("NGS2: parsing an extensible waveform of format size 0x34 without the ATRAC9 GUID is not implemented"); + if (std::memcmp(guid, TYPE_43_GUID, sizeof(TYPE_43_GUID)) == 0) throw std::runtime_error("NGS2: parsing waveform type 0x43 is not implemented"); + return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; } if (tag == WAVE_FORMAT_PCM) return ParsePcm(chunks, size, info, false); if (tag == WAVE_FORMAT_IEEE_FLOAT) return ParsePcm(chunks, size, info, true); - throw std::runtime_error("NGS2: parsing waveform format tag " + Ngs2Hex(tag) + " is not implemented"); + return SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; } #pragma GCC visibility push(default) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp index b72cc16451..ddc412134c 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Parse.cpp @@ -50,6 +50,24 @@ static std::vector PcmFile(std::uint32_t channels, std::uint32_t s return file; } +static std::vector ExtensibleFile(std::uint32_t formatSize, const std::uint8_t* guid) { + const auto pcm = PcmFile(1, 44100, 16, 10, 0xfffe); + std::vector format(pcm.begin() + 20, pcm.begin() + 60); + std::memcpy(format.data() + 24, guid, 16); + format.resize(formatSize); + std::vector file; + PutTag(file, "RIFF"); + Put32(file, 0); + PutTag(file, "WAVE"); + PutTag(file, "fmt "); + Put32(file, formatSize); + file.insert(file.end(), format.begin(), format.end()); + file.insert(file.end(), pcm.begin() + 60, pcm.end()); + const auto riffSize = static_cast(file.size() - 8); + std::memcpy(file.data() + 4, &riffSize, sizeof(riffSize)); + return file; +} + static void TestParsePcm() { const auto file = PcmFile(2, 44100, 16, 100); Ngs2WaveformInfo info{}; @@ -90,6 +108,36 @@ static void TestParsePcm() { Require(sceNgs2ParseWaveformData(noChannels.data(), noChannels.size(), &info) == SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT); } +static void TestParseUnknownFormats() { + const std::uint8_t pcmGuid[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}; + const std::uint8_t unknownGuid[16] = {0xaa, 0xbb, 0xcc, 0xdd, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc}; + Ngs2WaveformInfo info{}; + for (const std::uint32_t tag : {0x0u, 0x2u, 0x11u, 0x55u, 0x165u, 0xffffu}) { + const auto wave = PcmFile(1, 44100, 16, 10, tag); + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + } + for (const std::uint32_t formatSize : {0x10u, 0x18u, 0x26u}) { + const auto wave = ExtensibleFile(formatSize, pcmGuid); + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + } + for (const std::uint32_t formatSize : {0x28u, 0x30u, 0x36u}) { + const auto wave = ExtensibleFile(formatSize, unknownGuid); + Require(sceNgs2ParseWaveformData(wave.data(), wave.size(), &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + } + const auto pcm = ExtensibleFile(0x34, pcmGuid); + Require(sceNgs2ParseWaveformData(pcm.data(), pcm.size(), &info) == SCE_NGS2_OK && info.format.waveform_type == SCE_NGS2_WAVEFORM_TYPE_PCM_I16L && info.num_samples == 10); + + const auto wave = PcmFile(1, 44100, 16, 10, 0x55); + const char* const guestPath = "/aps5_ngs2_parse_unknown.wav"; + const auto path = ResolvePath_nid_no_patch(guestPath); + { + std::ofstream out(path, std::ios::binary); + out.write(reinterpret_cast(wave.data()), static_cast(wave.size())); + } + Require(sceNgs2ParseWaveformFile(guestPath, 0, &info) == SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT); + std::filesystem::remove(path); +} + static void TestParseFile() { const auto wave = PcmFile(1, 22050, 16, 64); const char* const guestPath = "/aps5_ngs2_parse_file.wav"; @@ -187,6 +235,7 @@ static void TestFrameInfoErrors() { int main() { TestParsePcm(); + TestParseUnknownFormats(); TestParseFile(); TestParseFileBank(); TestFrameInfoPcm(); diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp index 8ac2f96323..c8e9a9ca52 100644 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ b/core/libs/prx/libSceNpCppWebApi/Export.cpp @@ -8,6 +8,7 @@ #include "prx/libc/include/General.hpp" static constexpr int SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN = static_cast(0x80552907); +static constexpr int TransactionNotStarted = static_cast(0x80553508); // Offline PSN: request entry points (API calls, transaction start/readData, factories) fail with // SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN, the constant shadPS4/fpPS4 document for this code; parameter @@ -419,7 +420,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE5startEPNS2_10LibContextE() { @@ -427,11 +428,11 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { @@ -439,7 +440,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextEm() { @@ -1272,7 +1273,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead14sessionIdIsSetEv(const void* self) { @@ -1438,7 +1439,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { @@ -1446,7 +1447,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponse } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } bool APS5_VABI _ZNK3sce2Np9CppWebApi14SessionManager2V136UsersPlayerSessionsInvitationForRead22receivedTimestampIsSetEv(const void* self) { @@ -1465,7 +1466,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14Sessi } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V154GetUsersAccountIdPlayerSessionsInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE11getResponseERS4_() { @@ -1668,8 +1669,7 @@ void* APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_17Ti } void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { - (void)self; - NotImplemented_nid_no_patch(__func__); + std::memset(self, 0, 0x10); } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERSB_(const void* self, void* response) { @@ -1681,12 +1681,10 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6 void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { (void)self; - NotImplemented_nid_no_patch(__func__); } void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { - (void)self; - NotImplemented_nid_no_patch(__func__); + std::memset(self, 0, 0x10); } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(const void* self, void* response) { @@ -1698,13 +1696,11 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_1 void APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { (void)self; - NotImplemented_nid_no_patch(__func__); } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void* self) { (void)self; - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void* self, void* context) { @@ -1716,8 +1712,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv(void* self) { (void)self; - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE(void* self, void* context) { @@ -2418,7 +2413,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorImED2Ev(void* self) { } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEEEED1Ev(IntrusivePtr* self) { @@ -2430,8 +2425,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEEC1Ev(IntrusivePtr* self) { @@ -2439,7 +2433,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V112JoinableUserEEEE8pushBackERKS8_(IntrusivePtr* self, const void* element) { @@ -2451,7 +2445,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEaSERS7_(IntrusivePtr* self, const IntrusivePtr* source) { @@ -2488,7 +2482,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V118GetFriendsResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { @@ -2500,7 +2494,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V124GetBlockingUsersResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { @@ -2512,7 +2506,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_11UserProfile2V125GetBasicPresencesResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { @@ -2524,7 +2518,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_15ProfanityFilter2V223FilterProfanityResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE28setResponseInformationOptionEP37SceNpWebApi2ResponseInformationOption() { @@ -2536,7 +2530,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_30CommunicationRestrictionStatus2V338CommunicationRestrictionStatusResponseEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common18ResponseHeaderBase14getHeaderValueEPKcRNS2_12IntrusivePtrINS2_6StringEEE() { @@ -2545,8 +2539,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common18ResponseHeaderBase14getHeaderValueEPK } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEE3endEv() { @@ -2781,8 +2774,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorImE4sizeEv() { @@ -2790,8 +2782,7 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorImE4sizeEv() { return 0; } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { - NotImplemented_nid_no_patch(__func__); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { return 0; } @@ -2837,8 +2828,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14 } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEneERKS9_() { @@ -2896,8 +2886,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14Sessi self->libContext = libContext; } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { - NotImplemented_nid_no_patch(__func__); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { return 0; } @@ -2910,8 +2899,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V1 ReleaseIntrusivePtr(self, 0x00, __func__); } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { - NotImplemented_nid_no_patch(__func__); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { return 0; } @@ -2948,8 +2936,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEED1Ev(IntrusivePtr* self) { @@ -2964,8 +2951,7 @@ void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2 return self->object; } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { - NotImplemented_nid_no_patch(__func__); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv() { return 0; } @@ -3236,8 +3222,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringeqEPKc(const CommonString* self, return other != nullptr && std::strcmp(self->buffer != nullptr ? self->buffer : "", other) == 0; } -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv() { - NotImplemented_nid_no_patch(__func__); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv() { return 0; } @@ -3247,8 +3232,7 @@ void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEaSERS6_ } int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5abortEv() { - NotImplemented_nid_no_patch(__func__); - return 0; + return TransactionNotStarted; } int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv() { diff --git a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp index 77fa374881..b827506807 100644 --- a/core/libs/prx/libSceNpCppWebApi/include/Api.hpp +++ b/core/libs/prx/libSceNpCppWebApi/include/Api.hpp @@ -580,7 +580,7 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_1 void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V12ToEED1Ev(IntrusivePtr* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorImE4sizeEv(); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_11UserProfile2V113BasicPresenceEEEEdeEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11hasResponseEv(); void* APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEEixEm(IntrusivePtr* self, unsigned long index); @@ -603,10 +603,10 @@ void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2 int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_(); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V113PlayerSessionEEC1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V130RequestPlayerSessionInvitationEEEEC1EPNS2_10LibContextE(IntrusivePtr* self, void* libContext); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEdeEv(); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V118LeaderWithOnlineIdEED1Ev(IntrusivePtr* self); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorImEneERKS4_(); std::uint64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEE4sizeEv(const IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V120PlayerSessionForReadEEEEEED1Ev(IntrusivePtr* self); @@ -617,7 +617,7 @@ int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V149PostPlayerSessionsSessionIdInvitationsRequestBodyEED1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V130RequestPlayerSessionInvitationEEEED1Ev(IntrusivePtr* self); void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V120PlayerSessionForReadEEptEv(const IntrusivePtr* self); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv(); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_14SessionManager2V127ResponsePlayerSessionPlayerEEEE8copyFromERKS9_(); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEED1Ev(IntrusivePtr* self); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_14SessionManager2V113PlayerSessionEEEEEEC1Ev(IntrusivePtr* self); @@ -673,7 +673,7 @@ int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_15DefaultResponseENS void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS2_6StringEEEEptEv(const IntrusivePtr* self); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE3endEv(); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringeqEPKc(const CommonString* self, const char* other); -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv(); +std::int64_t APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv(); void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorImEEEaSERS6_(IntrusivePtr* self, const IntrusivePtr* source); int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5abortEv(); int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_6StringEE5beginEv(); diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index e0593d4afb..44530aadcd 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -126,11 +126,13 @@ int APS5_VABI sceNpEntitlementAccessInitialize(const NpEntitlementAccessInitPara } int APS5_VABI sceNpEntitlementAccessAbortRequest(std::int64_t requestId) { - throw std::logic_error("sceNpEntitlementAccessAbortRequest: asynchronous request tracking is unimplemented"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceNpEntitlementAccessDeleteRequest(std::int64_t requestId) { - throw std::logic_error("sceNpEntitlementAccessDeleteRequest: asynchronous request tracking is unimplemented"); + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(NpEntitlementAccessTransactionId* transactionId) { diff --git a/core/libs/prx/libSceNpWebApi/Export.cpp b/core/libs/prx/libSceNpWebApi/Export.cpp index 269bbec537..49a0b7357b 100644 --- a/core/libs/prx/libSceNpWebApi/Export.cpp +++ b/core/libs/prx/libSceNpWebApi/Export.cpp @@ -223,7 +223,7 @@ int APS5_VABI sceNpWebApiGetHttpResponseHeaderValueLength() { return 0; } -int APS5_VABI sceNpWebApiGetHttpStatusCode() { +int APS5_VABI sceNpWebApiGetHttpStatusCode(std::int64_t requestId, int* statusCode) { NotImplemented_nid_no_patch(__func__); return 0; } @@ -239,7 +239,7 @@ int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize) { return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX; } -int APS5_VABI sceNpWebApiReadData() { +int APS5_VABI sceNpWebApiReadData(std::int64_t requestId, void* data, std::size_t size) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceNpWebApi/include/Api.hpp b/core/libs/prx/libSceNpWebApi/include/Api.hpp index ec9ce3c836..47b0d19b1b 100644 --- a/core/libs/prx/libSceNpWebApi/include/Api.hpp +++ b/core/libs/prx/libSceNpWebApi/include/Api.hpp @@ -25,9 +25,9 @@ int APS5_VABI sceNpWebApiDeleteRequest(std::int64_t requestId); int APS5_VABI sceNpWebApiDeleteServicePushEventFilter(int libCtxId, int filterId); int APS5_VABI sceNpWebApiGetHttpResponseHeaderValue(); int APS5_VABI sceNpWebApiGetHttpResponseHeaderValueLength(); -int APS5_VABI sceNpWebApiGetHttpStatusCode(); +int APS5_VABI sceNpWebApiGetHttpStatusCode(std::int64_t requestId, int* statusCode); int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize); -int APS5_VABI sceNpWebApiReadData(); +int APS5_VABI sceNpWebApiReadData(std::int64_t requestId, void* data, std::size_t size); int APS5_VABI sceNpWebApiRegisterPushEventCallback(int titleUserCtxId, int filterId, void* callback, void* userArg); int APS5_VABI sceNpWebApiRegisterServicePushEventCallback(int titleUserCtxId, int filterId, void* callback, void* userArg); int APS5_VABI sceNpWebApiSendRequest(std::int64_t requestId, const void* data, std::size_t dataSize); diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 9731e4cb0b..fda278f8c6 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -235,7 +235,10 @@ int APS5_VABI sceNpWebApi2AddMultipartPart() { } int APS5_VABI sceNpWebApi2CreateMultipartRequest(int user_ctx_id, const char* api_group, const char* path, const char* method, int64_t* request_id) { - NotImplemented_nid_no_patch(__func__); + (void)user_ctx_id; + if (!api_group || !path || !method) return SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + const int64_t created = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + if (request_id) *request_id = created; return 0; } @@ -270,8 +273,10 @@ int APS5_VABI sceNpWebApi2IntPushEventCreateDestPs4CtxIndFilter() { } int APS5_VABI sceNpWebApi2SendMultipartRequest(int64_t request_id, int part_index, void* data, uint64_t data_size, void* resp_info_option) { - NotImplemented_nid_no_patch(__func__); - return 0; + (void)request_id; + (void)resp_info_option; + if (part_index <= 0 || !data || data_size == 0) return SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + return SCE_NP_WEBAPI2_ERROR_UNAVAILABLE; } int APS5_VABI sceNpWebApi2SetMultipartContentType() { diff --git a/core/libs/prx/libSceOpusCeltEnc/CMakeLists.txt b/core/libs/prx/libSceOpusCeltEnc/CMakeLists.txt new file mode 100644 index 0000000000..c988c3c633 --- /dev/null +++ b/core/libs/prx/libSceOpusCeltEnc/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceOpusCeltEnc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +target_include_directories(libSceOpusCeltEnc PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceOpusCeltEnc PRIVATE opus) +configure_windows_unwind(libSceOpusCeltEnc) + +if(BUILD_TESTING) + add_test_executable(guest_opus_celt_enc_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusCeltEnc.cpp) + target_include_directories(guest_opus_celt_enc_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_opus_celt_enc_tests PRIVATE libSceOpusCeltEnc libc opus) + configure_windows_unwind(guest_opus_celt_enc_tests) + add_test(NAME guest_opus_celt_enc COMMAND guest_opus_celt_enc_tests) + if(WIN32) + set_tests_properties(guest_opus_celt_enc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceOpusCeltEnc/Export.cpp b/core/libs/prx/libSceOpusCeltEnc/Export.cpp index 5f837e73e4..47556b14d1 100644 --- a/core/libs/prx/libSceOpusCeltEnc/Export.cpp +++ b/core/libs/prx/libSceOpusCeltEnc/Export.cpp @@ -1,64 +1,112 @@ #include "prx/libSceOpusCeltEnc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceOpusCeltEnc/include/OpusEncoder.hpp" #include -#include -#include "SceTypes.hpp" +#include #include "prx/libc/include/General.hpp" +namespace { + +std::uint32_t bitrate(int rate, int channels) { + if (rate == 8000) return channels == 1 ? 11000u : 19000u; + if (rate == 12000) return channels == 1 ? 15000u : 27000u; + if (rate == 16000) return channels == 1 ? 19000u : 35000u; + if (rate == 24000) return channels == 1 ? 27000u : 51000u; + return channels == 1 ? 51000u : 99000u; +} + +void writeWord(std::uint8_t* state, std::size_t offset, std::uint32_t value) { + std::memcpy(state + offset, &value, sizeof value); +} + +void writeState(void* state, std::uint32_t instanceId, int rate, int channels) { + auto* bytes = static_cast(state); + const auto rateWord = static_cast(rate); + const auto frameWord = static_cast(rate / 50); + const auto channelWord = static_cast(channels); + const auto bandwidth = bitrate(rate, channels); + writeWord(bytes, 0x00u, instanceId); + writeWord(bytes, 0x08u, 8u * channelWord); + writeWord(bytes, 0x0Cu, 2u); + writeWord(bytes, 0x10u, channelWord); + writeWord(bytes, 0x14u, rateWord); + writeWord(bytes, 0x1Cu, bandwidth); + writeWord(bytes, 0x20u, 2u); + writeWord(bytes, 0x24u, 0x451u); + writeWord(bytes, 0x28u, 0x5E0u); + writeWord(bytes, 0x2Cu, 10u); + writeWord(bytes, 0x40u, frameWord); + writeWord(bytes, 0xA8u, 0x803u); + writeWord(bytes, 0xACu, 0x451u); + writeWord(bytes, 0xB0u, 0x451u); + writeWord(bytes, 0xB4u, 1u); + writeWord(bytes, 0xB8u, 1u); + writeWord(bytes, 0xBCu, 0xFFFFFC18u); + writeWord(bytes, 0xC0u, bandwidth); + writeWord(bytes, 0xC4u, 0x10u); + writeWord(bytes, 0xC8u, frameWord); + writeWord(bytes, 0xCCu, 0x17B2u); +} + +constexpr EncoderCodes codes{0x813F0000u, 1u, 2u, 3u, 4u, 5u, 0x10u, 0x0Du, 0x11u, 0x16u, 0x19u, 728, 0xCCu, 0x00u, 0x14u, 0x10u, 0x17B2u, {1u, 2u, 4u, 8u, 16u, 24u}, 6, writeState}; + +OpusEncoderLibrary library("OpusCeltEnc", OPUS_APPLICATION_RESTRICTED_LOWDELAY, OPUS_APPLICATION_RESTRICTED_CELT, codes); + +} // namespace + extern "C" { -int APS5_VABI sceOpusCeltEncCreate(OpusCeltEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { +int APS5_VABI sceOpusCeltEncInitialize(std::uint32_t* context) { return library.Initialize(context); } + +int APS5_VABI sceOpusCeltEncTerminate(const std::uint32_t* context) { return library.Terminate(context); } + +int APS5_VABI sceOpusCeltEncGetSize(int channels) { return library.GetSize(channels); } + +int APS5_VABI sceOpusCeltEncCreate(OpusCeltEncoder* state, int sampleRate, int channels, int application) { + return library.Create(state, sampleRate, channels, application); +} + +int APS5_VABI sceOpusCeltEncCreateEx(const std::uint32_t* context, OpusCeltEncoder* state, int sampleRate, int channels, int application) { + return library.CreateEx(context, state, sampleRate, channels, application); +} + +int APS5_VABI sceOpusCeltEncEncode(OpusCeltEncoder* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + return library.Encode(state, samples, frameSize, data, maxDataBytes); +} + +int APS5_VABI sceOpusCeltEncEncodeFloat(OpusCeltEncoder* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + return library.EncodeFloat(state, samples, frameSize, data, maxDataBytes); +} + +int APS5_VABI sceOpusCeltEncDestroy(OpusCeltEncoder* state) { return library.Destroy(state); } + +int APS5_VABI sceOpusCeltEncCtl(OpusCeltEncoder* state, int request, ...) { + (void)state; + (void)request; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncCreateEx(const std::uint32_t* contextId, OpusCeltEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { +int APS5_VABI sceOpusCeltEncEncodeWithPriority(OpusCeltEncoder* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncCtl(OpusCeltEncoder* encoder, std::int32_t request, ...) { +int APS5_VABI sceOpusCeltEncEncodeFloatWithPriority(OpusCeltEncoder* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusCeltEncDestroy(OpusCeltEncoder* encoder) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncEncode(OpusCeltEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncEncodeFloat(OpusCeltEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncEncodeFloatWithPriority(OpusCeltEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncEncodeWithPriority(OpusCeltEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncGetSize(std::int32_t channels) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncInitialize(std::uint32_t* contextId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusCeltEncTerminate(const std::uint32_t* contextId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} +} // extern "C" diff --git a/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp new file mode 100644 index 0000000000..be8801cd6d --- /dev/null +++ b/core/libs/prx/libSceOpusCeltEnc/include/OpusEncoder.hpp @@ -0,0 +1,223 @@ +#ifndef CORE_LIBS_PRX_LIBSCEOPUSCELTENC_INCLUDE_OPUSENCODER_HPP +#define CORE_LIBS_PRX_LIBSCEOPUSCELTENC_INCLUDE_OPUSENCODER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +struct EncoderCodes { + std::uint32_t baseError; + std::uint32_t nullErrorOffset; + std::uint32_t nullSamplesErrorOffset; + std::uint32_t frameErrorOffset; + std::uint32_t nullDataErrorOffset; + std::uint32_t sizeErrorOffset; + std::uint32_t rateErrorOffset; + std::uint32_t channelsErrorOffset; + std::uint32_t applicationErrorOffset; + std::uint32_t destroyStateErrorOffset; + std::uint32_t initializeNullErrorOffset; + std::size_t stateSize; + std::size_t stateMagicOffset; + std::size_t stateInstanceOffset; + std::size_t stateRateOffset; + std::size_t stateChannelsOffset; + std::uint16_t stateMagic; + std::uint8_t frameSteps[6]; + std::size_t frameStepCount; + void (*setupState)(void* state, std::uint32_t instanceId, int rate, int channels); +}; + +class OpusEncoderLibrary { +public: + OpusEncoderLibrary(const char* libraryName, int callerApplication, int codecApplication, const EncoderCodes& libraryCodes) : name(libraryName), requiredApplication(callerApplication), application(codecApplication), codes(libraryCodes), errorBase(libraryCodes.baseError) {} + + int Initialize(std::uint32_t* context) { + if (!context) return error(codes.initializeNullErrorOffset); + std::lock_guard lock(mutex); + for (const auto& [id, owner] : contexts) + if (owner == context) fail("context already initialized"); + if (nextContext == 0) fail("context identifiers exhausted"); + const auto id = nextContext++; + contexts.emplace(id, context); + *context = id; + return 0; + } + + int Terminate(const std::uint32_t* context) { + if (!context) fail("null context"); + std::lock_guard lock(mutex); + if (!contexts.contains(*context)) fail("unknown context"); + for (const auto& [id, encoder] : encoders) + if (encoder->context == *context) fail("termination with live encoders is not implemented"); + contexts.erase(*context); + return 0; + } + + int GetSize(int channels) { + if (channels != 1 && channels != 2) return error(codes.channelsErrorOffset); + return static_cast(codes.stateSize); + } + + int Create(void* state, int sampleRate, int channels, int createApplication) { + if (!state) return error(codes.nullErrorOffset); + if (!acceptedRate(sampleRate)) return error(codes.rateErrorOffset); + if (channels != 1 && channels != 2) return error(codes.channelsErrorOffset); + if (createApplication != requiredApplication) return error(codes.applicationErrorOffset); + std::lock_guard lock(mutex); + Make(state, 0, sampleRate, channels); + return 0; + } + + int CreateEx(const std::uint32_t* context, void* state, int sampleRate, int channels, int createApplication) { + if (!context || !state) return error(codes.nullErrorOffset); + if (!acceptedRate(sampleRate)) return error(codes.rateErrorOffset); + if (channels != 1 && channels != 2) return error(codes.channelsErrorOffset); + if (createApplication != requiredApplication) return error(codes.applicationErrorOffset); + std::lock_guard lock(mutex); + if (!contexts.contains(*context)) fail("unknown context"); + Make(state, *context, sampleRate, channels); + return 0; + } + + int Encode(void* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + if (!state) return error(codes.nullErrorOffset); + if (!samples) return error(codes.nullSamplesErrorOffset); + if (frameSize == 0) return error(codes.frameErrorOffset); + std::lock_guard lock(mutex); + if (!hasMarker(state)) return error(codes.destroyStateErrorOffset); + if (!data) return error(codes.nullDataErrorOffset); + if (maxDataBytes == 0) return error(codes.sizeErrorOffset); + if (!acceptedFrame(stateRate(state), frameSize)) return error(codes.frameErrorOffset); + const auto found = encoders.find(stateWord(state, codes.stateInstanceOffset)); + if (found == encoders.end()) fail("state does not reference a live encoder"); + const int bytes = opus_encode(found->second->codec.get(), samples, frameSize, data, maxDataBytes); + if (bytes < 0) fail("libopus rejected the frame"); + return bytes; + } + + int EncodeFloat(void* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + if (!state) return error(codes.nullErrorOffset); + if (!samples) return error(codes.nullSamplesErrorOffset); + if (frameSize == 0) return error(codes.frameErrorOffset); + std::lock_guard lock(mutex); + if (!hasMarker(state)) return error(codes.destroyStateErrorOffset); + if (frameSize < 0) fail("the converted frame reservation does not fit the stack"); + if (static_cast(frameSize) * stateWord(state, codes.stateChannelsOffset) * 4 > 0x800000) fail("the converted frame reservation does not fit the stack"); + if (!data) return error(codes.nullDataErrorOffset); + if (maxDataBytes == 0) return error(codes.sizeErrorOffset); + if (!acceptedFrame(stateRate(state), frameSize)) return error(codes.frameErrorOffset); + const auto found = encoders.find(stateWord(state, codes.stateInstanceOffset)); + if (found == encoders.end()) fail("state does not reference a live encoder"); + const int bytes = opus_encode_float(found->second->codec.get(), samples, frameSize, data, maxDataBytes); + if (bytes < 0) fail("libopus rejected the frame"); + return bytes; + } + + int Destroy(void* state) { + if (!state) return error(codes.nullErrorOffset); + std::uint16_t magic = 0; + std::memcpy(&magic, static_cast(state) + codes.stateMagicOffset, sizeof magic); + if (magic != codes.stateMagic) return error(codes.destroyStateErrorOffset); + std::lock_guard lock(mutex); + encoders.erase(stateWord(state, codes.stateInstanceOffset)); + setWord(state, codes.stateMagicOffset, 0); + return 0; + } + +private: + struct Encoder { + std::uint32_t context; + int channels; + int sampleRate; + std::unique_ptr codec{nullptr, opus_encoder_destroy}; + }; + + void Make(void* state, std::uint32_t context, int sampleRate, int channels) { + if (hasMarker(state)) encoders.erase(stateWord(state, codes.stateInstanceOffset)); + const auto instanceId = nextInstanceId++; + std::memset(state, 0, codes.stateSize); + codes.setupState(state, instanceId, sampleRate, channels); + auto encoder = std::make_unique(); + encoder->context = context; + encoder->channels = channels; + encoder->sampleRate = sampleRate; + int opusError = OPUS_OK; + encoder->codec.reset(opus_encoder_create(sampleRate, channels, application, &opusError)); + if (opusError == OPUS_ALLOC_FAIL) { + std::memset(state, 0, codes.stateSize); + throw std::bad_alloc(); + } + if (!encoder->codec || opusError != OPUS_OK) { + std::memset(state, 0, codes.stateSize); + fail("cannot create libopus encoder"); + } + encoders.emplace(instanceId, std::move(encoder)); + } + + static bool acceptedRate(int rate) { + return rate == 8000 || rate == 12000 || rate == 16000 || rate == 24000 || rate == 48000; + } + + bool acceptedFrame(int rate, int frameSize) const { + if (frameSize < 0 || rate <= 0) return false; + for (std::size_t index = 0; index < codes.frameStepCount; ++index) + if (frameSize == rate * codes.frameSteps[index] / 400) return true; + return false; + } + + std::uint32_t stateWord(const void* state, std::size_t offset) const { + std::uint32_t word = 0; + std::memcpy(&word, static_cast(state) + offset, sizeof word); + return word; + } + + void setWord(void* state, std::size_t offset, std::uint32_t value) const { + std::memcpy(static_cast(state) + offset, &value, sizeof value); + } + + int stateRate(void* state) const { + return static_cast(stateWord(state, codes.stateRateOffset)); + } + + bool hasMarker(void* state) const { + std::uint16_t magic = 0; + std::memcpy(&magic, static_cast(state) + codes.stateMagicOffset, sizeof magic); + return magic == codes.stateMagic; + } + + const char* name; + int requiredApplication; + int application; + EncoderCodes codes; + std::uint32_t errorBase; + std::mutex mutex; + std::unordered_map contexts; + std::unordered_map> encoders; + std::uint32_t nextContext = 1; + std::uint32_t nextInstanceId = 1; + + int error(std::uint32_t offset) const { + return static_cast(errorBase + offset); + } + + [[noreturn]] void fail(const char* message) const { + throw std::runtime_error(std::string(name) + ": " + message); + } +}; + +} // namespace + +#endif diff --git a/core/libs/prx/libSceOpusSilkEnc/CMakeLists.txt b/core/libs/prx/libSceOpusSilkEnc/CMakeLists.txt new file mode 100644 index 0000000000..de311aade8 --- /dev/null +++ b/core/libs/prx/libSceOpusSilkEnc/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceOpusSilkEnc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +target_include_directories(libSceOpusSilkEnc PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceOpusSilkEnc PRIVATE opus) +configure_windows_unwind(libSceOpusSilkEnc) + +if(BUILD_TESTING) + add_test_executable(guest_opus_silk_enc_tests ${LIBS_INCLUDE_DIR}/tests/GuestOpusSilkEnc.cpp) + target_include_directories(guest_opus_silk_enc_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_opus_silk_enc_tests PRIVATE libSceOpusSilkEnc libc opus) + configure_windows_unwind(guest_opus_silk_enc_tests) + add_test(NAME guest_opus_silk_enc COMMAND guest_opus_silk_enc_tests) + if(WIN32) + set_tests_properties(guest_opus_silk_enc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceOpusSilkEnc/Export.cpp b/core/libs/prx/libSceOpusSilkEnc/Export.cpp index 71bb17fe68..826f776fd1 100644 --- a/core/libs/prx/libSceOpusSilkEnc/Export.cpp +++ b/core/libs/prx/libSceOpusSilkEnc/Export.cpp @@ -1,79 +1,141 @@ #include "prx/libSceOpusSilkEnc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceOpusCeltEnc/include/OpusEncoder.hpp" #include -#include -#include "SceTypes.hpp" +#include #include "prx/libc/include/General.hpp" +namespace { + +std::uint32_t bitrate(int rate, int channels) { + if (rate == 8000) return channels == 1 ? 12000u : 19000u; + if (rate == 12000) return channels == 1 ? 15000u : 27000u; + if (rate == 16000) return channels == 1 ? 19000u : 35000u; + if (rate == 24000) return channels == 1 ? 27000u : 51000u; + return channels == 1 ? 51000u : 99000u; +} + +void writeWord(std::uint8_t* state, std::size_t offset, std::uint32_t value) { + std::memcpy(state + offset, &value, sizeof value); +} + +void writeState(void* state, std::uint32_t instanceId, int rate, int channels) { + auto* bytes = static_cast(state); + const auto rateWord = static_cast(rate); + const auto frameWord = static_cast(rate / 50); + const auto channelWord = static_cast(channels); + const auto bandwidth = bitrate(rate, channels); + writeWord(bytes, 0x00u, instanceId); + writeWord(bytes, 0x08u, 8u * channelWord); + writeWord(bytes, 0x10u, 0x800u); + writeWord(bytes, 0x14u, rateWord); + writeWord(bytes, 0x18u, channelWord); + writeWord(bytes, 0x1Cu, bandwidth); + writeWord(bytes, 0x28u, 0x44Fu); + writeWord(bytes, 0x2Cu, 0x44Fu); + writeWord(bytes, 0x30u, 0x5E0u); + writeWord(bytes, 0x34u, 10u); + writeWord(bytes, 0x48u, 0x3E8u); + writeWord(bytes, 0x58u, frameWord); + writeWord(bytes, 0xE4u, frameWord); + writeWord(bytes, 0xD8u, 0xFFFFFC18u); + writeWord(bytes, 0xDCu, bandwidth); + writeWord(bytes, 0xE0u, 0x10u); + writeWord(bytes, 0xE8u, 0x17B2u); +} + +constexpr EncoderCodes codes{0x81770000u, 1u, 2u, 3u, 4u, 5u, 0x0Fu, 0x0Cu, 0x10u, 0x15u, 0x18u, 752, 0xE8u, 0x00u, 0x14u, 0x18u, 0x17B2u, {4u, 8u, 16u, 24u, 0u, 0u}, 4, writeState}; + +OpusEncoderLibrary library("OpusSilkEnc", OPUS_APPLICATION_VOIP, OPUS_APPLICATION_RESTRICTED_SILK, codes); + +} // namespace + extern "C" { -int APS5_VABI sceOpusSilkEncCancel(OpusSilkEncoder* encoder, std::uint32_t batchId) { +int APS5_VABI sceOpusSilkEncInitialize(std::uint32_t* context) { return library.Initialize(context); } + +int APS5_VABI sceOpusSilkEncTerminate(const std::uint32_t* context) { return library.Terminate(context); } + +int APS5_VABI sceOpusSilkEncGetSize(int channels) { return library.GetSize(channels); } + +int APS5_VABI sceOpusSilkEncCreate(OpusSilkEncoder* state, int sampleRate, int channels, int application) { + return library.Create(state, sampleRate, channels, application); +} + +int APS5_VABI sceOpusSilkEncCreateEx(const std::uint32_t* context, OpusSilkEncoder* state, int sampleRate, int channels, int application) { + return library.CreateEx(context, state, sampleRate, channels, application); +} + +int APS5_VABI sceOpusSilkEncEncode(OpusSilkEncoder* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + return library.Encode(state, samples, frameSize, data, maxDataBytes); +} + +int APS5_VABI sceOpusSilkEncEncodeFloat(OpusSilkEncoder* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes) { + return library.EncodeFloat(state, samples, frameSize, data, maxDataBytes); +} + +int APS5_VABI sceOpusSilkEncDestroy(OpusSilkEncoder* state) { return library.Destroy(state); } + +int APS5_VABI sceOpusSilkEncCtl(OpusSilkEncoder* state, int request, ...) { + (void)state; + (void)request; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncCreate(OpusSilkEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { +int APS5_VABI sceOpusSilkEncCancel(OpusSilkEncoder* state, std::uint32_t batchId) { + (void)state; + (void)batchId; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncCreateEx(const std::uint32_t* contextId, OpusSilkEncoder* encoder, std::int32_t sampleRate, std::int32_t channels, std::int32_t application) { +int APS5_VABI sceOpusSilkEncEncodeWithPriority(OpusSilkEncoder* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncCtl(OpusSilkEncoder* encoder, std::int32_t request, ...) { +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority(OpusSilkEncoder* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncDestroy(OpusSilkEncoder* encoder) { +int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout(OpusSilkEncoder* state, const std::int16_t* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; + (void)timeout; + (void)batchId; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncEncode(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { +int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout(OpusSilkEncoder* state, const float* samples, int frameSize, std::uint8_t* data, int maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { + (void)state; + (void)samples; + (void)frameSize; + (void)data; + (void)maxDataBytes; + (void)priority; + (void)timeout; + (void)batchId; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceOpusSilkEncEncodeFloat(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncEncodeFloatWithPriority(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncEncodeFloatWithPriorityAndTimeout(OpusSilkEncoder* encoder, const float* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncEncodeWithPriority(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncEncodeWithPriorityAndTimeout(OpusSilkEncoder* encoder, const std::int16_t* samples, std::int32_t frameSize, std::uint8_t* data, std::int32_t maxDataBytes, std::uint32_t priority, std::uint32_t timeout, std::uint32_t* batchId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncGetSize(std::int32_t channels) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncInitialize(std::uint32_t* contextId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceOpusSilkEncTerminate(const std::uint32_t* contextId) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} +} // extern "C" diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 0787a07dbf..edbe4a9af2 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -37,6 +37,7 @@ constexpr int PAD_ERROR_ALREADY_OPENED = static_cast(0x80920004); constexpr int PAD_ERROR_UNKNOWN_USER = static_cast(0x809b0081); constexpr int PAD_ERROR_INVALID_USER_ID = static_cast(0x809b0001); constexpr int PAD_ERROR_INVALID_PORT_INDEX = static_cast(0x809b0003); +constexpr int PAD_ERROR_NOT_PERMITTED = static_cast(0x80920101); constexpr int PAD_USER_ID_INVALID = -1; std::array g_opened{false}; @@ -83,6 +84,13 @@ bool RemoteHandle(int handle) { return g_remoteOpened && handle == PAD_REMOTE_HANDLE; } +int OpenPortType(int handle) { + if (SpecialHandle(handle)) return PAD_PORT_TYPE_SPECIAL; + if (RemoteHandle(handle)) return PAD_PORT_TYPE_REMOTE; + const int port = PortOf(handle); + return port >= 0 && g_opened[port] ? PAD_PORT_TYPE_STANDARD : -1; +} + int PortForUser(int userId, int type, int index) { if (index != 0) return -1; if (type == PAD_PORT_TYPE_STANDARD) { @@ -228,18 +236,8 @@ int APS5_VABI scePadGetTriggerEffectState(int handle, PadTriggerEffectStateInfor int APS5_VABI scePadGetExtControllerInformation(int handle, void* info) { if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (info == nullptr) return PAD_ERROR_INVALID_ARG; + if (!SpecialHandle(handle)) return PAD_ERROR_NOT_PERMITTED; std::memset(info, 0, 0x2c); - if (SpecialHandle(handle)) return PAD_OK; - PadControllerInformation* base = static_cast(info); - base->touchPadInfo.pixelDensity = 44.86f; - base->touchPadInfo.resolution.x = kTouchPadWidth; - base->touchPadInfo.resolution.y = kTouchPadHeight; - base->stickInfo.deadZoneLeft = 13; - base->stickInfo.deadZoneRight = 13; - base->connectionType = PAD_CONNECTION_TYPE_LOCAL; - base->connectedCount = 1; - base->connected = true; - base->deviceClass = PAD_DEVICE_CLASS_STANDARD; return PAD_OK; } @@ -614,8 +612,13 @@ int APS5_VABI scePadShareOutputDataWithFunctionType(std::int32_t source, std::in } int APS5_VABI scePadSetVibrationForce(std::int32_t handle, const PadVibrationParam* param) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; + if (param == nullptr) return PAD_ERROR_INVALID_ARG; + if (SpecialHandle(handle)) return PAD_OK; + const int port = PortOf(handle); + if (port < 0) NotImplemented_nid_no_patch("scePadSetVibrationForce: a handle with no pad port"); + Pad::SetVibration(port, param->large_motor, param->small_motor); + return PAD_OK; } int APS5_VABI scePadGetVersionInfo(std::int32_t handle, PadVersionInfo* info) { @@ -914,13 +917,16 @@ int APS5_VABI scePadGetInfoByPortType(std::int32_t type, PadPortInfo* info) { } int APS5_VABI scePadResetLightBarAll(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return scePadResetLightBarAllByPortType(PAD_PORT_TYPE_STANDARD); } int APS5_VABI scePadResetLightBarAllByPortType(std::uint32_t portType) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (portType != PAD_PORT_TYPE_STANDARD && portType != 1 && portType != PAD_PORT_TYPE_SPECIAL) return PAD_ERROR_INVALID_ARG; + if (!Pad::IsInitialized()) return PAD_OK; + const int type = static_cast(portType); + for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) if (OpenPortType(PAD_HANDLE_BASE + port) == type) scePadResetLightBar(PAD_HANDLE_BASE + port); + for (int slot = 0; slot < PAD_SPECIAL_HANDLE_COUNT; ++slot) if (OpenPortType(PAD_SPECIAL_HANDLE + slot) == type) scePadResetLightBar(PAD_SPECIAL_HANDLE + slot); + return PAD_OK; } int APS5_VABI scePadSetLightBarForTracker(std::int32_t handle, const PadLightBarParam* color) { @@ -964,8 +970,8 @@ int APS5_VABI scePadGetSphereRadius(std::int32_t handle, float* radius) { } bool APS5_VABI scePadIsValidHandle(std::int32_t handle, std::int32_t portType) { - NotImplemented_nid_no_patch(__func__); - return 0; + const int type = OpenPortType(handle); + return Pad::IsInitialized() && type >= 0 && type == portType; } bool APS5_VABI scePadIsDS4Connected(std::int32_t handle) { diff --git a/core/libs/prx/libScePsm/Export.cpp b/core/libs/prx/libScePsm/Export.cpp index fb34206c21..d31d96f41f 100644 --- a/core/libs/prx/libScePsm/Export.cpp +++ b/core/libs/prx/libScePsm/Export.cpp @@ -1,3 +1,4 @@ +#include "prx/libScePsm/include/Api.hpp" #include #include #include "SceTypes.hpp" @@ -5,6 +6,329 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI ScePsmMonoArrayAddrWithSize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoArrayLength() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoDomainGet() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoFree() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoGcWbarrierGenericStore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringChars() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringFromUtf16() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringLength() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringNew() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringNewWrapper() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI ScePsmMonoStringToUtf8() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core14IntHandleTable13NextIntHandleEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics18ReleaseVideoMemoryEPv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjj() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjjPv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19ReleaseSystemMemoryEPv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjj() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjjPv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9PsmObjectC1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9PsmObjectC2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9PsmObjectD0Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9PsmObjectD1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9PsmObjectD2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4Cond4WaitEi() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4Cond6NotifyEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4CondC1ERNS2_5MutexEPKci() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4CondC2ERNS2_5MutexEPKci() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4CondD1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss4core9threading4CondD2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmArrayObj6LengthEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmArrayObj9ToPointerEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList10UnregisterEPFvPvES4_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList6UpdateEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList8RegisterEPFvPvES4_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallListC1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallListC2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmEventData8SetValueERKNS_4Json5ValueE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework10InitializeERKNS2_12PsmInitParamEiPPc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework20RegisterInternalCallEPKNS2_12PsmCallEntryE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework24RegisterPInvokeCallTableEPKNS2_15PsmLibraryEntryE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework3RunEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework8FinalizeEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmInitParamC1Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmInitParamC2Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj17NewAndStoreStringEPvPc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj6ToUtf8Ev() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj8FreeUtf8EPc() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework14PsmDelegateObj11FreePointerEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework14PsmDelegateObj15AllocatePointerEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework18PsmFrameworkCsharp21SetEnableNativeMethodEb() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList10UnregisterEPFvPvES4_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList6UpdateEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList8RegisterEPFvPvES4_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework23TerminatePsmEventCsharpEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework24InitializePsmEventCsharpEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework27TerminatePsmFrameworkCsharpEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework28InitializePsmFrameworkCsharpEv() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent10UnregisterEiPFvRKNS2_12PsmEventDataEPvES7_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent17RegisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent19UnregisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent4PostERKNS2_12PsmEventDataE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent4SendERKNS2_12PsmEventDataE() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent8RegisterEiPFvRKNS2_12PsmEventDataEPvES7_() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePsmRegisterInternalCall() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePsmRegisterPInvokeCallTable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libScePsm/include/Api.hpp b/core/libs/prx/libScePsm/include/Api.hpp new file mode 100644 index 0000000000..d2c372b43a --- /dev/null +++ b/core/libs/prx/libScePsm/include/Api.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "SceTypes.hpp" + +extern "C" { + +int APS5_VABI ScePsmMonoArrayAddrWithSize(void); +int APS5_VABI ScePsmMonoArrayLength(void); +int APS5_VABI ScePsmMonoDomainGet(void); +int APS5_VABI ScePsmMonoFree(void); +int APS5_VABI ScePsmMonoGcWbarrierGenericStore(void); +int APS5_VABI ScePsmMonoStringChars(void); +int APS5_VABI ScePsmMonoStringFromUtf16(void); +int APS5_VABI ScePsmMonoStringLength(void); +int APS5_VABI ScePsmMonoStringNew(void); +int APS5_VABI ScePsmMonoStringNewWrapper(void); +int APS5_VABI ScePsmMonoStringToUtf8(void); +int APS5_VABI _ZN3sce3pss4core14IntHandleTable13NextIntHandleEv(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics18ReleaseVideoMemoryEPv(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjj(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjjPv(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics19ReleaseSystemMemoryEPv(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjj(void); +int APS5_VABI _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjjPv(void); +int APS5_VABI _ZN3sce3pss4core9PsmObjectC1Ev(void); +int APS5_VABI _ZN3sce3pss4core9PsmObjectC2Ev(void); +int APS5_VABI _ZN3sce3pss4core9PsmObjectD0Ev(void); +int APS5_VABI _ZN3sce3pss4core9PsmObjectD1Ev(void); +int APS5_VABI _ZN3sce3pss4core9PsmObjectD2Ev(void); +int APS5_VABI _ZN3sce3pss4core9threading4Cond4WaitEi(void); +int APS5_VABI _ZN3sce3pss4core9threading4Cond6NotifyEv(void); +int APS5_VABI _ZN3sce3pss4core9threading4CondC1ERNS2_5MutexEPKci(void); +int APS5_VABI _ZN3sce3pss4core9threading4CondC2ERNS2_5MutexEPKci(void); +int APS5_VABI _ZN3sce3pss4core9threading4CondD1Ev(void); +int APS5_VABI _ZN3sce3pss4core9threading4CondD2Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmArrayObj6LengthEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmArrayObj9ToPointerEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList10UnregisterEPFvPvES4_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList6UpdateEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallList8RegisterEPFvPvES4_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallListC1Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework11PsmCallListC2Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmEventData8SetValueERKNS_4Json5ValueE(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework10InitializeERKNS2_12PsmInitParamEiPPc(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework20RegisterInternalCallEPKNS2_12PsmCallEntryE(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework24RegisterPInvokeCallTableEPKNS2_15PsmLibraryEntryE(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework3RunEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmFramework8FinalizeEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmInitParamC1Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmInitParamC2Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj17NewAndStoreStringEPvPc(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj6ToUtf8Ev(void); +int APS5_VABI _ZN3sce3pss5orbis9framework12PsmStringObj8FreeUtf8EPc(void); +int APS5_VABI _ZN3sce3pss5orbis9framework14PsmDelegateObj11FreePointerEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework14PsmDelegateObj15AllocatePointerEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework18PsmFrameworkCsharp21SetEnableNativeMethodEb(void); +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList10UnregisterEPFvPvES4_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList6UpdateEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework21PsmMainThreadCallList8RegisterEPFvPvES4_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework23TerminatePsmEventCsharpEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework24InitializePsmEventCsharpEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework27TerminatePsmFrameworkCsharpEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework28InitializePsmFrameworkCsharpEv(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent10UnregisterEiPFvRKNS2_12PsmEventDataEPvES7_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent17RegisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent19UnregisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent4PostERKNS2_12PsmEventDataE(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent4SendERKNS2_12PsmEventDataE(void); +int APS5_VABI _ZN3sce3pss5orbis9framework8PsmEvent8RegisterEiPFvRKNS2_12PsmEventDataEPvES7_(void); +int APS5_VABI scePsmRegisterInternalCall(void); +int APS5_VABI scePsmRegisterPInvokeCallTable(void); + +} diff --git a/core/libs/prx/libScePsm/libScePsm.hpp b/core/libs/prx/libScePsm/libScePsm.hpp index 6f70f09bee..8ba5c40198 100644 --- a/core/libs/prx/libScePsm/libScePsm.hpp +++ b/core/libs/prx/libScePsm/libScePsm.hpp @@ -1 +1,3 @@ #pragma once + +#include "prx/libScePsm/include/Api.hpp" diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index e11ae99de4..15ababe5db 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -437,6 +437,14 @@ int APS5_VABI sceRtcSetTick(RtcDateTime* time, const RtcTick* tick) { return 0; } +int APS5_VABI sceRtcConvertDateTimeToTick(const RtcDateTime* time, RtcTick* tick) { + return sceRtcGetTick(time, tick); +} + +int APS5_VABI sceRtcConvertTickToDateTime(const RtcTick* tick, RtcDateTime* time) { + return sceRtcSetTick(time, tick); +} + int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick) { if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; tick->tick = currentTick(); @@ -466,6 +474,10 @@ int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time) { return 0; } +int APS5_VABI sceRtcGetCurrentClockUtc(RtcDateTime* time) { + return sceRtcGetCurrentClock(time, 0); +} + int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick* utc, RtcTick* local_time) { if (!utc || !local_time) return SCE_RTC_ERROR_INVALID_POINTER; if (utc->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; @@ -494,16 +506,25 @@ int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds) { } int APS5_VABI sceRtcSetTime_t(RtcDateTime* time, int64_t seconds) { - if (!time) return SCE_RTC_ERROR_INVALID_POINTER; if (rangeChecked()) { if (seconds < 0) return SCE_RTC_ERROR_INVALID_VALUE; + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; *time = fromTick(UNIX_EPOCH_TICK + static_cast(seconds) * TICKS_PER_SECOND); return 0; } + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; *time = fromTick(UNIX_EPOCH_TICK + static_cast(static_cast(seconds)) * TICKS_PER_SECOND); return 0; } +int APS5_VABI sceRtcConvertDateTimeToTime_t(const RtcDateTime* time, int64_t* seconds) { + return sceRtcGetTime_t(time, seconds); +} + +int APS5_VABI sceRtcConvertTime_tToDateTime(int64_t seconds, RtcDateTime* time) { + return sceRtcSetTime_t(time, seconds); +} + int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime* time, uint64_t* win32_time) { if (!win32_time) return SCE_RTC_ERROR_INVALID_POINTER; if (const int result = validate(time); result != 0) return result; diff --git a/core/libs/prx/libSceRtc/include/Rtc.hpp b/core/libs/prx/libSceRtc/include/Rtc.hpp index 2e570dc02b..e4840b1e87 100644 --- a/core/libs/prx/libSceRtc/include/Rtc.hpp +++ b/core/libs/prx/libSceRtc/include/Rtc.hpp @@ -11,14 +11,19 @@ int APS5_VABI sceRtcGetDayOfWeek(int year, int month, int day); int APS5_VABI sceRtcGetTickResolution(void); int APS5_VABI sceRtcGetTick(const RtcDateTime* time, RtcTick* tick); int APS5_VABI sceRtcSetTick(RtcDateTime* time, const RtcTick* tick); +int APS5_VABI sceRtcConvertDateTimeToTick(const RtcDateTime* time, RtcTick* tick); +int APS5_VABI sceRtcConvertTickToDateTime(const RtcTick* tick, RtcDateTime* time); int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick); int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick* tick); int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int timeZoneMinutes); int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time); +int APS5_VABI sceRtcGetCurrentClockUtc(RtcDateTime* time); int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick* utc, RtcTick* localTime); int APS5_VABI sceRtcConvertLocalTimeToUtc(const RtcTick* localTime, RtcTick* utc); int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds); int APS5_VABI sceRtcSetTime_t(RtcDateTime* time, int64_t seconds); +int APS5_VABI sceRtcConvertDateTimeToTime_t(const RtcDateTime* time, int64_t* seconds); +int APS5_VABI sceRtcConvertTime_tToDateTime(int64_t seconds, RtcDateTime* time); int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime* time, uint64_t* win32Time); int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime* time, uint64_t win32Time); int APS5_VABI sceRtcGetDosTime(const RtcDateTime* time, uint32_t* dosTime); diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index dd4dd2c260..e50ffaab62 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -60,6 +60,7 @@ enum RudpOption { std::mutex g_mutex; bool g_inited = false; +uint16_t g_max_segment_size = RUDP_MAX_SEGMENT_SIZE; int g_next_ctx = 0; struct RudpContext { bool bound = false; @@ -495,8 +496,15 @@ int APS5_VABI sceRudpPollCancel() { return 0; } -int APS5_VABI sceRudpSetMaxSegmentSize() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceRudpSetMaxSegmentSize(uint16_t mss) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + if (!g_ctx.empty()) { + return RUDP_ERROR_NOT_ACCEPTABLE; + } + g_max_segment_size = mss; return 0; } diff --git a/core/libs/prx/libSceRudp/include/Api.hpp b/core/libs/prx/libSceRudp/include/Api.hpp index 44b6881d58..9ea882c3bc 100644 --- a/core/libs/prx/libSceRudp/include/Api.hpp +++ b/core/libs/prx/libSceRudp/include/Api.hpp @@ -39,6 +39,6 @@ int APS5_VABI sceRudpFlush(); int APS5_VABI sceRudpGetLocalInfo(); int APS5_VABI sceRudpListen(); int APS5_VABI sceRudpPollCancel(); -int APS5_VABI sceRudpSetMaxSegmentSize(); +int APS5_VABI sceRudpSetMaxSegmentSize(uint16_t mss); } diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 0627767b30..e32d58bb14 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -925,7 +925,7 @@ int APS5_VABI sceSaveDataTransferringMountPs4(const SaveDataTransferringMount* m static int dirNameSearchPs4(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { if (cond == nullptr || result == nullptr) { - throw std::runtime_error("sceSaveDataDirNameSearchPs4: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } result->hit_num = 0; result->set_num = 0; @@ -934,7 +934,7 @@ static int dirNameSearchPs4(const SaveDataDirNameSearchCond* cond, SaveDataDirNa int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { const int rc = dirNameSearchPs4(cond, result); - SAVEDATA_TRACE("dirNameSearchPs4 user=%d -> 0x%08x", cond->user_id, static_cast(rc)); + SAVEDATA_TRACE("dirNameSearchPs4 user=%d -> 0x%08x", cond != nullptr ? cond->user_id : -1, static_cast(rc)); return rc; } @@ -1053,7 +1053,7 @@ int APS5_VABI sceSaveDataInitialize(const void* init) { } int APS5_VABI sceSaveDataMount(const SaveDataMountPs4* mount, SaveDataMountResult* mount_result) { - if (mount == nullptr || mount->dir_name == nullptr) return SAVE_DATA_ERROR_PARAMETER; + if (mount == nullptr || mount_result == nullptr || mount->dir_name == nullptr) return SAVE_DATA_ERROR_PARAMETER; if (mount->title_id != nullptr) NotImplemented_nid_no_patch("sceSaveDataMount of another title's save data"); SaveDataMount3 converted{}; converted.user_id = mount->user_id; diff --git a/core/libs/prx/libSceUsbStorage/CMakeLists.txt b/core/libs/prx/libSceUsbStorage/CMakeLists.txt index a4280361de..9be261a135 100644 --- a/core/libs/prx/libSceUsbStorage/CMakeLists.txt +++ b/core/libs/prx/libSceUsbStorage/CMakeLists.txt @@ -1,6 +1,10 @@ -add_library(libSceUsbStorage SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceUsbStorage SHARED EXCLUDE_FROM_ALL Export.cpp src/HostVolumes.cpp) target_include_directories(libSceUsbStorage PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceUsbStorage PRIVATE libc) +if(WIN32) + target_sources(libSceUsbStorage PRIVATE src/HostVolumesWindows.cpp) + target_link_libraries(libSceUsbStorage PRIVATE setupapi cfgmgr32) +endif() configure_windows_unwind(libSceUsbStorage) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceUsbStorage/Export.cpp b/core/libs/prx/libSceUsbStorage/Export.cpp index 978a9dc386..ef68706db3 100644 --- a/core/libs/prx/libSceUsbStorage/Export.cpp +++ b/core/libs/prx/libSceUsbStorage/Export.cpp @@ -1,31 +1,300 @@ #include "prx/libSceSystemService/include/SystemService.hpp" +#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/include/Api.hpp" #include "prx/libSceUsbStorage/UsbStorage.hpp" +#include "prx/libSceUsbStorage/include/HostVolumes.hpp" + +static_assert(sizeof(UsbStorageEvent) == 16); +static_assert(offsetof(UsbStorageEvent, device) == 4); +static_assert(offsetof(UsbStorageEvent, flags) == 8); +static_assert(sizeof(UsbStorageDeviceInfo) == 0x338); +static_assert(offsetof(UsbStorageDeviceInfo, vendorId) == 0x10); +static_assert(offsetof(UsbStorageDeviceInfo, productId) == 0x12); +static_assert(offsetof(UsbStorageDeviceInfo, bcdDevice) == 0x14); +static_assert(offsetof(UsbStorageDeviceInfo, vendor) == 0x16); +static_assert(offsetof(UsbStorageDeviceInfo, vendorLength) == 0x118); +static_assert(offsetof(UsbStorageDeviceInfo, product) == 0x120); +static_assert(offsetof(UsbStorageDeviceInfo, productLength) == 0x220); +static_assert(offsetof(UsbStorageDeviceInfo, serialNumber) == 0x228); +static_assert(offsetof(UsbStorageDeviceInfo, serialNumberLength) == 0x328); +static_assert(offsetof(UsbStorageDeviceInfo, flags) == 0x330); + +namespace { + +constexpr std::uint64_t GetdentsBufferSize = 0x8000; +constexpr std::size_t DirentHeaderSize = 8; +constexpr std::size_t NameLimit = 255; +constexpr std::uint8_t DirectoryType = 4; +constexpr std::uint8_t RegularType = 8; +constexpr auto RescanInterval = std::chrono::seconds(1); + +struct Device { + UsbStorage::HostVolume volume; + std::vector maps; +}; + +struct Registration { + std::int32_t eventType; + UsbStorageCallback callback; + void* arg; +}; + +struct DirectoryEntry { + std::string name; + bool directory; +}; + +struct DirectoryScan { + std::vector entries; + std::size_t next = 0; +}; + +struct Service { + std::mutex mutex; + std::mutex dispatch; + std::condition_variable wake; + bool initialized = false; + bool stopping = false; + Pthread monitor = nullptr; + std::array, UsbStorage::MaxDevices> devices; + std::vector registrations; + std::map scans; + std::int32_t nextDescriptor = 1; +}; + +Service& State() { + static auto* service = new Service; + return *service; +} + +std::string MountPoint(std::size_t slot) { + return "/mnt/usb" + std::to_string(slot); +} + +std::uint64_t Flags(const UsbStorage::HostVolume& volume) { + std::uint64_t flags = volume.fileSystem == UsbStorage::FileSystem::ExFat ? UsbStorage::ExFatFlag : UsbStorage::FatFlag; + if (volume.readOnly) flags |= UsbStorage::ReadOnlyFlag; + return flags; +} + +UsbStorageEvent Event(std::int32_t type, std::size_t slot, const UsbStorage::HostVolume& volume) { + return {static_cast(type), static_cast(slot), Flags(volume)}; +} + +Device* Find(Service& service, std::uint32_t device) { + if (device >= service.devices.size() || !service.devices[device]) return nullptr; + return &*service.devices[device]; +} + +void Update(Service& service, std::vector volumes, std::vector& events) { + for (std::size_t slot = 0; slot < service.devices.size(); ++slot) { + auto& device = service.devices[slot]; + if (!device) continue; + const auto found = std::find_if(volumes.begin(), volumes.end(), [&](const auto& volume) { return volume.key == device->volume.key; }); + if (found != volumes.end()) { + if (found->root != device->volume.root) AddPathAlias_nid_no_patch(MountPoint(slot).c_str(), found->root.string().c_str()); + device->volume = std::move(*found); + volumes.erase(found); + continue; + } + events.push_back(Event(UsbStorage::UnmountEvent, slot, device->volume)); + RemovePathAlias_nid_no_patch(MountPoint(slot).c_str()); + device.reset(); + } + for (auto& volume : volumes) { + const auto free = std::find_if(service.devices.begin(), service.devices.end(), [](const auto& device) { return !device; }); + if (free == service.devices.end()) break; + const auto slot = static_cast(free - service.devices.begin()); + AddPathAlias_nid_no_patch(MountPoint(slot).c_str(), volume.root.string().c_str()); + events.push_back(Event(UsbStorage::MountEvent, slot, volume)); + free->emplace(Device{std::move(volume), {}}); + } +} + +std::vector::iterator Registered(Service& service, std::int32_t eventType, UsbStorageCallback callback) { + return std::find_if(service.registrations.begin(), service.registrations.end(), + [&](const auto& registration) { return registration.eventType == eventType && registration.callback == callback; }); +} + +void Dispatch(Service& service, const std::vector& events) { + for (const auto& event : events) { + std::vector registrations; + { + std::lock_guard lock(service.mutex); + registrations = service.registrations; + } + for (const auto& registration : registrations) { + if (registration.eventType != static_cast(event.event)) continue; + std::lock_guard calling(service.dispatch); + void* arg = nullptr; + { + std::lock_guard lock(service.mutex); + const auto current = Registered(service, registration.eventType, registration.callback); + if (current == service.registrations.end()) continue; + arg = current->arg; + } + registration.callback(&event, arg); + } + } +} + +void* APS5_VABI Monitor(void*) { + auto& service = State(); + std::unique_lock lock(service.mutex); + while (!service.wake.wait_for(lock, RescanInterval, [&] { return service.stopping; })) { + lock.unlock(); + auto volumes = UsbStorage::ScanHostVolumes(); + lock.lock(); + if (service.stopping) break; + std::vector events; + Update(service, std::move(volumes), events); + lock.unlock(); + Dispatch(service, events); + lock.lock(); + } + return nullptr; +} + +std::filesystem::path HostName(std::string_view name) { + return std::filesystem::path(std::u8string(name.begin(), name.end())); +} + +std::vector DirectoryNames(const char* directory, const char* function) { + std::vector names; + if (directory == nullptr) return names; + std::string_view text(directory); + if (text.find('\\') != std::string_view::npos) throw std::runtime_error(std::string(function) + ": a directory with a backslash is not implemented"); + while (!text.empty()) { + const auto separator = text.find('/'); + const auto name = text.substr(0, separator); + if (name == "." || name == "..") throw std::runtime_error(std::string(function) + ": a directory with . or .. components is not implemented"); + if (HostName(name).has_root_path()) throw std::runtime_error(std::string(function) + ": a directory with a root name is not implemented"); + if (!name.empty()) names.emplace_back(name); + if (separator == std::string_view::npos) break; + text.remove_prefix(separator + 1); + } + return names; +} + +std::filesystem::path HostDirectory(const Device& device, const std::vector& names) { + auto host = device.volume.root; + for (const auto& name : names) host /= HostName(name); + return host; +} + +std::string Map(Device& device, std::uint32_t slot, const char* directory, const char* function) { + const auto names = DirectoryNames(directory, function); + auto path = MountPoint(slot); + for (const auto& name : names) path += "/" + name; + std::error_code error; + if (!std::filesystem::is_directory(HostDirectory(device, names), error)) { + throw std::runtime_error(std::string(function) + ": mapping a directory the device does not hold is not implemented"); + } + if (path.size() >= UsbStorage::MountPointSize) throw std::runtime_error(std::string(function) + ": a mount point longer than 127 bytes is not implemented"); + if (std::find(device.maps.begin(), device.maps.end(), path) != device.maps.end()) { + throw std::runtime_error(std::string(function) + ": mapping a mapped directory again is not implemented"); + } + device.maps.push_back(path); + return path; +} + +void WriteMountPoint(char* mountPoint, const std::string& path) { + if (mountPoint != nullptr) std::memcpy(mountPoint, path.c_str(), path.size() + 1); +} + +std::uint64_t CopyString(char (&destination)[256], const std::string& text, const char* function) { + if (text.size() >= sizeof(destination)) throw std::runtime_error(std::string(function) + ": a device string longer than 255 bytes is not implemented"); + std::memcpy(destination, text.data(), text.size()); + return text.size(); +} + +} extern "C" { std::int32_t APS5_VABI sceUsbStorageInit(void* attr) { - (void)attr; + if (attr != nullptr) throw std::runtime_error("sceUsbStorageInit: a thread attribute is not implemented"); + auto& service = State(); + { + std::lock_guard lock(service.mutex); + if (service.stopping) throw std::runtime_error("sceUsbStorageInit: initializing while sceUsbStorageTerm runs is not implemented"); + if (service.initialized) return 0; + } + auto volumes = UsbStorage::ScanHostVolumes(); + std::lock_guard lock(service.mutex); + if (service.stopping) throw std::runtime_error("sceUsbStorageInit: initializing while sceUsbStorageTerm runs is not implemented"); + if (service.initialized) return 0; + std::vector events; + Update(service, std::move(volumes), events); + if (scePthreadCreate(&service.monitor, nullptr, Monitor, nullptr, "SceUsbStorageMonitor") != 0) { + Update(service, {}, events); + service.monitor = nullptr; + throw std::runtime_error("sceUsbStorageInit: scePthreadCreate failed"); + } + service.initialized = true; return 0; } std::int32_t APS5_VABI sceUsbStorageTerm(void) { + auto& service = State(); + Pthread monitor = nullptr; + { + std::lock_guard lock(service.mutex); + if (!service.initialized) return 0; + if (service.stopping) throw std::runtime_error("sceUsbStorageTerm: a concurrent termination is not implemented"); + if (scePthreadEqual(scePthreadSelf(), service.monitor) != 0) { + throw std::runtime_error("sceUsbStorageTerm: terminating from a USB storage callback is not implemented"); + } + service.stopping = true; + monitor = service.monitor; + } + service.wake.notify_all(); + if (scePthreadJoin(monitor, nullptr) != 0) throw std::runtime_error("sceUsbStorageTerm: scePthreadJoin failed"); + std::lock_guard lock(service.mutex); + std::vector events; + Update(service, {}, events); + service.registrations.clear(); + service.scans.clear(); + service.monitor = nullptr; + service.stopping = false; + service.initialized = false; return 0; } std::int32_t APS5_VABI sceUsbStorageRegisterCallback(std::int32_t eventType, UsbStorageCallback callback, void* arg) { - (void)arg; if (callback == nullptr) { return UsbStorage::InvalidArgument; } - if (eventType < UsbStorage::CallbackEventTypeFirst || eventType > UsbStorage::CallbackEventTypeLast) { + if (eventType != UsbStorage::MountEvent && eventType != UsbStorage::UnmountEvent) { return UsbStorage::InvalidArgument; } - NotImplemented_nid_no_patch(__func__); + auto& service = State(); + std::lock_guard lock(service.mutex); + if (!service.initialized) throw std::runtime_error("sceUsbStorageRegisterCallback: registering before sceUsbStorageInit is not implemented"); + if (Registered(service, eventType, callback) != service.registrations.end()) { + throw std::runtime_error("sceUsbStorageRegisterCallback: registering a registered callback again is not implemented"); + } + service.registrations.push_back({eventType, callback, arg}); return 0; } @@ -33,9 +302,23 @@ std::int32_t APS5_VABI sceUsbStorageUnregisterCallback(std::int32_t eventType, U if (callback == nullptr) { return UsbStorage::InvalidArgument; } - if (eventType < UsbStorage::CallbackEventTypeFirst || eventType > UsbStorage::CallbackEventTypeLast) { + if (eventType != UsbStorage::MountEvent && eventType != UsbStorage::UnmountEvent) { return UsbStorage::InvalidArgument; } + auto& service = State(); + bool fromCallback = false; + { + std::lock_guard lock(service.mutex); + fromCallback = service.monitor != nullptr && scePthreadEqual(scePthreadSelf(), service.monitor) != 0; + } + std::unique_lock calling(service.dispatch, std::defer_lock); + if (!fromCallback) calling.lock(); + std::lock_guard lock(service.mutex); + const auto found = Registered(service, eventType, callback); + if (found != service.registrations.end()) { + service.registrations.erase(found); + return 0; + } NotImplemented_nid_no_patch(__func__); return 0; } @@ -79,75 +362,166 @@ std::int32_t APS5_VABI sceUsbStorageUnregisterCallbackForMapUnavailable(std::uin } std::int32_t APS5_VABI sceUsbStorageGetDeviceInfo(std::uint32_t device, UsbStorageDeviceInfo* info) { - (void)device; if (info == nullptr) { return UsbStorage::InvalidArgument; } - return UsbStorage::DeviceNotFound; + auto& service = State(); + std::lock_guard lock(service.mutex); + const auto* found = Find(service, device); + if (found == nullptr) { + return UsbStorage::DeviceNotFound; + } + const auto& volume = found->volume; + UsbStorageDeviceInfo record{}; + record.volumeSize = volume.volumeSize; + record.vendorId = volume.vendorId; + record.productId = volume.productId; + record.bcdDevice = volume.bcdDevice; + record.vendorLength = CopyString(record.vendor, volume.vendor, __func__); + record.productLength = CopyString(record.product, volume.product, __func__); + record.serialNumberLength = CopyString(record.serialNumber, volume.serialNumber, __func__); + record.flags = Flags(volume); + std::memcpy(info, &record, sizeof(record)); + return 0; } std::int32_t APS5_VABI sceUsbStorageGetDeviceList(std::uint32_t* devices, std::int32_t* count) { if (devices == nullptr || count == nullptr) { return UsbStorage::InvalidArgument; } - *count = 0; + auto& service = State(); + std::lock_guard lock(service.mutex); + std::int32_t listed = 0; + for (std::size_t slot = 0; slot < service.devices.size(); ++slot) { + if (service.devices[slot]) devices[listed++] = static_cast(slot); + } + *count = listed; return 0; } int APS5_VABI sceUsbStorageGetMountPointOfShellCore(uint32_t device, char* mountPoint) { - (void)device; - (void)mountPoint; - NotImplemented_nid_no_patch(__func__); + auto& service = State(); + std::lock_guard lock(service.mutex); + if (Find(service, device) == nullptr || mountPoint == nullptr) { + NotImplemented_nid_no_patch(__func__); + } + WriteMountPoint(mountPoint, MountPoint(device)); return 0; } bool APS5_VABI sceUsbStorageIsExist(std::uint32_t device, const char* directory, std::uint8_t* exists) { - (void)device; - (void)directory; - *exists = 0; + if (exists == nullptr) throw std::runtime_error("sceUsbStorageIsExist: a null output is not implemented"); + auto& service = State(); + std::lock_guard lock(service.mutex); + const auto* found = Find(service, device); + std::error_code error; + *exists = found != nullptr && std::filesystem::is_directory(HostDirectory(*found, DirectoryNames(directory, __func__)), error) ? 1 : 0; return true; } std::int32_t APS5_VABI sceUsbStorageRequestMap(std::uint32_t device, const char* directory, std::int32_t flags, std::uint64_t volumeSize, char* mountPoint, std::uint64_t* mountId) { - (void)device; - (void)directory; - (void)flags; - (void)volumeSize; - (void)mountPoint; - (void)mountId; - return UsbStorage::DeviceNotFound; + auto& service = State(); + std::lock_guard lock(service.mutex); + auto* found = Find(service, device); + if (found == nullptr) { + return UsbStorage::DeviceNotFound; + } + if (flags != 0 || volumeSize != 0) throw std::runtime_error("sceUsbStorageRequestMap: nonzero flags or a volume size are not implemented"); + WriteMountPoint(mountPoint, Map(*found, device, directory, __func__)); + if (mountId != nullptr) *mountId = 0; + return 0; } std::int32_t APS5_VABI sceUsbStorageRequestUnmap(std::uint32_t device, const char* directory) { - (void)device; if (directory == nullptr) { return UsbStorage::InvalidArgument; } + auto& service = State(); + std::lock_guard lock(service.mutex); + if (auto* found = Find(service, device)) { + const auto map = std::find(found->maps.begin(), found->maps.end(), std::string_view(directory)); + if (map != found->maps.end()) { + found->maps.erase(map); + return 0; + } + } NotImplemented_nid_no_patch(__func__); return 0; } std::int32_t APS5_VABI sceUsbStorageGetdentsOpen(std::uint32_t device, std::int32_t* fd, std::uint64_t* bufferSize) { - (void)device; if (fd == nullptr || bufferSize == nullptr) { return UsbStorage::InvalidArgument; } - return UsbStorage::DeviceNotFound; + auto& service = State(); + std::lock_guard lock(service.mutex); + const auto* found = Find(service, device); + if (found == nullptr) { + return UsbStorage::DeviceNotFound; + } + DirectoryScan scan; + std::error_code error; + for (std::filesystem::directory_iterator entry(found->volume.root, error), end; !error && entry != end; entry.increment(error)) { + const auto name = entry->path().filename().u8string(); + if (name.empty() || name.front() == u8'.') continue; + if (name.size() > NameLimit) throw std::runtime_error("sceUsbStorageGetdentsOpen: a name longer than 255 bytes is not implemented"); + std::error_code typeError; + scan.entries.push_back({std::string(name.begin(), name.end()), entry->is_directory(typeError)}); + } + if (error) throw std::runtime_error("sceUsbStorageGetdentsOpen: listing the device failed: " + error.message()); + if (service.nextDescriptor == std::numeric_limits::max()) throw std::runtime_error("sceUsbStorageGetdentsOpen: the descriptor numbers are exhausted"); + const auto descriptor = service.nextDescriptor++; + service.scans.emplace(descriptor, std::move(scan)); + *fd = descriptor; + *bufferSize = GetdentsBufferSize; + return 0; } std::int32_t APS5_VABI sceUsbStorageGetdentsRead(std::int32_t fd, void* buffer, std::uint64_t bufferSize, std::uint64_t* bytesRead) { - (void)fd; - (void)bufferSize; if (buffer == nullptr || bytesRead == nullptr) { return UsbStorage::InvalidArgument; } - *bytesRead = 0; - return UsbStorage::NotSupported; + auto& service = State(); + std::lock_guard lock(service.mutex); + const auto found = service.scans.find(fd); + if (found == service.scans.end()) { + *bytesRead = 0; + return UsbStorage::NotSupported; + } + auto& scan = found->second; + auto* const output = static_cast(buffer); + std::uint64_t written = 0; + while (scan.next < scan.entries.size()) { + const auto& entry = scan.entries[scan.next]; + const auto length = static_cast((DirentHeaderSize + entry.name.size() + 1 + 7) & ~std::size_t{7}); + if (length > bufferSize - written) break; + auto* const record = output + written; + const auto fileNumber = static_cast(scan.next + 1); + const auto type = entry.directory ? DirectoryType : RegularType; + const auto nameLength = static_cast(entry.name.size()); + std::memset(record, 0, length); + std::memcpy(record, &fileNumber, sizeof(fileNumber)); + std::memcpy(record + 4, &length, sizeof(length)); + std::memcpy(record + 6, &type, sizeof(type)); + std::memcpy(record + 7, &nameLength, sizeof(nameLength)); + std::memcpy(record + DirentHeaderSize, entry.name.data(), entry.name.size()); + written += length; + ++scan.next; + } + if (written == 0 && scan.next < scan.entries.size()) { + throw std::runtime_error("sceUsbStorageGetdentsRead: a buffer smaller than the next entry is not implemented"); + } + *bytesRead = written; + return 0; } std::int32_t APS5_VABI sceUsbStorageGetdentsClose(std::int32_t fd) { - (void)fd; + auto& service = State(); + std::lock_guard lock(service.mutex); + if (service.scans.erase(fd) == 1) { + return 0; + } NotImplemented_nid_no_patch(__func__); return 0; } @@ -159,13 +533,16 @@ int APS5_VABI sceUsbStorageClearFakeWriteProtection() { std::int32_t APS5_VABI sceUsbStorageRequestMapWSB(std::uint32_t device, const char* subpath, std::int32_t flags, std::int32_t unknown, char* mountPath, std::int32_t* status) { - (void)device; - (void)subpath; - (void)flags; - (void)unknown; - (void)mountPath; - (void)status; - return UsbStorage::DeviceNotFound; + auto& service = State(); + std::lock_guard lock(service.mutex); + auto* found = Find(service, device); + if (found == nullptr) { + return UsbStorage::DeviceNotFound; + } + if (flags != 0 || unknown != 0) throw std::runtime_error("sceUsbStorageRequestMapWSB: nonzero flags or a nonzero fourth argument are not implemented"); + WriteMountPoint(mountPath, Map(*found, device, subpath, __func__)); + if (status != nullptr) *status = 0; + return 0; } int APS5_VABI sceUsbStorageSetFakeMapLockForDebug() { diff --git a/core/libs/prx/libSceUsbStorage/UsbStorage.hpp b/core/libs/prx/libSceUsbStorage/UsbStorage.hpp index b3bc2d24ab..3ccc495a6e 100644 --- a/core/libs/prx/libSceUsbStorage/UsbStorage.hpp +++ b/core/libs/prx/libSceUsbStorage/UsbStorage.hpp @@ -12,13 +12,40 @@ inline constexpr std::int32_t InvalidArgument = static_cast(0x80f4 inline constexpr std::int32_t DeviceNotFound = static_cast(0x80f40004); inline constexpr std::int32_t NotSupported = static_cast(0x80f40005); -inline constexpr std::int32_t CallbackEventTypeFirst = 1; -inline constexpr std::int32_t CallbackEventTypeLast = 2; +inline constexpr std::int32_t MountEvent = 1; +inline constexpr std::int32_t UnmountEvent = 2; + +inline constexpr std::uint64_t ReadOnlyFlag = 1ull << 4; +inline constexpr std::uint64_t FatFlag = 1ull << 6; +inline constexpr std::uint64_t ExFatFlag = 1ull << 7; + +inline constexpr std::size_t MaxDevices = 8; +inline constexpr std::size_t MountPointSize = 128; } -struct UsbStorageEvent; -struct UsbStorageDeviceInfo; +struct UsbStorageEvent { + std::uint32_t event; + std::uint32_t device; + std::uint64_t flags; +}; + +struct UsbStorageDeviceInfo { + std::uint64_t volumeSize; + std::uint64_t reserved; + std::uint16_t vendorId; + std::uint16_t productId; + std::uint16_t bcdDevice; + char vendor[256]; + std::uint8_t padding[2]; + std::uint64_t vendorLength; + char product[256]; + std::uint64_t productLength; + char serialNumber[256]; + std::uint64_t serialNumberLength; + std::uint64_t flags; +}; + using UsbStorageCallback = void (APS5_VABI *)(const UsbStorageEvent* event, void* userData); extern "C" { diff --git a/core/libs/prx/libSceUsbStorage/include/HostVolumes.hpp b/core/libs/prx/libSceUsbStorage/include/HostVolumes.hpp new file mode 100644 index 0000000000..6cf25c4b02 --- /dev/null +++ b/core/libs/prx/libSceUsbStorage/include/HostVolumes.hpp @@ -0,0 +1,56 @@ +#ifndef CORE_LIBS_PRX_LIBSCEUSBSTORAGE_INCLUDE_HOSTVOLUMES_HPP +#define CORE_LIBS_PRX_LIBSCEUSBSTORAGE_INCLUDE_HOSTVOLUMES_HPP + +#include +#include +#include +#include +#include + +namespace UsbStorage { + +enum class FileSystem { + Fat, + ExFat, +}; + +struct HostVolume { + std::string key; + std::filesystem::path root; + std::uint64_t volumeSize = 0; + std::uint16_t vendorId = 0; + std::uint16_t productId = 0; + std::uint16_t bcdDevice = 0; + std::string vendor; + std::string product; + std::string serialNumber; + bool readOnly = false; + FileSystem fileSystem = FileSystem::Fat; +}; + +std::vector ScanSysfsVolumes(const std::filesystem::path& mountInfo, const std::filesystem::path& sysRoot); +#ifdef _WIN32 +std::vector ScanWindowsVolumes(); +#endif +std::vector ScanHostVolumes(); + +} + +struct UsbStorageTestVolume { + const char* key; + const char* root; + std::uint64_t volumeSize; + std::uint16_t vendorId; + std::uint16_t productId; + std::uint16_t bcdDevice; + const char* vendor; + const char* product; + const char* serialNumber; + bool readOnly; + bool exFat; +}; + +extern "C" void SetUsbStorageHostRootsForTest_nid_no_patch(const char* mountInfo, const char* sysRoot); +extern "C" void SetUsbStorageHostVolumesForTest_nid_no_patch(const UsbStorageTestVolume* volumes, std::size_t count); + +#endif diff --git a/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp b/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp new file mode 100644 index 0000000000..d34861756d --- /dev/null +++ b/core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp @@ -0,0 +1,213 @@ +#include "prx/libSceUsbStorage/include/HostVolumes.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace UsbStorage { +namespace { + +std::mutex rootsMutex; +std::optional> testRoots; +std::optional> testVolumes; + +std::filesystem::path Utf8Path(std::string_view text) { + return std::filesystem::path(std::u8string(text.begin(), text.end())); +} + +std::optional ReadText(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return std::nullopt; + std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (file.bad()) return std::nullopt; + while (!text.empty() && (text.back() == '\n' || text.back() == '\r' || text.back() == ' ' || text.back() == '\t')) text.pop_back(); + return text; +} + +std::optional ReadHex(const std::filesystem::path& path) { + const auto text = ReadText(path); + if (!text || text->empty() || text->size() > 4) return std::nullopt; + std::uint16_t value = 0; + const auto [end, error] = std::from_chars(text->data(), text->data() + text->size(), value, 16); + if (error != std::errc{} || end != text->data() + text->size()) return std::nullopt; + return value; +} + +std::string Unescape(std::string_view text) { + std::string result; + const auto octal = [&](std::size_t at) { return text[at] >= '0' && text[at] <= '7'; }; + for (std::size_t i = 0; i < text.size(); ++i) { + if (text[i] == '\\' && i + 3 < text.size() && octal(i + 1) && octal(i + 2) && octal(i + 3)) { + result += static_cast((text[i + 1] - '0') * 64 + (text[i + 2] - '0') * 8 + (text[i + 3] - '0')); + i += 3; + continue; + } + result += text[i]; + } + return result; +} + +std::vector Fields(std::string_view line) { + std::vector fields; + while (!line.empty()) { + const auto separator = line.find(' '); + if (separator != 0) fields.push_back(line.substr(0, separator)); + if (separator == std::string_view::npos) break; + line.remove_prefix(separator + 1); + } + return fields; +} + +bool ReadOnly(std::string_view options) { + while (true) { + const auto separator = options.find(','); + if (options.substr(0, separator) == "ro") return true; + if (separator == std::string_view::npos) return false; + options.remove_prefix(separator + 1); + } +} + +std::optional FileSystemOf(std::string_view type) { + if (type == "vfat" || type == "msdos") return FileSystem::Fat; + if (type == "exfat") return FileSystem::ExFat; + return std::nullopt; +} + +std::optional Describe(const std::filesystem::path& sysRoot, std::string_view device, const std::string& mountPoint, + FileSystem fileSystem, bool readOnly) { + std::error_code error; + const auto top = std::filesystem::weakly_canonical(sysRoot, error); + if (error) return std::nullopt; + const auto block = std::filesystem::canonical(sysRoot / "dev" / "block" / std::filesystem::path(std::string(device)), error); + if (error) return std::nullopt; + std::optional scsi; + std::optional usb; + for (auto current = block; current != top && current.has_relative_path(); current = current.parent_path()) { + if (!scsi && std::filesystem::is_regular_file(current / "vendor", error) && std::filesystem::is_regular_file(current / "model", error)) { + scsi = current; + } + if (ReadText(current / "bInterfaceClass") == "08") { + usb = current.parent_path(); + break; + } + } + if (!usb) return std::nullopt; + const auto vendorId = ReadHex(*usb / "idVendor"); + const auto productId = ReadHex(*usb / "idProduct"); + const auto bcdDevice = ReadHex(*usb / "bcdDevice"); + if (!vendorId || !productId || !bcdDevice) return std::nullopt; + const auto root = Utf8Path(mountPoint); + if (!std::filesystem::is_directory(root, error)) return std::nullopt; + const auto space = std::filesystem::space(root, error); + if (error) return std::nullopt; + HostVolume volume; + volume.key = std::string(device) + ' ' + mountPoint; + volume.root = root; + volume.volumeSize = space.capacity; + volume.vendorId = *vendorId; + volume.productId = *productId; + volume.bcdDevice = *bcdDevice; + if (scsi) { + volume.vendor = ReadText(*scsi / "vendor").value_or(std::string()); + volume.product = ReadText(*scsi / "model").value_or(std::string()); + } + volume.serialNumber = ReadText(*usb / "serial").value_or(std::string()); + volume.readOnly = readOnly; + volume.fileSystem = fileSystem; + return volume; +} + +} + +std::vector ScanSysfsVolumes(const std::filesystem::path& mountInfo, const std::filesystem::path& sysRoot) { + std::vector volumes; + std::ifstream file(mountInfo, std::ios::binary); + if (!file) return volumes; + std::set> devices; + std::string line; + while (std::getline(file, line)) { + const auto fields = Fields(line); + const auto separator = std::find(fields.begin(), fields.end(), std::string_view("-")); + if (separator - fields.begin() < 6 || fields.end() - separator < 3) continue; + const auto fileSystem = FileSystemOf(separator[1]); + if (!fileSystem || fields[3] != "/" || devices.contains(fields[2])) continue; + devices.emplace(fields[2]); + if (auto volume = Describe(sysRoot, fields[2], Unescape(fields[4]), *fileSystem, ReadOnly(fields[5]))) volumes.push_back(std::move(*volume)); + } + return volumes; +} + +namespace { + +bool PassthroughEnabled() { + const char* configured = std::getenv("APS5_USB_STORAGE_PASSTHROUGH"); + return configured != nullptr && configured[0] != '\0'; +} + +} + +std::vector ScanHostVolumes() { + if (!PassthroughEnabled()) return {}; + std::optional> roots; + { + std::lock_guard lock(rootsMutex); + if (testVolumes) return *testVolumes; + roots = testRoots; + } + if (roots) return ScanSysfsVolumes(roots->first, roots->second); +#ifdef _WIN32 + return ScanWindowsVolumes(); +#elif defined(__linux__) + return ScanSysfsVolumes("/proc/self/mountinfo", "/sys"); +#else + return {}; +#endif +} + +} + +extern "C" void SetUsbStorageHostRootsForTest_nid_no_patch(const char* mountInfo, const char* sysRoot) { + std::lock_guard lock(UsbStorage::rootsMutex); + UsbStorage::testVolumes.reset(); + if (mountInfo == nullptr || sysRoot == nullptr) { + UsbStorage::testRoots.reset(); + return; + } + UsbStorage::testRoots.emplace(UsbStorage::Utf8Path(mountInfo), UsbStorage::Utf8Path(sysRoot)); +} + +extern "C" void SetUsbStorageHostVolumesForTest_nid_no_patch(const UsbStorageTestVolume* volumes, std::size_t count) { + std::lock_guard lock(UsbStorage::rootsMutex); + UsbStorage::testRoots.reset(); + if (volumes == nullptr) { + UsbStorage::testVolumes.reset(); + return; + } + const auto text = [](const char* value) { return value == nullptr ? std::string() : std::string(value); }; + std::vector copies; + for (std::size_t i = 0; i < count; ++i) { + UsbStorage::HostVolume volume; + volume.key = text(volumes[i].key); + volume.root = UsbStorage::Utf8Path(text(volumes[i].root)); + volume.volumeSize = volumes[i].volumeSize; + volume.vendorId = volumes[i].vendorId; + volume.productId = volumes[i].productId; + volume.bcdDevice = volumes[i].bcdDevice; + volume.vendor = text(volumes[i].vendor); + volume.product = text(volumes[i].product); + volume.serialNumber = text(volumes[i].serialNumber); + volume.readOnly = volumes[i].readOnly; + volume.fileSystem = volumes[i].exFat ? UsbStorage::FileSystem::ExFat : UsbStorage::FileSystem::Fat; + copies.push_back(std::move(volume)); + } + UsbStorage::testVolumes = std::move(copies); +} diff --git a/core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp b/core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp new file mode 100644 index 0000000000..edb88eaee0 --- /dev/null +++ b/core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp @@ -0,0 +1,198 @@ +#include "prx/libSceUsbStorage/include/HostVolumes.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace UsbStorage { +namespace { + +struct Handle { + HANDLE value; + ~Handle() { + if (value != INVALID_HANDLE_VALUE) CloseHandle(value); + } +}; + +struct ErrorMode { + DWORD previous = 0; + ErrorMode() { SetThreadErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX, &previous); } + ~ErrorMode() { SetThreadErrorMode(previous, nullptr); } +}; + +struct UsbIdentity { + std::uint16_t vendorId; + std::uint16_t productId; + std::uint16_t bcdDevice; +}; + +std::string Trimmed(std::string text) { + while (!text.empty() && (text.back() == ' ' || text.back() == '\t')) text.pop_back(); + while (!text.empty() && (text.front() == ' ' || text.front() == '\t')) text.erase(text.begin()); + return text; +} + +std::string DescriptorString(const std::vector& descriptor, DWORD size, DWORD offset) { + if (offset == 0 || offset >= size) return {}; + const auto* begin = reinterpret_cast(descriptor.data() + offset); + return Trimmed(std::string(begin, strnlen(begin, size - offset))); +} + +std::optional Hex(std::wstring_view text) { + if (text.size() < 4) return std::nullopt; + std::uint16_t value = 0; + for (std::size_t i = 0; i < 4; ++i) { + const wchar_t digit = text[i]; + value = static_cast(value << 4); + if (digit >= L'0' && digit <= L'9') value = static_cast(value | (digit - L'0')); + else if (digit >= L'a' && digit <= L'f') value = static_cast(value | (digit - L'a' + 10)); + else if (digit >= L'A' && digit <= L'F') value = static_cast(value | (digit - L'A' + 10)); + else return std::nullopt; + } + return value; +} + +std::optional TaggedHex(std::wstring_view text, std::wstring_view tag) { + for (std::size_t at = 0; at + tag.size() <= text.size(); ++at) { + if (_wcsnicmp(text.data() + at, tag.data(), tag.size()) == 0) return Hex(text.substr(at + tag.size())); + } + return std::nullopt; +} + +std::optional DeviceNumber(HANDLE handle) { + STORAGE_DEVICE_NUMBER number{}; + DWORD bytes = 0; + if (!DeviceIoControl(handle, IOCTL_STORAGE_GET_DEVICE_NUMBER, nullptr, 0, &number, sizeof(number), &bytes, nullptr)) return std::nullopt; + return number; +} + +std::optional DiskInstance(DWORD diskNumber) { + const HDEVINFO set = SetupDiGetClassDevsW(&GUID_DEVINTERFACE_DISK, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (set == INVALID_HANDLE_VALUE) return std::nullopt; + const std::unique_ptr owner(set, &SetupDiDestroyDeviceInfoList); + SP_DEVICE_INTERFACE_DATA interfaceData{}; + interfaceData.cbSize = sizeof(interfaceData); + for (DWORD index = 0; SetupDiEnumDeviceInterfaces(set, nullptr, &GUID_DEVINTERFACE_DISK, index, &interfaceData); ++index) { + DWORD required = 0; + SetupDiGetDeviceInterfaceDetailW(set, &interfaceData, nullptr, 0, &required, nullptr); + if (required < sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W)) continue; + std::vector storage((required + sizeof(DWORD) - 1) / sizeof(DWORD)); + auto* detail = reinterpret_cast(storage.data()); + detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); + SP_DEVINFO_DATA deviceData{}; + deviceData.cbSize = sizeof(deviceData); + if (!SetupDiGetDeviceInterfaceDetailW(set, &interfaceData, detail, required, nullptr, &deviceData)) continue; + const Handle disk{CreateFileW(detail->DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)}; + if (disk.value == INVALID_HANDLE_VALUE) continue; + const auto number = DeviceNumber(disk.value); + if (number && number->DeviceNumber == diskNumber) return deviceData.DevInst; + } + return std::nullopt; +} + +std::optional Identity(DEVINST instance) { + for (DEVINST current = instance;;) { + DEVINST parent = 0; + if (CM_Get_Parent(&parent, current, 0) != CR_SUCCESS) return std::nullopt; + current = parent; + ULONG length = 0; + if (CM_Get_Device_ID_Size(&length, current, 0) != CR_SUCCESS) return std::nullopt; + std::wstring id(length + 1, L'\0'); + if (CM_Get_Device_IDW(current, id.data(), length + 1, 0) != CR_SUCCESS) return std::nullopt; + id.resize(length); + if (id.size() < 8 || _wcsnicmp(id.c_str(), L"USB\\VID_", 8) != 0) continue; + const auto vendorId = TaggedHex(id, L"VID_"); + const auto productId = TaggedHex(id, L"PID_"); + ULONG size = 0; + if (CM_Get_DevNode_Registry_PropertyW(current, CM_DRP_HARDWAREID, nullptr, nullptr, &size, 0) != CR_BUFFER_SMALL || size == 0) return std::nullopt; + std::vector hardwareIds(size / sizeof(wchar_t) + 2, L'\0'); + if (CM_Get_DevNode_Registry_PropertyW(current, CM_DRP_HARDWAREID, nullptr, hardwareIds.data(), &size, 0) != CR_SUCCESS) return std::nullopt; + std::optional bcdDevice; + for (const wchar_t* entry = hardwareIds.data(); *entry != L'\0' && !bcdDevice; entry += std::wcslen(entry) + 1) { + bcdDevice = TaggedHex(entry, L"REV_"); + } + if (!vendorId || !productId || !bcdDevice) return std::nullopt; + return UsbIdentity{*vendorId, *productId, *bcdDevice}; + } +} + +std::optional Describe(wchar_t letter) { + const wchar_t root[] = {letter, L':', L'\\', L'\0'}; + const UINT type = GetDriveTypeW(root); + if (type != DRIVE_REMOVABLE && type != DRIVE_FIXED) return std::nullopt; + wchar_t fileSystemName[MAX_PATH + 1]{}; + DWORD fileSystemFlags = 0; + if (!GetVolumeInformationW(root, nullptr, 0, nullptr, nullptr, &fileSystemFlags, fileSystemName, MAX_PATH + 1)) return std::nullopt; + std::optional fileSystem; + if (_wcsicmp(fileSystemName, L"FAT") == 0 || _wcsicmp(fileSystemName, L"FAT32") == 0) fileSystem = FileSystem::Fat; + if (_wcsicmp(fileSystemName, L"exFAT") == 0) fileSystem = FileSystem::ExFat; + if (!fileSystem) return std::nullopt; + const wchar_t devicePath[] = {L'\\', L'\\', L'.', L'\\', letter, L':', L'\0'}; + const Handle volume{CreateFileW(devicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)}; + if (volume.value == INVALID_HANDLE_VALUE) return std::nullopt; + STORAGE_PROPERTY_QUERY query{}; + query.PropertyId = StorageDeviceProperty; + query.QueryType = PropertyStandardQuery; + std::vector descriptor(1024); + DWORD bytes = 0; + for (int attempt = 0; attempt < 2; ++attempt) { + if (!DeviceIoControl(volume.value, IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(query), descriptor.data(), + static_cast(descriptor.size()), &bytes, nullptr)) { + return std::nullopt; + } + if (bytes < sizeof(STORAGE_DESCRIPTOR_HEADER)) return std::nullopt; + const auto size = reinterpret_cast(descriptor.data())->Size; + if (size <= descriptor.size()) break; + descriptor.resize(size); + } + if (bytes < sizeof(STORAGE_DEVICE_DESCRIPTOR)) return std::nullopt; + STORAGE_DEVICE_DESCRIPTOR header{}; + std::memcpy(&header, descriptor.data(), sizeof(header)); + if (header.BusType != BusTypeUsb) return std::nullopt; + const auto number = DeviceNumber(volume.value); + if (!number) return std::nullopt; + const auto instance = DiskInstance(number->DeviceNumber); + if (!instance) return std::nullopt; + const auto identity = Identity(*instance); + if (!identity) return std::nullopt; + ULARGE_INTEGER total{}; + if (!GetDiskFreeSpaceExW(root, nullptr, &total, nullptr)) return std::nullopt; + wchar_t volumeName[MAX_PATH]{}; + if (!GetVolumeNameForVolumeMountPointW(root, volumeName, MAX_PATH)) return std::nullopt; + HostVolume result; + for (const wchar_t* character = volumeName; *character != L'\0'; ++character) result.key += static_cast(*character & 0x7f); + result.root = std::filesystem::path(root); + result.volumeSize = total.QuadPart; + result.vendorId = identity->vendorId; + result.productId = identity->productId; + result.bcdDevice = identity->bcdDevice; + result.vendor = DescriptorString(descriptor, bytes, header.VendorIdOffset); + result.product = DescriptorString(descriptor, bytes, header.ProductIdOffset); + result.serialNumber = DescriptorString(descriptor, bytes, header.SerialNumberOffset); + result.readOnly = (fileSystemFlags & FILE_READ_ONLY_VOLUME) != 0; + result.fileSystem = *fileSystem; + return result; +} + +} + +std::vector ScanWindowsVolumes() { + const ErrorMode errorMode; + std::vector volumes; + const DWORD drives = GetLogicalDrives(); + for (int letter = 0; letter < 26; ++letter) { + if ((drives & (DWORD{1} << letter)) == 0) continue; + if (auto volume = Describe(static_cast(L'A' + letter))) volumes.push_back(std::move(*volume)); + } + return volumes; +} + +} diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index d15ec5c2fa..8eab884339 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceVideodec2/include/Api.hpp" +#include "prx/libSceVideodec2/include/PictureMetadata.hpp" #include "prx/libSceVdecsw/include/Api.hpp" #include #include @@ -33,34 +34,6 @@ static_assert(sizeof(OutputInfo) == 0x38 && offsetof(OutputInfo, isLastFrame) == static_assert(sizeof(AvcPictureInfo) == 0x78 && offsetof(AvcPictureInfo, picWidthInMbsMinus1) == 0x2c && offsetof(AvcPictureInfo, frameCropBottomOffset) == 0x44); static_assert(sizeof(HevcPictureInfo) == 0xb8 && offsetof(HevcPictureInfo, picWidthInLumaSamples) == 0x28 && offsetof(HevcPictureInfo, sarWidth) == 0x42 && offsetof(HevcPictureInfo, picStruct) == 0x50 && offsetof(HevcPictureInfo, conformanceWindowFlag) == 0x5c && offsetof(HevcPictureInfo, idrPictureFlag) == 0x93 && offsetof(HevcPictureInfo, generalProfileIdc) == 0x97 && offsetof(HevcPictureInfo, generalLevelIdc) == 0x9b && offsetof(HevcPictureInfo, frameCroppingFlag) == 0xa5 && offsetof(HevcPictureInfo, frameCropBottomOffset) == 0xb4); -struct Videodec2Crop { - std::uint32_t left; - std::uint32_t right; - std::uint32_t top; - std::uint32_t bottom; -}; - -struct Videodec2PictureHeader { - std::uint64_t pts; - std::uint64_t dts; - std::uint64_t attached; - bool keyFrame; - bool hevcIdr; - std::uint32_t profile; - std::uint32_t level; - Videodec2Crop crop; - std::uint8_t bitDepthMinus8; -}; - -} - -extern "C" { - -void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Videodec2PictureHeader* header); -} - -namespace { - struct PendingInput { SceVideodec2::InputData input; std::vector data; @@ -103,7 +76,7 @@ std::shared_ptr findDecoder(std::uint64_t handle) { } PictureInfo pictureInfo(std::uint64_t handle, const SceVideodec2::OutputInfo& output) { - Videodec2PictureHeader header{}; + SceVideodec2::Internal::PictureHeader header{}; Videodec2PictureHeader_nid_no_patch(handle, output.frameBuffer, &header); const auto& crop = header.crop; const bool cropped = crop.left != 0 || crop.right != 0 || crop.top != 0 || crop.bottom != 0; @@ -157,11 +130,9 @@ PictureInfo pictureInfo(std::uint64_t handle, const SceVideodec2::OutputInfo& ou return info; } -SceVideodec2::Vp9PictureInfo vp9PictureInfo(const SceVideodec2::OutputInfo& output) { +SceVideodec2::Vp9PictureInfo vp9PictureInfo(std::uint64_t handle, const SceVideodec2::OutputInfo& output) { SceVideodec2::Vp9PictureInfo info{sizeof(SceVideodec2::Vp9PictureInfo)}; - sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &info); - - if (!info.isValid) throw std::runtime_error("Vdecsw: decoded picture has no VP9 picture info"); + Videodec2Vp9PictureInfo_nid_no_patch(handle, output.frameBuffer, &info); return info; } @@ -331,7 +302,7 @@ int APS5_VABI sceVdecswTrySyncDecodeOutput(std::uint64_t handle, OutputInfo* out output->frameBufferSize = picture.output.frameBufferSize; output->frameFormat = picture.output.frameFormat; output->framePitchInBytes = picture.output.framePitchInBytes; - if (picture.output.codecType == CodecVp9) decoder->deliveredVp9[target.frameBuffer] = vp9PictureInfo(picture.output); + if (picture.output.codecType == CodecVp9) decoder->deliveredVp9[target.frameBuffer] = vp9PictureInfo(handle, picture.output); else decoder->delivered[target.frameBuffer] = picture.info; return 0; } diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 1ded17a58c..86779908ff 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -131,6 +131,9 @@ if(BUILD_TESTING AND NOT WIN32) add_test(NAME video_out_frame_dump_wait COMMAND video_out_frame_dump_queue_tests wait) set_tests_properties(video_out_frame_dump_wait PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_DUMP_FRAMES=4;APS5_DUMP_SCALE=1") + add_test(NAME video_out_frame_dump_cpu_queue COMMAND video_out_frame_dump_queue_tests) + set_tests_properties(video_out_frame_dump_cpu_queue PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 + ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_DUMP_FRAMES=4;APS5_DUMP_SCALE=4;APS5_NO_GPU_DUMP=1") endif() if(BUILD_TESTING) diff --git a/core/libs/prx/libSceVideoOut/include/MainThread.hpp b/core/libs/prx/libSceVideoOut/include/MainThread.hpp new file mode 100644 index 0000000000..7250dd4394 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/MainThread.hpp @@ -0,0 +1,38 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_MAINTHREAD_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_INCLUDE_MAINTHREAD_HPP + +#include +#include +#include +#include + +#ifdef __APPLE__ +extern "C" void Aps5RunOnMainThread_nid_no_patch(void (*function)(void*), void* context); +#endif + +template +std::invoke_result_t OnMainThread(Function&& function) { +#ifdef __APPLE__ + using Result = std::invoke_result_t; + struct Call { + std::remove_reference_t* function; + std::exception_ptr error; + std::conditional_t, bool, std::optional> result {}; + } call {&function, nullptr}; + Aps5RunOnMainThread_nid_no_patch([](void* context) { + auto& call = *static_cast(context); + try { + if constexpr (std::is_void_v) (*call.function)(); + else call.result.emplace((*call.function)()); + } catch (...) { + call.error = std::current_exception(); + } + }, &call); + if (call.error) std::rethrow_exception(call.error); + if constexpr (!std::is_void_v) return std::move(*call.result); +#else + return function(); +#endif +} + +#endif diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index cb8afb2826..1bf7e49f71 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -13,6 +13,7 @@ #include "SDL.h" #include "SDL_vulkan.h" +#include "prx/libSceVideoOut/include/MainThread.hpp" #include "prx/libSceVideoOut/include/PadInput.hpp" #include "prx/libSceVideoOut/include/MouseInput.hpp" #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" @@ -64,31 +65,37 @@ std::optional configuredResolution() { } std::vector windowExtensions(SDL_Window* window) { - unsigned extensionCount = 0; - if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); - std::vector extensions(extensionCount); - if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); - extensions.resize(extensionCount); - return extensions; + return OnMainThread([&] { + unsigned extensionCount = 0; + if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); + std::vector extensions(extensionCount); + if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError()); + extensions.resize(extensionCount); + return extensions; + }); } VkSurfaceKHR createWindowSurface(void* context, VkInstance instance) { - VkSurfaceKHR surface = VK_NULL_HANDLE; - if (!SDL_Vulkan_CreateSurface(static_cast(context), instance, &surface)) throw std::runtime_error(std::string("SDL_Vulkan_CreateSurface failed: ") + SDL_GetError()); - return surface; + return OnMainThread([&] { + VkSurfaceKHR surface = VK_NULL_HANDLE; + if (!SDL_Vulkan_CreateSurface(static_cast(context), instance, &surface)) throw std::runtime_error(std::string("SDL_Vulkan_CreateSurface failed: ") + SDL_GetError()); + return surface; + }); } void windowDrawableSize(void* context, std::uint32_t* width, std::uint32_t* height) { - if ((SDL_GetWindowFlags(static_cast(context)) & SDL_WINDOW_MINIMIZED) != 0) { - *width = 0; - *height = 0; - return; - } - int drawableWidth = 0; - int drawableHeight = 0; - SDL_Vulkan_GetDrawableSize(static_cast(context), &drawableWidth, &drawableHeight); - *width = drawableWidth > 0 ? static_cast(drawableWidth) : 0; - *height = drawableHeight > 0 ? static_cast(drawableHeight) : 0; + OnMainThread([&] { + if ((SDL_GetWindowFlags(static_cast(context)) & SDL_WINDOW_MINIMIZED) != 0) { + *width = 0; + *height = 0; + return; + } + int drawableWidth = 0; + int drawableHeight = 0; + SDL_Vulkan_GetDrawableSize(static_cast(context), &drawableWidth, &drawableHeight); + *width = drawableWidth > 0 ? static_cast(drawableWidth) : 0; + *height = drawableHeight > 0 ? static_cast(drawableHeight) : 0; + }); } void checkConfig(const VideoOutConfig& cfg) { @@ -291,17 +298,19 @@ VideoOutDriver& VideoOutDriver::Get() { } VideoOutDriver::VideoOutDriver() { - SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); + OnMainThread([] { + SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); #ifdef _WIN32 - SDL_SetHintWithPriority(SDL_HINT_DIRECTINPUT_ENABLED, "0", SDL_HINT_DEFAULT); - if (std::getenv("APS5_NO_WGI") != nullptr) SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_WGI, "0", SDL_HINT_DEFAULT); + SDL_SetHintWithPriority(SDL_HINT_DIRECTINPUT_ENABLED, "0", SDL_HINT_DEFAULT); + if (std::getenv("APS5_NO_WGI") != nullptr) SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_WGI, "0", SDL_HINT_DEFAULT); #endif #ifdef __linux__ - SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); + SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); #endif - if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { - throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError()); - } + if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { + throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError()); + } + }); try { AgcDriverWaitIdle_nid_postfix(); std::promise started; @@ -329,7 +338,7 @@ VideoOutDriver::VideoOutDriver() { flipQueue->changed.NotifyAll(); presentThread.join(); } - SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); + OnMainThread([] { SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); }); throw; } } @@ -358,7 +367,7 @@ void VideoOutDriver::Shutdown() { if (outputs[handle]) close(handle); } } - SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); + OnMainThread([] { SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); }); stopped = true; std::lock_guard lock(flipQueue->mutex); if (flipQueue->failure) std::rethrow_exception(flipQueue->failure); @@ -598,7 +607,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { req.gpuComplete = true; } } else { - window.Ensure(req.width, req.height); + OnMainThread([&] { window.Ensure(req.width, req.height); }); const auto extensions = windowExtensions(window.Handle()); const AgcDriver::PresentationWindow target{window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, req.width, req.height, req.timing}; timing.Mark("window_prepare"); @@ -619,7 +628,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { AgcDriverPresentClear_nid_postfix(target, req.unregistered || req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, plane ? &*plane : nullptr, gpuReady, &req); } timing.Mark("present"); - window.UpdateTitle(); + OnMainThread([&] { window.UpdateTitle(); }); timing.Mark("window_title"); } std::lock_guard lock(req.cfg->mutex); @@ -673,20 +682,26 @@ std::optional VideoOutDriver::overlayPlane() { void VideoOutDriver::presentLoop(std::stop_token token, std::promise& started) { try { - window.Ensure(VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT); - updateDisplayRefreshRate(); + OnMainThread([&] { + window.Ensure(VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT); + updateDisplayRefreshRate(); + }); hdrProbe.Poll(window.Handle()); const auto extensions = windowExtensions(window.Handle()); AgcDriverAttachWindow_nid_postfix({window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT, nullptr}); hdrGeneration = AgcDriver::CurrentHostHdr().generation; } catch (...) { hdrProbe.Release(); - window.Destroy(); + OnMainThread([&] { window.Destroy(); }); started.set_exception(std::current_exception()); return; } started.set_value(); std::shared_ptr current; + std::optional padInput; + std::optional mouseInput; + std::optional keyboardInput; + std::optional imeTextInput; #if APS5_ENABLE_TIMING_LOG std::unique_ptr timingLog; #endif @@ -694,10 +709,12 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star #if APS5_ENABLE_TIMING_LOG timingLog = std::make_unique(); #endif - PadInput padInput; - MouseInput mouseInput; - KeyboardInput keyboardInput; - ImeTextInput imeTextInput; + OnMainThread([&] { + padInput.emplace(); + mouseInput.emplace(); + keyboardInput.emplace(); + imeTextInput.emplace(); + }); while (!token.stop_requested()) { { std::unique_lock lock(flipQueue->mutex); @@ -710,23 +727,27 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star } } - SDL_Event event; - while (SDL_PollEvent(&event)) { - if (event.type == SDL_QUIT) { - LibcRequestExit_nid_postfix(0); - throw ProcessShutdown{}; - } - padInput.HandleEvent(event, window); - if (window.Handle() != nullptr) { - mouseInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); - keyboardInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); - imeTextInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); + const bool quit = OnMainThread([&] { + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) return true; + padInput->HandleEvent(event, window); + if (window.Handle() != nullptr) { + mouseInput->HandleEvent(event, SDL_GetWindowID(window.Handle())); + keyboardInput->HandleEvent(event, SDL_GetWindowID(window.Handle())); + imeTextInput->HandleEvent(event, SDL_GetWindowID(window.Handle())); + } } + padInput->Update(window); + updateDisplayRefreshRate(); + return false; + }); + if (quit) { + LibcRequestExit_nid_postfix(0); + throw ProcessShutdown{}; } - padInput.Update(window); - updateDisplayRefreshRate(); pollHostHdr(); - mouseInput.UpdateCapture(!padInput.MouseModeEnabled()); + OnMainThread([&] { mouseInput->UpdateCapture(!padInput->MouseModeEnabled()); }); if (current) { require(current->timing != nullptr, "missing presentation timing"); const auto dequeued = AgcDriver::FrameTiming::Clock::now(); @@ -734,10 +755,10 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star processFlip(*current); int windowWidth = 0; int windowHeight = 0; - if (window.Handle() != nullptr) SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); + if (window.Handle() != nullptr) OnMainThread([&] { SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); }); if (windowWidth > 0 && windowHeight > 0) { const auto presented = AgcDriver::ComputeContainSize_nid_postfix(current->width, current->height, static_cast(windowWidth), static_cast(windowHeight), true); - mouseInput.SetPresentation(current->width, current->height, presented.width, presented.height); + mouseInput->SetPresentation(current->width, current->height, presented.width, presented.height); } const auto finished = AgcDriver::FrameTiming::Clock::now(); AgcDriver::FrameTiming::Clock::duration interval{}; @@ -790,7 +811,13 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star if (!flipQueue->failure) flipQueue->failure = std::current_exception(); } hdrProbe.Release(); - window.Destroy(); + OnMainThread([&] { + padInput.reset(); + mouseInput.reset(); + keyboardInput.reset(); + imeTextInput.reset(); + window.Destroy(); + }); } void VideoOutDriver::vblankLoop(std::stop_token token) { diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index f548693fed..ca0ae05f8a 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -1160,7 +1160,11 @@ void testOutputResolutionOverride() { } -int main(int argc, char** argv) { +#ifdef __APPLE__ +extern "C" [[noreturn]] void Aps5StartGuest_nid_no_patch(void (*entry)(void*, void*), void* block); +#endif + +int runTests(int argc, char** argv) { VideoOutTestEnvironment environment; try { if (argc == 2 && std::string(argv[1]) == "decode") testDecode(); @@ -1202,3 +1206,15 @@ int main(int argc, char** argv) { return 1; } } + +int main(int argc, char** argv) { +#ifdef __APPLE__ + static int arguments; + static char** values; + arguments = argc; + values = argv; + Aps5StartGuest_nid_no_patch([](void*, void*) { std::exit(runTests(arguments, values)); }, nullptr); +#else + return runTests(argc, argv); +#endif +} diff --git a/core/libs/prx/libSceVideodec/Export.cpp b/core/libs/prx/libSceVideodec/Export.cpp index efb762dbfe..f5110f0274 100644 --- a/core/libs/prx/libSceVideodec/Export.cpp +++ b/core/libs/prx/libSceVideodec/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceVideodec2/include/Api.hpp" +#include "prx/libSceVideodec2/include/PictureMetadata.hpp" #include "prx/libSceVideodec/include/Api.hpp" #include #include @@ -28,12 +29,7 @@ constexpr int VideodecErrorDecodeAu = static_cast(0x80C10011); constexpr std::uint32_t CodecAvc = 1; constexpr std::uint32_t PitchAlignment = 64; -struct Decoder2Crop { - std::uint32_t left; - std::uint32_t right; - std::uint32_t top; - std::uint32_t bottom; -}; +using Decoder2Crop = SceVideodec2::Internal::Crop; static_assert(sizeof(ConfigInfo) == 40 && sizeof(ResourceInfo) == 56 && sizeof(Ctrl) == 24); static_assert(offsetof(ConfigInfo, maxFrameWidth) == 20 && offsetof(ConfigInfo, videodecFlags) == 32); @@ -62,15 +58,6 @@ std::uint32_t AlignUp(std::uint32_t value, std::uint32_t alignment) { return (value + alignment - 1) / alignment * alignment; } -} - -extern "C" { - -void Videodec2PictureCrop_nid_no_patch(std::uint64_t, const void*, Decoder2Crop*); -} - -namespace { - struct Geometry { std::uint32_t width = 0; std::uint32_t height = 0; @@ -199,14 +186,12 @@ int APS5_VABI sceVideodecDecode(Ctrl* ctrl, const SceVideodec2::InputData* input } catch (const std::exception&) { return VideodecErrorDecodeAu; } - Decoder2Crop crop{}; - if (output.isValid) Videodec2PictureCrop_nid_no_patch(decoder->handle, output.frameBuffer, &crop); - const auto geometry = Measure(output, crop); - SceVideodec2::AvcPictureInfo avc{sizeof(SceVideodec2::AvcPictureInfo)}; - if (output.isValid) sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr); + SceVideodec2::Internal::PictureHeader header{}; + if (output.isValid) Videodec2PictureHeader_nid_no_patch(decoder->handle, output.frameBuffer, &header); + const auto geometry = Measure(output, header.crop); FillPictureInfo(geometry, output, picture); - picture->ptsData = avc.ptsData; - picture->attachedData = avc.attachedData; + picture->ptsData = header.pts; + picture->attachedData = header.attached; if (!output.isValid) return 0; return CopyPicture(output, geometry, frameBuffer); } @@ -253,14 +238,12 @@ int APS5_VABI sceVideodecFlush(Ctrl* ctrl, FrameBuffer* frameBuffer, PictureInfo } catch (const std::exception&) { return VideodecErrorDecodeAu; } - Decoder2Crop crop{}; - if (output.isValid) Videodec2PictureCrop_nid_no_patch(decoder->handle, output.frameBuffer, &crop); - const auto geometry = Measure(output, crop); - SceVideodec2::AvcPictureInfo avc{sizeof(SceVideodec2::AvcPictureInfo)}; - if (output.isValid) sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr); + SceVideodec2::Internal::PictureHeader header{}; + if (output.isValid) Videodec2PictureHeader_nid_no_patch(decoder->handle, output.frameBuffer, &header); + const auto geometry = Measure(output, header.crop); FillPictureInfo(geometry, output, picture); - picture->ptsData = avc.ptsData; - picture->attachedData = avc.attachedData; + picture->ptsData = header.pts; + picture->attachedData = header.attached; if (!output.isValid) return 0; return CopyPicture(output, geometry, frameBuffer); } diff --git a/core/libs/prx/libSceVideodec2/CMakeLists.txt b/core/libs/prx/libSceVideodec2/CMakeLists.txt index 8bc94af3d1..244b8050b3 100644 --- a/core/libs/prx/libSceVideodec2/CMakeLists.txt +++ b/core/libs/prx/libSceVideodec2/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(libSceVideodec2 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/HevcMetadata.cpp ) target_include_directories(libSceVideodec2 PRIVATE ${LIBS_INCLUDE_DIR}) @@ -9,6 +10,17 @@ configure_windows_unwind(libSceVideodec2) target_link_libraries(libSceVideodec2 PRIVATE ffmpeg) if(BUILD_TESTING) + add_test_executable(guest_hevc_metadata_tests ${LIBS_INCLUDE_DIR}/tests/GuestHevcMetadata.cpp) + target_include_directories(guest_hevc_metadata_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_hevc_metadata_tests PRIVATE libSceVideodec2 libc) + configure_windows_unwind(guest_hevc_metadata_tests) + add_test(NAME guest_hevc_metadata COMMAND guest_hevc_metadata_tests) + set_tests_properties(guest_hevc_metadata PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_hevc_metadata PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_videodec2_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideodec2.cpp) target_include_directories(guest_videodec2_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_videodec2_tests PRIVATE libSceVideodec2 libc) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index df09695444..3ffb7a3ca4 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -1,4 +1,5 @@ #include "prx/libSceVideodec2/include/Api.hpp" +#include "prx/libSceVideodec2/include/PictureMetadata.hpp" #include #include #include @@ -10,11 +11,14 @@ #include #include #include +#include #include #include #include #include #include "prx/libc/include/General.hpp" +#include "HevcMetadata.hpp" +#include "prx/libc/include/GuestArena.hpp" extern "C" { #include @@ -26,6 +30,10 @@ extern "C" { namespace { using namespace SceVideodec2; +using SceVideodec2::Internal::Crop; +using SceVideodec2::Internal::PictureHeader; + +struct UnsupportedHevcMetadata {}; constexpr std::uint64_t WorkMemoryBytes = 1u << 20u; constexpr std::uint32_t PitchAlignment = 256; @@ -35,6 +43,12 @@ constexpr std::uint32_t CodecHevc = 974921; constexpr std::uint32_t CodecVp9 = 2382845; constexpr int Videodec2ErrorAccessUnitSize = static_cast(0x811D010Du); constexpr int Videodec2ErrorAccessUnitPointer = static_cast(0x811D010Eu); +constexpr int Videodec2ErrorStructureSize = static_cast(0x811D0101u); +constexpr int Videodec2ErrorArgumentPointer = static_cast(0x811D0102u); +constexpr int Videodec2ErrorFrameBufferSize = static_cast(0x811D0106u); +constexpr int Videodec2ErrorFrameBufferPointer = static_cast(0x811D0107u); +constexpr int Videodec2ErrorFrameBufferAlignment = static_cast(0x811D0108u); +constexpr int Videodec2ErrorPictureCount = static_cast(0x811D010Fu); static_assert(sizeof(Vp9PictureInfo) == 88); static_assert(offsetof(Vp9PictureInfo, colorSpace) == 60); @@ -172,13 +186,6 @@ std::vector ParseVp9Packet(std::span bytes, std::ar return result; } -struct Crop { - std::uint32_t left = 0; - std::uint32_t right = 0; - std::uint32_t top = 0; - std::uint32_t bottom = 0; -}; - struct Picture { std::uint64_t pts = 0; std::uint64_t dts = 0; @@ -188,18 +195,7 @@ struct Picture { Crop crop{}; bool hevcIdr = false; std::uint8_t bitDepthMinus8 = 0; -}; - -struct PictureHeader { - std::uint64_t pts; - std::uint64_t dts; - std::uint64_t attached; - bool keyFrame; - bool hevcIdr; - std::uint32_t profile; - std::uint32_t level; - Crop crop; - std::uint8_t bitDepthMinus8; + Videodec2Hevc::Metadata hevc; }; struct FrameDeleter { @@ -229,6 +225,7 @@ struct Decoder { bool flushing = false; std::array vp9References{}; Vp9PictureInfo vp9Colors{}; + Videodec2Hevc::State hevc; ~Decoder() { sws_freeContext(scaler); @@ -547,6 +544,7 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa const Picture picture{input->ptsData, input->dtsData, input->attachedData}; if (decoder.flushing) { avcodec_flush_buffers(decoder.context); + decoder.hevc.afterEnd = true; decoder.flushing = false; } if (input->auSize > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); @@ -595,6 +593,12 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa packet->pts = decoder.nextKey; auto tagged = picture; tagged.hevcIdr = decoder.codec == CodecHevc && StartsHevcIdr(*unit); + if (decoder.codec == CodecHevc) { + tagged.hevc = Videodec2Hevc::Parse_nid_no_patch(decoder.hevc, *unit); + tagged.hevc.info.ptsData = picture.pts; + tagged.hevc.info.dtsData = picture.dts; + tagged.hevc.info.attachedData = picture.attached; + } decoder.inputs.emplace(decoder.nextKey++, tagged); Send(decoder, packet); EmitDecoded(handle, decoder, frame, output); @@ -628,6 +632,7 @@ int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle) { std::erase_if(pictureOwners, [handle](const auto& owner) { return owner.second == handle; }); } avcodec_flush_buffers(decoder.context); + decoder.hevc.afterEnd = true; decoder.ready.clear(); decoder.inputs.clear(); decoder.parameterSets.clear(); @@ -635,9 +640,50 @@ int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle) { return 0; } -int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, AvcPictureInfo* first, void* second) { - (void)second; - if (!output || !first) throw std::runtime_error("Videodec2: invalid argument pointer"); +static std::uint64_t PictureInfoSize(const void* info) { + std::uint64_t size = 0; + std::memcpy(&size, info, sizeof(size)); + return size; +} + +static bool PictureInfoSizeSupported(const void* info) { + switch (PictureInfoSize(info)) { + case 0x58: + case 0x68: + case 0x78: + case 0x88: + case 0xa8: + case 0xb8: return true; + default: return false; + } +} + +static int PictureArguments(const OutputInfo* output, void* first, void* second) { + if (!first) return Videodec2ErrorArgumentPointer; + auto* firstValidity = static_cast(first) + 8; + GuestArena::HostWrite firstAccess(firstValidity, sizeof(std::uint32_t)); + if (!firstAccess.Open()) throw UnsupportedHevcMetadata{}; + std::optional secondAccess; + if (second) { + secondAccess.emplace(static_cast(second) + 8, sizeof(std::uint32_t)); + if (!secondAccess->Open()) throw UnsupportedHevcMetadata{}; + } + std::memset(firstValidity, 0, sizeof(std::uint32_t)); + if (second) std::memset(static_cast(second) + 8, 0, sizeof(std::uint32_t)); + if (!output) return Videodec2ErrorArgumentPointer; + if (output->thisSize != 0x30 && output->thisSize != 0x38) return Videodec2ErrorStructureSize; + if ((reinterpret_cast(output->frameBuffer) & 0xffu) != 0) return Videodec2ErrorFrameBufferAlignment; + if (!output->frameBuffer) return Videodec2ErrorFrameBufferPointer; + if (output->pictureCount != 1 && output->pictureCount != 2) return Videodec2ErrorPictureCount; + if (output->frameBufferSize <= std::uint64_t{output->pictureCount} * 0x400u) return Videodec2ErrorFrameBufferSize; + if (!PictureInfoSizeSupported(first) || (output->pictureCount == 2 && second && !PictureInfoSizeSupported(second))) return Videodec2ErrorStructureSize; + return 0; +} + +static int GetPictureInfo(const OutputInfo* output, void* first, void* second) { + const int result = PictureArguments(output, first, second); + if (result != 0) return result; + if (output->pictureCount != 1) throw UnsupportedHevcMetadata{}; const auto owner = [output]() -> std::uint64_t { std::lock_guard guard(lock); const auto found = pictureOwners.find(output->frameBuffer); @@ -649,62 +695,46 @@ int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, A std::lock_guard guard(decoder->mutex); if (owner() != handle) continue; const auto found = decoder->pictures.find(output->frameBuffer); - if (found == decoder->pictures.end()) throw std::runtime_error("Videodec2: owned picture is missing"); - if (decoder->codec == CodecVp9) { - auto* info = reinterpret_cast(first); - if (!Fits(info, sizeof(Vp9PictureInfo))) throw std::runtime_error("Videodec2: VP9 picture structure is too small"); - const auto size = info->thisSize; - *info = found->second.vp9; - info->thisSize = size; + if (found == decoder->pictures.end()) throw UnsupportedHevcMetadata{}; + const auto size = PictureInfoSize(first); + if (decoder->codec == CodecVp9 && size == sizeof(Vp9PictureInfo)) { + GuestArena::HostWrite access(first, sizeof(Vp9PictureInfo)); + if (!access.Open()) throw UnsupportedHevcMetadata{}; + std::memcpy(first, &found->second.vp9, sizeof(Vp9PictureInfo)); + std::memcpy(first, &size, sizeof(size)); return 0; } - if (decoder->codec != CodecAvc) throw std::runtime_error("Videodec2: picture info is only implemented for H.264 and VP9"); - const auto& picture = found->second; - first->isValid = true; - first->ptsData = picture.pts; - first->dtsData = picture.dts; - first->attachedData = picture.attached; - if (Fits(first, offsetof(AvcPictureInfo, picHeightInLumaSamples) + 4)) { - first->idrPictureFlag = picture.idr ? 1 : 0; - first->profileIdc = static_cast(decoder->profile); - first->levelIdc = static_cast(decoder->level); - first->picWidthInLumaSamples = output->frameWidth; - first->picHeightInLumaSamples = output->frameHeight; + if (decoder->codec == CodecHevc && size == sizeof(Videodec2Hevc::PictureInfo)) { + if (!found->second.hevc.unsupported.empty()) throw UnsupportedHevcMetadata{}; + GuestArena::HostWrite access(first, sizeof(Videodec2Hevc::PictureInfo)); + if (!access.Open()) throw UnsupportedHevcMetadata{}; + std::memcpy(first, &found->second.hevc.info, sizeof(Videodec2Hevc::PictureInfo)); + std::memcpy(first, &size, sizeof(size)); + return 0; } - return 0; + throw UnsupportedHevcMetadata{}; } - first->isValid = false; - return 0; + throw UnsupportedHevcMetadata{}; +} + +int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, AvcPictureInfo* first, void* second) { + try { return GetPictureInfo(output, first, second); } + catch (const UnsupportedHevcMetadata&) { NotImplemented_nid_no_patch(__func__); } } int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const OutputInfo* output, AvcPictureInfo* first, AvcPictureInfo* second) { - return sceVideodec2GetPictureInfo_nid_postfix(output, first, second); + try { return GetPictureInfo(output, first, second); } + catch (const UnsupportedHevcMetadata&) { NotImplemented_nid_no_patch(__func__); } +} + +int APS5_VABI sceVideodec2GetHevcPictureInfo_nid_postfix(const OutputInfo* output, Videodec2Hevc::PictureInfo* info) { + try { return GetPictureInfo(output, info, nullptr); } + catch (const UnsupportedHevcMetadata&) { NotImplemented_nid_no_patch(__func__); } } int APS5_VABI sceVideodec2GetVp9PictureInfo_nid_postfix(const OutputInfo* output, Vp9PictureInfo* info) { - if (!output || !info) throw std::runtime_error("Videodec2: invalid VP9 picture argument"); - if (!Fits(output, offsetof(OutputInfo, frameBuffer) + sizeof(output->frameBuffer)) || !Fits(info, sizeof(Vp9PictureInfo))) - throw std::runtime_error("Videodec2: VP9 picture structure is too small"); - const auto owner = [output]() -> std::uint64_t { - std::lock_guard guard(lock); - const auto found = pictureOwners.find(output->frameBuffer); - return found != pictureOwners.end() ? found->second : 0; - }; - for (auto handle = owner(); handle != 0; handle = owner()) { - const auto decoder = FindDecoder(handle); - if (!decoder) continue; - std::lock_guard guard(decoder->mutex); - if (owner() != handle) continue; - const auto found = decoder->pictures.find(output->frameBuffer); - if (found == decoder->pictures.end()) throw std::runtime_error("Videodec2: owned VP9 picture is missing"); - if (decoder->codec != CodecVp9) throw std::runtime_error("Videodec2: VP9 info requested for another codec"); - const auto size = info->thisSize; - *info = found->second.vp9; - info->thisSize = size; - return 0; - } - info->isValid = false; - return 0; + try { return GetPictureInfo(output, info, nullptr); } + catch (const UnsupportedHevcMetadata&) { NotImplemented_nid_no_patch(__func__); } } void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, PictureHeader* header) { @@ -719,11 +749,18 @@ void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frame *header = {stored.pts, stored.dts, stored.attached, stored.idr, stored.hevcIdr, decoder.profile, decoder.level, stored.crop, stored.bitDepthMinus8}; } -void Videodec2PictureCrop_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Crop* crop) { - if (!crop) throw std::runtime_error("Videodec2: invalid argument pointer"); - PictureHeader header{}; - Videodec2PictureHeader_nid_no_patch(handle, frameBuffer, &header); - *crop = header.crop; +void Videodec2Vp9PictureInfo_nid_no_patch(std::uint64_t handle, const void* frameBuffer, Vp9PictureInfo* info) { + if (!info) throw std::runtime_error("Videodec2: invalid argument pointer"); + const auto found = FindDecoder(handle); + if (!found) throw std::runtime_error("Videodec2: invalid decoder handle"); + auto& decoder = *found; + std::lock_guard guard(decoder.mutex); + if (decoder.codec != CodecVp9) throw std::runtime_error("Videodec2: VP9 picture info requested for another codec"); + const auto picture = decoder.pictures.find(frameBuffer); + if (picture == decoder.pictures.end()) throw std::runtime_error("Videodec2: unknown frame buffer"); + auto snapshot = picture->second.vp9; + snapshot.thisSize = sizeof(*info); + *info = snapshot; } } diff --git a/core/libs/prx/libSceVideodec2/HevcMetadata.cpp b/core/libs/prx/libSceVideodec2/HevcMetadata.cpp new file mode 100644 index 0000000000..b005af7cf4 --- /dev/null +++ b/core/libs/prx/libSceVideodec2/HevcMetadata.cpp @@ -0,0 +1,505 @@ +#include "HevcMetadata.hpp" +#include +#include +#include +#include +#include + +namespace { + +class Bits { +public: + explicit Bits(std::span bytes) : bytes(bytes) {} + + std::uint32_t Read(unsigned count) { + if (count > 32 || count > bytes.size() * 8 - position) throw std::runtime_error("truncated HEVC syntax"); + std::uint32_t value = 0; + for (unsigned bit = 0; bit < count; ++bit, ++position) + value = (value << 1) | ((bytes[position / 8] >> (7 - position % 8)) & 1); + return value; + } + + void Skip(std::size_t count) { + if (count > bytes.size() * 8 - position) throw std::runtime_error("truncated HEVC syntax"); + position += count; + } + + std::uint32_t Unsigned(std::uint32_t maximum = std::numeric_limits::max()) { + unsigned zeros = 0; + while (Read(1) == 0) if (++zeros > 31) throw std::runtime_error("HEVC exponential code overflow"); + const auto value = ((std::uint32_t{1} << zeros) - 1) + Read(zeros); + if (value > maximum) throw std::runtime_error("HEVC syntax value outside its domain"); + return value; + } + + void Signed() { Unsigned(); } + +private: + std::span bytes; + std::size_t position = 0; +}; + +std::vector Rbsp(std::span nal) { + std::vector bytes; + bytes.reserve(nal.size()); + unsigned zeros = 0; + for (std::size_t index = 0; index < nal.size(); ++index) { + const auto value = nal[index]; + if (zeros == 2 && value == 3) { + if (index + 1 < nal.size() && nal[index + 1] > 3) throw std::runtime_error("invalid HEVC emulation prevention"); + zeros = 0; + continue; + } + if (zeros == 2 && value < 3) throw std::runtime_error("missing HEVC emulation prevention"); + bytes.push_back(value); + zeros = value == 0 ? std::min(zeros + 1, 2u) : 0; + } + return bytes; +} + +std::uint32_t Profile(Bits& bits, unsigned sublayers, bool* mixed = nullptr, Videodec2Hevc::PictureInfo* info = nullptr) { + const auto space = bits.Read(2); + const auto tier = bits.Read(1); + const auto profileId = bits.Read(5); + bits.Skip(32); + const bool progressive = bits.Read(1) != 0; + const bool interlaced = bits.Read(1) != 0; + if (mixed) *mixed = progressive && interlaced; + bits.Skip(1); + const auto frameOnly = bits.Read(1); + bits.Skip(44); + const auto levelId = bits.Read(8); + if (info) { + info->profileSpace = static_cast(space); + info->tier = static_cast(tier); + info->profile = static_cast(profileId); + info->progressiveSource = progressive; + info->interlacedSource = interlaced; + info->frameOnlyConstraint = static_cast(frameOnly); + info->level = static_cast(levelId); + } + std::array profile{}; + std::array level{}; + for (unsigned layer = 0; layer < sublayers; ++layer) { + profile[layer] = bits.Read(1) != 0; + level[layer] = bits.Read(1) != 0; + } + if (sublayers != 0) bits.Skip(2 * (8 - sublayers)); + for (unsigned layer = 0; layer < sublayers; ++layer) { + if (profile[layer]) bits.Skip(88); + if (level[layer]) bits.Skip(8); + } + return progressive == interlaced ? 2 : progressive ? 1 : 0; +} + +void Ordering(Bits& bits, unsigned sublayers, Videodec2Hevc::PictureInfo* info = nullptr) { + const auto present = bits.Read(1); + const unsigned begin = present != 0 ? 0 : sublayers; + unsigned buffering = 0; + for (unsigned layer = begin; layer <= sublayers; ++layer) { + buffering = std::max(buffering, bits.Unsigned(15)); + bits.Unsigned(); + bits.Unsigned(); + } + if (info) { + info->subLayerOrderingPresent = static_cast(present); + info->maxDecPicBufferingMinus1 = static_cast(buffering); + } +} + +std::pair ParseVps(Bits& bits) { + const auto id = bits.Read(4); + bits.Skip(2); + const auto layers = bits.Read(6); + const auto sublayers = bits.Read(3); + if (layers != 0 || sublayers > 6) throw std::runtime_error("multilayer HEVC VPS metadata is unsupported"); + bits.Skip(17); + Profile(bits, sublayers); + Ordering(bits, sublayers); + const auto maxLayer = bits.Read(6); + const auto sets = bits.Unsigned(1023); + if (maxLayer != 0 || sets != 0) throw std::runtime_error("HEVC layer-set metadata is unsupported"); + Videodec2Hevc::Vps vps; + vps.timing = bits.Read(1) != 0; + if (vps.timing) { + vps.numUnitsInTick = bits.Read(32); + vps.timeScale = bits.Read(32); + } + return {id, vps}; +} + +void ScalingLists(Bits& bits) { + for (unsigned size = 0; size < 4; ++size) { + for (unsigned matrix = 0; matrix < 6; matrix += size == 3 ? 3 : 1) { + if (bits.Read(1) == 0) bits.Unsigned(); + else { + if (size > 1) bits.Signed(); + const unsigned coefficients = size == 0 ? 16 : 64; + for (unsigned coefficient = 0; coefficient < coefficients; ++coefficient) bits.Signed(); + } + } + } +} + +void ShortTermReferences(Bits& bits) { + const auto count = bits.Unsigned(64); + std::array deltas{}; + for (unsigned index = 0; index < count; ++index) { + if (index != 0 && bits.Read(1) != 0) { + bits.Skip(1); + bits.Unsigned(); + for (unsigned reference = 0; reference <= deltas[index - 1]; ++reference) { + const bool used = bits.Read(1) != 0; + if (used || bits.Read(1) != 0) ++deltas[index]; + } + } else { + const auto negative = bits.Unsigned(16); + const auto positive = bits.Unsigned(16); + if (negative + positive > 16) throw std::runtime_error("too many HEVC short-term references"); + deltas[index] = negative + positive; + for (unsigned reference = 0; reference < deltas[index]; ++reference) { + bits.Unsigned(); + bits.Skip(1); + } + } + if (deltas[index] > 16) throw std::runtime_error("too many predicted HEVC references"); + } +} + +void Hrd(Bits& bits, unsigned sublayers) { + const bool nal = bits.Read(1) != 0; + const bool vcl = bits.Read(1) != 0; + bool subpicture = false; + if (nal || vcl) { + subpicture = bits.Read(1) != 0; + if (subpicture) bits.Skip(19); + bits.Skip(8); + if (subpicture) bits.Skip(4); + bits.Skip(15); + } + for (unsigned layer = 0; layer <= sublayers; ++layer) { + const bool general = bits.Read(1) != 0; + const bool fixed = general || bits.Read(1) != 0; + bool lowDelay = false; + if (fixed) bits.Unsigned(); + else lowDelay = bits.Read(1) != 0; + const auto count = lowDelay ? 0 : bits.Unsigned(31); + for (unsigned kind = 0; kind < 2; ++kind) { + if (!(kind == 0 ? nal : vcl)) continue; + for (unsigned entry = 0; entry <= count; ++entry) { + bits.Unsigned(); + bits.Unsigned(); + if (subpicture) { + bits.Unsigned(); + bits.Unsigned(); + } + bits.Skip(1); + } + } + } +} + +void Vui(Bits& bits, unsigned sublayers, Videodec2Hevc::PictureInfo& info) { + info.aspectRatioInfoPresent = bits.Read(1); + if (info.aspectRatioInfoPresent) { + info.aspectRatioIdc = static_cast(bits.Read(8)); + if (info.aspectRatioIdc == 255) { + info.sarWidth = static_cast(bits.Read(16)); + info.sarHeight = static_cast(bits.Read(16)); + } else { + constexpr std::uint16_t ratios[17][2] = {{0, 0}, {1, 1}, {12, 11}, {10, 11}, {16, 11}, {40, 33}, {24, 11}, {20, 11}, {32, 11}, {80, 33}, {18, 11}, {15, 11}, {64, 33}, {160, 99}, {4, 3}, {3, 2}, {2, 1}}; + if (info.aspectRatioIdc > 16) throw std::runtime_error("reserved HEVC aspect ratio"); + info.sarWidth = ratios[info.aspectRatioIdc][0]; + info.sarHeight = ratios[info.aspectRatioIdc][1]; + } + } + if (bits.Read(1) != 0) bits.Skip(1); + info.videoSignalTypePresent = static_cast(bits.Read(1)); + if (info.videoSignalTypePresent) { + info.videoFormat = static_cast(bits.Read(3)); + info.fullRange = static_cast(bits.Read(1)); + info.colourDescriptionPresent = static_cast(bits.Read(1)); + if (info.colourDescriptionPresent) { + info.colourPrimaries = static_cast(bits.Read(8)); + info.transferCharacteristics = static_cast(bits.Read(8)); + info.matrixCoefficients = static_cast(bits.Read(8)); + } + } + info.chromaLocInfoPresent = static_cast(bits.Read(1)); + if (info.chromaLocInfoPresent) { + info.chromaLocTop = static_cast(bits.Unsigned(5)); + info.chromaLocBottom = static_cast(bits.Unsigned(5)); + } + bits.Skip(1); + info.fieldSequence = static_cast(bits.Read(1)); + info.frameFieldInfoPresent = static_cast(bits.Read(1)); + info.defaultDisplayWindow = bits.Read(1); + if (info.defaultDisplayWindow) { + info.displayLeft = bits.Unsigned(); + info.displayRight = bits.Unsigned(); + info.displayTop = bits.Unsigned(); + info.displayBottom = bits.Unsigned(); + } + info.timingInfoPresent = static_cast(bits.Read(1)); + if (info.timingInfoPresent) { + info.numUnitsInTick = bits.Read(32); + info.timeScale = bits.Read(32); + if (bits.Read(1) != 0) bits.Unsigned(); + if (bits.Read(1) != 0) Hrd(bits, sublayers); + } + if (bits.Read(1) != 0) { + bits.Skip(3); + for (unsigned value = 0; value < 5; ++value) bits.Unsigned(); + } +} + +std::pair ParseSps(Bits& bits) { + Videodec2Hevc::Sps sps; + auto& info = sps.info; + info.thisSize = sizeof(info); + info.isValid = true; + info.videoFormat = 5; + info.colourPrimaries = 2; + info.transferCharacteristics = 2; + info.matrixCoefficients = 2; + sps.vps = bits.Read(4); + const auto sublayers = bits.Read(3); + if (sublayers > 6) throw std::runtime_error("invalid HEVC temporal layers"); + bits.Skip(1); + bool mixed = false; + info.sourceScanType = Profile(bits, sublayers, &mixed, &info); + info.frameFieldInfoPresent = mixed ? 1 : 0; + const auto id = bits.Unsigned(15); + const auto chroma = bits.Unsigned(3); + if (chroma == 3) bits.Skip(1); + info.width = bits.Unsigned(); + info.height = bits.Unsigned(); + info.conformanceWindow = bits.Read(1); + if (info.conformanceWindow) { + info.confLeft = bits.Unsigned(); + info.confRight = bits.Unsigned(); + info.confTop = bits.Unsigned(); + info.confBottom = bits.Unsigned(); + } + info.bitDepthLumaMinus8 = static_cast(bits.Unsigned(8)); + info.bitDepthChromaMinus8 = static_cast(bits.Unsigned(8)); + const auto orderBits = bits.Unsigned(12) + 4; + Ordering(bits, sublayers, &info); + for (unsigned value = 0; value < 6; ++value) bits.Unsigned(); + if (bits.Read(1) != 0 && bits.Read(1) != 0) ScalingLists(bits); + bits.Skip(2); + if (bits.Read(1) != 0) { + bits.Skip(8); + bits.Unsigned(); + bits.Unsigned(); + bits.Skip(1); + } + ShortTermReferences(bits); + if (bits.Read(1) != 0) { + const auto count = bits.Unsigned(32); + for (unsigned reference = 0; reference < count; ++reference) bits.Skip(orderBits + 1); + } + bits.Skip(2); + if (bits.Read(1) != 0) Vui(bits, sublayers, info); + if (bits.Read(1) != 0 && bits.Read(8) != 0) sps.unsupported = "HEVC SPS extension metadata is unsupported"; + if (sublayers != 0) sps.unsupported = "HEVC sub-layer metadata projection is unsupported"; + if (info.conformanceWindow || info.defaultDisplayWindow) sps.unsupported = "HEVC crop/window metadata projection is unsupported"; + return {id, std::move(sps)}; +} + +std::vector PictureTiming(std::span bytes, Videodec2Hevc::PictureInfo& flags, bool& transferAmbiguous) { + std::vector timing; + std::size_t position = 0; + while (position < bytes.size() && !(bytes[position] == 0x80 && position + 1 == bytes.size())) { + const auto number = [&]() { + std::size_t value = 0; + for (;;) { + if (position == bytes.size()) throw std::runtime_error("truncated HEVC SEI header"); + const auto next = bytes[position++]; + if (value > std::numeric_limits::max() - next) throw std::runtime_error("HEVC SEI length overflow"); + value += next; + if (next != 255) return value; + } + }; + const auto type = number(); + const auto size = number(); + if (size > bytes.size() - position) throw std::runtime_error("truncated HEVC SEI payload"); + if (type == 1) { + if (flags.pictureTimingPresent) throw std::runtime_error("multiple HEVC picture-timing messages"); + flags.pictureTimingPresent = 1; + timing.assign(bytes.begin() + position, bytes.begin() + position + size); + } else if (type == 0) flags.bufferingPeriodPresent = 1; + else if (type == 45) flags.framePackingPresent = 1; + else if (type == 147) { + if (size != 1 || (flags.alternativeTransferPresent && flags.preferredTransfer != bytes[position])) { + transferAmbiguous = true; + throw std::runtime_error("invalid or conflicting HEVC alternative-transfer metadata"); + } + flags.alternativeTransferPresent = 1; + flags.preferredTransfer = bytes[position]; + } else if (type != 4 && type != 5 && type != 6 && type != 129 && type != 132 && type != 136 && type != 137 && type != 144) { + throw std::runtime_error("HEVC SEI metadata payload type " + std::to_string(type) + " is unsupported"); + } + position += size; + } + return timing; +} + +} // namespace + +namespace Videodec2Hevc { + +Metadata Parse_nid_no_patch(State& state, std::span annexB) { + Metadata metadata; + metadata.unsupported = state.unsupported; + PictureInfo flags{}; + std::vector starts; + for (std::size_t position = 0; position + 2 < annexB.size(); ++position) { + if (annexB[position] == 0 && annexB[position + 1] == 0 && annexB[position + 2] == 1) { + starts.push_back(position + 3); + position += 2; + } + } + std::optional pictureSps; + std::optional pictureType; + std::optional selectedSps; + std::optional selectedVps; + std::vector timing; + bool transferAmbiguous = false; + for (std::size_t index = 0; index < starts.size(); ++index) { + const auto start = starts[index]; + auto end = index + 1 == starts.size() ? annexB.size() : starts[index + 1] - 3; + while (end > start && annexB[end - 1] == 0) --end; + unsigned type = 64; + try { + if (end - start < 2) throw std::runtime_error("truncated HEVC NAL header"); + type = (annexB[start] >> 1) & 63; + if ((annexB[start] & 0x81) != 0 || (annexB[start + 1] & 0xf8) != 0) + throw std::runtime_error("multilayer HEVC picture metadata is unsupported"); + if ((annexB[start + 1] & 7) == 0) throw std::runtime_error("invalid HEVC temporal identifier"); + const auto bytes = Rbsp(annexB.subspan(start + 2, end - start - 2)); + Bits bits(bytes); + if (type == 32) { + flags.vpsPresent = 1; + auto [id, vps] = ParseVps(bits); + vps.bytes = bytes; + if (state.vps[id]) { + vps.unsupported = state.vps[id]->unsupported; + if (state.vps[id]->bytes != bytes) vps.unsupported = "HEVC VPS replacement metadata is unsupported until decoder recreation"; + } + state.vps[id] = std::move(vps); + } else if (type == 33) { + flags.spsPresent = 1; + auto [id, sps] = ParseSps(bits); + sps.bytes = bytes; + if (state.sps[id]) { + if (!state.sps[id]->unsupported.empty()) sps.unsupported = state.sps[id]->unsupported; + if (state.sps[id]->bytes != bytes) sps.unsupported = "HEVC SPS replacement metadata is unsupported until decoder recreation"; + } + state.sps[id] = std::move(sps); + } else if (type == 34) { + flags.ppsPresent = 1; + const auto id = bits.Unsigned(63); + Pps pps{bits.Unsigned(15), bytes, {}}; + if (state.pps[id]) { + pps.unsupported = state.pps[id]->unsupported; + if (state.pps[id]->bytes != bytes) pps.unsupported = "HEVC PPS replacement metadata is unsupported until decoder recreation"; + } + state.pps[id] = std::move(pps); + } else if (type == 35) flags.delimiterPresent = 1; + else if (type == 36) flags.endSequencePresent = 1; + else if (type == 37) flags.endStreamPresent = 1; + else if (type == 39 || type == 40) { + auto message = PictureTiming(bytes, flags, transferAmbiguous); + if (!message.empty()) { + if (type != 39 || !timing.empty()) throw std::runtime_error("unsupported HEVC picture-timing association"); + timing = std::move(message); + } + } else if (type < 32) { + const bool first = bits.Read(1) != 0; + if (type >= 16 && type <= 23) bits.Skip(1); + const auto pps = bits.Unsigned(63); + if (!state.pps[pps]) throw std::runtime_error("HEVC picture parameter set is unavailable"); + if (!state.pps[pps]->unsupported.empty() && metadata.unsupported.empty()) metadata.unsupported = state.pps[pps]->unsupported; + const auto sps = state.pps[pps]->sps; + if (pictureSps && (first || *pictureSps != sps)) throw std::runtime_error("multiple HEVC pictures in one access unit"); + if (pictureType && *pictureType != type) throw std::runtime_error("mixed HEVC slice NAL types in one access unit"); + if (!pictureSps) { + if (!state.sps[sps]) throw std::runtime_error("HEVC sequence parameter set is unavailable"); + selectedSps = state.sps[sps]; + selectedVps = state.vps[selectedSps->vps]; + pictureType = type; + flags.idr = type == 19 || type == 20; + flags.irap = type >= 16 && type <= 23; + } + pictureSps = sps; + } else if (type == 38) flags.fillerPresent = 1; + else throw std::runtime_error("reserved HEVC NAL metadata is unsupported"); + } catch (const std::runtime_error& error) { + if (metadata.unsupported.empty()) metadata.unsupported = error.what(); + if (type >= 32 && type <= 34 && state.unsupported.empty()) state.unsupported = error.what(); + } + } + if (!selectedSps) { + if (flags.endSequencePresent || flags.endStreamPresent) state.afterEnd = true; + if ((flags.alternativeTransferPresent || transferAmbiguous) && state.unsupported.empty()) + state.unsupported = "HEVC alternative transfer in a pictureless access unit is unsupported"; + if (metadata.unsupported.empty()) metadata.unsupported = "HEVC sequence parameter set is unavailable"; + return metadata; + } + const auto& sps = *selectedSps; + metadata.info = sps.info; + const bool newSequence = !state.hadPicture || (*pictureType >= 16 && *pictureType <= 20) || (*pictureType == 21 && state.afterEnd); + if (newSequence) { + state.preferredTransfer.reset(); + state.transferUnsupported.clear(); + } + if ((state.afterEnd || !state.hadPicture) && !flags.irap) + state.transferUnsupported = "HEVC non-IRAP continuation after reset/flush is unsupported"; + if (transferAmbiguous) state.transferUnsupported = "ambiguous HEVC alternative-transfer metadata"; + if (flags.alternativeTransferPresent) { + if (state.preferredTransfer && *state.preferredTransfer != flags.preferredTransfer) + state.transferUnsupported = "conflicting HEVC alternative transfers within one CLVS"; + state.preferredTransfer = flags.preferredTransfer; + } + if (state.preferredTransfer) metadata.info.preferredTransfer = *state.preferredTransfer; + if (!state.transferUnsupported.empty() && metadata.unsupported.empty()) metadata.unsupported = state.transferUnsupported; + state.hadPicture = true; + state.afterEnd = flags.endSequencePresent || flags.endStreamPresent; + if (!sps.unsupported.empty() && metadata.unsupported.empty()) metadata.unsupported = sps.unsupported; + if (!selectedVps) { + if (metadata.unsupported.empty()) metadata.unsupported = "HEVC video parameter set is unavailable"; + } else if (!metadata.info.timingInfoPresent && selectedVps->timing) { + if (metadata.unsupported.empty()) metadata.unsupported = "HEVC VPS-only timing projection is unsupported"; + } + if (selectedVps && !selectedVps->unsupported.empty() && metadata.unsupported.empty()) metadata.unsupported = selectedVps->unsupported; + if (metadata.info.frameFieldInfoPresent) { + try { + if (timing.empty()) throw std::runtime_error("HEVC picture timing is unavailable"); + Bits bits(timing); + metadata.info.picStruct = bits.Read(4); + metadata.info.sourceScanType = bits.Read(2); + metadata.info.duplicate = bits.Read(1); + if (metadata.info.picStruct > 12 || metadata.info.sourceScanType > 2) + throw std::runtime_error("reserved HEVC picture-timing metadata"); + } catch (const std::runtime_error& error) { + if (metadata.unsupported.empty()) metadata.unsupported = error.what(); + } + } + metadata.info.vpsPresent = flags.vpsPresent; + metadata.info.spsPresent = flags.spsPresent; + metadata.info.ppsPresent = flags.ppsPresent; + metadata.info.delimiterPresent = flags.delimiterPresent; + metadata.info.endSequencePresent = flags.endSequencePresent; + metadata.info.endStreamPresent = flags.endStreamPresent; + metadata.info.fillerPresent = flags.fillerPresent; + metadata.info.pictureTimingPresent = flags.pictureTimingPresent; + metadata.info.bufferingPeriodPresent = flags.bufferingPeriodPresent; + metadata.info.framePackingPresent = flags.framePackingPresent; + metadata.info.alternativeTransferPresent = flags.alternativeTransferPresent; + metadata.info.idr = flags.idr; + metadata.info.irap = flags.irap; + return metadata; +} + +} // namespace Videodec2Hevc diff --git a/core/libs/prx/libSceVideodec2/HevcMetadata.hpp b/core/libs/prx/libSceVideodec2/HevcMetadata.hpp new file mode 100644 index 0000000000..f9a980fef8 --- /dev/null +++ b/core/libs/prx/libSceVideodec2/HevcMetadata.hpp @@ -0,0 +1,61 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEODEC2_HEVCMETADATA_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEODEC2_HEVCMETADATA_HPP + +#include "prx/libSceVideodec2/include/Api.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace Videodec2Hevc { + +using PictureInfo = SceVideodec2::HevcPictureInfo; + +static_assert(sizeof(PictureInfo) == 184 && offsetof(PictureInfo, width) == 40 && offsetof(PictureInfo, picStruct) == 80 && offsetof(PictureInfo, vpsPresent) == 136); +static_assert(offsetof(PictureInfo, fillerPresent) == 142 && offsetof(PictureInfo, idr) == 147 && offsetof(PictureInfo, profile) == 151 && offsetof(PictureInfo, cropping) == 165 && offsetof(PictureInfo, cropBottom) == 180); + +struct Metadata { + PictureInfo info{}; + std::string unsupported; +}; + +struct Vps { + bool timing = false; + std::uint32_t numUnitsInTick = 0; + std::uint32_t timeScale = 0; + std::vector bytes; + std::string unsupported; +}; + +struct Sps { + PictureInfo info{}; + unsigned vps = 0; + std::string unsupported; + std::vector bytes; +}; + +struct Pps { + unsigned sps = 0; + std::vector bytes; + std::string unsupported; +}; + +struct State { + std::array, 16> vps; + std::array, 16> sps; + std::array, 64> pps; + std::string unsupported; + std::optional preferredTransfer; + std::string transferUnsupported; + bool hadPicture = false; + bool afterEnd = false; +}; + +Metadata Parse_nid_no_patch(State& state, std::span annexB); + +} // namespace Videodec2Hevc + +#endif diff --git a/core/libs/prx/libSceVideodec2/Unimplemented.cpp b/core/libs/prx/libSceVideodec2/Unimplemented.cpp index 62a9691332..dfe81fd3de 100644 --- a/core/libs/prx/libSceVideodec2/Unimplemented.cpp +++ b/core/libs/prx/libSceVideodec2/Unimplemented.cpp @@ -52,10 +52,6 @@ int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI sceVideodec2GetHevcPictureInfo() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI sceVideodec2InitLibrary() { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libSceVideodec2/include/Api.hpp b/core/libs/prx/libSceVideodec2/include/Api.hpp index b338ef37c4..8b00e719f1 100644 --- a/core/libs/prx/libSceVideodec2/include/Api.hpp +++ b/core/libs/prx/libSceVideodec2/include/Api.hpp @@ -155,6 +155,88 @@ struct Vp9PictureInfo { } +namespace SceVideodec2 { + +struct HevcPictureInfo { + std::uint64_t thisSize; + bool isValid; + std::uint64_t ptsData; + std::uint64_t dtsData; + std::uint64_t attachedData; + std::uint32_t width; + std::uint32_t height; + std::uint8_t bitDepthLumaMinus8; + std::uint8_t bitDepthChromaMinus8; + std::uint8_t timingInfoPresent; + std::uint32_t numUnitsInTick; + std::uint32_t timeScale; + std::uint32_t aspectRatioInfoPresent; + std::uint8_t aspectRatioIdc; + std::uint16_t sarWidth; + std::uint16_t sarHeight; + std::uint8_t videoSignalTypePresent; + std::uint8_t videoFormat; + std::uint8_t fullRange; + std::uint8_t colourDescriptionPresent; + std::uint8_t colourPrimaries; + std::uint8_t transferCharacteristics; + std::uint8_t matrixCoefficients; + std::uint8_t frameFieldInfoPresent; + std::uint32_t picStruct; + std::uint32_t sourceScanType; + std::uint32_t duplicate; + std::uint32_t conformanceWindow; + std::uint32_t confLeft; + std::uint32_t confRight; + std::uint32_t confTop; + std::uint32_t confBottom; + std::uint32_t defaultDisplayWindow; + std::uint32_t displayLeft; + std::uint32_t displayRight; + std::uint32_t displayTop; + std::uint32_t displayBottom; + std::uint8_t chromaLocInfoPresent; + std::uint8_t chromaLocTop; + std::uint8_t chromaLocBottom; + std::uint8_t fieldSequence; + std::uint8_t vpsPresent; + std::uint8_t spsPresent; + std::uint8_t ppsPresent; + std::uint8_t delimiterPresent; + std::uint8_t endSequencePresent; + std::uint8_t endStreamPresent; + std::uint8_t fillerPresent; + std::uint8_t pictureTimingPresent; + std::uint8_t bufferingPeriodPresent; + std::uint8_t framePackingPresent; + std::uint8_t alternativeTransferPresent; + std::uint8_t idr; + std::uint8_t irap; + std::uint8_t profileSpace; + std::uint8_t tier; + std::uint8_t profile; + std::uint8_t progressiveSource; + std::uint8_t interlacedSource; + std::uint8_t frameOnlyConstraint; + std::uint8_t level; + std::uint8_t subLayerProfilePresent; + std::uint8_t subLayerLevelPresent; + std::uint8_t subLayerProfileSpace; + std::uint8_t subLayerTier; + std::uint8_t subLayerProfile; + std::uint8_t subLayerLevel; + std::uint8_t subLayerOrderingPresent; + std::uint8_t maxDecPicBufferingMinus1; + std::uint8_t preferredTransfer; + std::uint8_t cropping; + std::uint32_t cropLeft; + std::uint32_t cropRight; + std::uint32_t cropTop; + std::uint32_t cropBottom; +}; + +} + extern "C" { int APS5_VABI sceVideodec2QueryComputeMemoryInfo_nid_postfix(SceVideodec2::ComputeMemoryInfo* info); @@ -181,7 +263,7 @@ int APS5_VABI sceVideodec2PresetDecoderConfigInfoVp9(); int APS5_VABI sceVideodec2CreateDecoderBid(); int APS5_VABI sceVideodec2CreateHevcDecoder(); int APS5_VABI sceVideodec2QueryHevcDecoderMemoryInfo(); -int APS5_VABI sceVideodec2GetHevcPictureInfo(); +int APS5_VABI sceVideodec2GetHevcPictureInfo_nid_postfix(const SceVideodec2::OutputInfo* output, SceVideodec2::HevcPictureInfo* info); int APS5_VABI sceVideodec2InitLibrary(); } diff --git a/core/libs/prx/libSceVideodec2/include/PictureMetadata.hpp b/core/libs/prx/libSceVideodec2/include/PictureMetadata.hpp new file mode 100644 index 0000000000..c8d505c5cb --- /dev/null +++ b/core/libs/prx/libSceVideodec2/include/PictureMetadata.hpp @@ -0,0 +1,36 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEODEC2_INCLUDE_PICTUREMETADATA_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEODEC2_INCLUDE_PICTUREMETADATA_HPP + +#include "Api.hpp" + +namespace SceVideodec2::Internal { + +struct Crop { + std::uint32_t left = 0; + std::uint32_t right = 0; + std::uint32_t top = 0; + std::uint32_t bottom = 0; +}; + +struct PictureHeader { + std::uint64_t pts; + std::uint64_t dts; + std::uint64_t attached; + bool keyFrame; + bool hevcIdr; + std::uint32_t profile; + std::uint32_t level; + Crop crop; + std::uint8_t bitDepthMinus8; +}; + +} + +extern "C" { + +void Videodec2PictureHeader_nid_no_patch(std::uint64_t handle, const void* frameBuffer, SceVideodec2::Internal::PictureHeader* header); +void Videodec2Vp9PictureInfo_nid_no_patch(std::uint64_t handle, const void* frameBuffer, SceVideodec2::Vp9PictureInfo* info); + +} + +#endif diff --git a/core/libs/prx/libSceZlib/CMakeLists.txt b/core/libs/prx/libSceZlib/CMakeLists.txt index 6beeb76258..4b216a85a4 100644 --- a/core/libs/prx/libSceZlib/CMakeLists.txt +++ b/core/libs/prx/libSceZlib/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceZlib SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +add_library(libSceZlib SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libkernel/Time/TimedWait.cpp) target_include_directories(libSceZlib PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/libjpeg-turbo/src/spng/zlib) set_target_properties(libSceZlib PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index f908068b6b..cf7ea91785 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -1,8 +1,7 @@ #include "prx/libSceZlib/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include "zlib.h" -#include -#include #include #include #include @@ -33,7 +32,7 @@ std::uint64_t nextId = 1; std::uint64_t session = 0; std::unordered_map results; std::deque completed; -std::condition_variable completion; +TimedWait::Condition completion; } // namespace @@ -55,7 +54,7 @@ std::int32_t APS5_VABI sceZlibFinalize() { completed.clear(); initialized = false; ++session; - completion.notify_all(); + completion.NotifyAll(); return 0; } @@ -82,7 +81,7 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe const std::uint64_t id = nextId++; results[id] = InflateResult{produced, status}; completed.push_back(id); - completion.notify_one(); + completion.NotifyOne(); *requestId = id; return 0; } @@ -93,8 +92,8 @@ std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t* requestId, std::uint32_ if (!requestId) return INVALID_ARGUMENT; const std::uint64_t waitSession = session; const auto finished = [waitSession] { return !completed.empty() || session != waitSession; }; - if (!timeout) completion.wait(lock, finished); - else if (!completion.wait_for(lock, std::chrono::microseconds(*timeout), finished)) return TIMED_OUT; + if (!timeout) completion.Wait(lock, finished); + else if (!completion.WaitUntil(lock, TimedWait::DeadlineNanos(*timeout), finished)) return TIMED_OUT; if (session != waitSession) throw std::runtime_error("sceZlibWaitForDone: finalization during a wait semantics unknown"); *requestId = completed.front(); completed.pop_front(); diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index a3b839e5b8..44c470edef 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -793,6 +793,11 @@ if(BUILD_TESTING) set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout guest_standard_streams_exclusive_stdout PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME guest_standard_streams_update_stdout COMMAND guest_standard_stream_tests --update-stdout) + if(WIN32) + set_tests_properties(guest_standard_streams_update_stdout PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_gets_tests ${LIBS_INCLUDE_DIR}/tests/GuestGets.cpp) target_include_directories(guest_gets_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_gets_tests PRIVATE libc ${CMAKE_DL_LIBS}) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index bce0b0b7f1..cf0eb2256a 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -260,6 +260,7 @@ void* APS5_VABI calloc_nid_postfix(size_t count, size_t size); int APS5_VABI posix_memalign_nid_postfix(void** pointer, size_t alignment, size_t size); void* APS5_VABI bsearch_nid_postfix(const void* key, const void* base, size_t count, size_t size, int (APS5_VABI *compare)(const void*, const void*)); int APS5_VABI mergesort_nid_postfix(void* base, size_t count, size_t size, GuestCompare compare); +int APS5_VABI heapsort_nid_postfix(void* base, size_t count, size_t size, GuestCompare compare); void APS5_VABI qsort_nid_postfix(void* base, size_t count, size_t size, int (APS5_VABI *compare)(const void*, const void*)); int APS5_VABI qsort_s_nid_postfix(void* base, size_t count, size_t size, GuestContextCompare compare, void* context); int APS5_VABI access_nid_postfix(const char* path, int mode); @@ -1028,6 +1029,7 @@ void APS5_VABI vsyslog_nid_postfix(int priority, const char* format, VaList* arg void APS5_VABI syslog_nid_postfix(int priority, const char* format, ...); int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); int APS5_VABI _Mtx_init_with_name_nid_postfix(void** handle, int type, const char*); +int APS5_VABI _Mtx_init_with_default_name_override_nid_postfix(void** handle, int type, const char* name); void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); int APS5_VABI _Mtx_lock_nid_postfix(void** handle); int APS5_VABI _Mtx_trylock_nid_postfix(void** handle); diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index a97f626f49..b624bc9433 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -281,6 +281,43 @@ public: #endif } + bool SwitchToWriting() { +#ifdef _WIN32 + ::_lock_file(GetHandle()); + const bool switched = SwitchToWritingUnlocked(); + ::_unlock_file(GetHandle()); + return switched; +#else + return true; +#endif + } + + bool SwitchToWritingUnlocked() { +#ifdef _WIN32 + constexpr long UcrtReading = 0x0001; + constexpr long UcrtWriting = 0x0002; + constexpr long UcrtUpdate = 0x0004; + constexpr long UcrtEndOfFile = 0x0008; + if (randomDevice) return true; + auto* native = reinterpret_cast(GetHandle()); + std::atomic_ref state(native->flags); + if ((state.load() & (UcrtWriting | UcrtUpdate)) == 0) { + errno = 9; + guestError = true; + SyncUnlockedStatus(); + return false; + } + if ((state.load() & (UcrtReading | UcrtUpdate)) != (UcrtReading | UcrtUpdate)) return true; + native->count = 0; + native->pointer = native->base; + state.fetch_and(~(UcrtReading | UcrtEndOfFile)); + SyncUnlockedStatus(); + return true; +#else + return true; +#endif + } + void KeepDirectoryReadError(bool failed) { if (failed && directory && (_guest.flags & 0x40) != 0) errno = 21; } diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index c94fd68bd1..6c1805fddc 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -69,6 +69,7 @@ void GuestAllocationsRemove_nid_postfix(void* mutation, const void* pointer); void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu, const std::function& apply); void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, const std::function& apply); Lease GuestAllocationsAcquire_nid_postfix(); +Lease GuestAllocationsAcquireMapped_nid_no_patch(const Mapped& mapped); Lease GuestAllocationsAcquireAll_nid_postfix(); std::shared_ptr GuestAllocationsLookup_nid_postfix(std::uintptr_t address, bool next); // Changes whenever a mutation ends, so callers can cache facts about guest mappings between changes. diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index ad07f41a7b..e3b9731782 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -18,6 +18,7 @@ #include extern "C" std::FILE* GuestStandardOutputFile_nid_no_patch(); +extern "C" std::FILE* GuestStandardOutputForWriting_nid_no_patch(); namespace LibcDetail { @@ -729,9 +730,11 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo } inline int PrintWindows(const char* format, const void* args) { + auto* output = GuestStandardOutputForWriting_nid_no_patch(); + if (!output) return -1; std::string buffer; const int size = FormatWindows(nullptr, 0, format, args, &buffer); - if (std::fwrite(buffer.data(), 1, static_cast(size), GuestStandardOutputFile_nid_no_patch()) != static_cast(size)) + if (std::fwrite(buffer.data(), 1, static_cast(size), output) != static_cast(size)) throw std::runtime_error("Formatted output write failed"); return size; } diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 4af1c81d98..11563e2673 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -90,8 +90,7 @@ public: cursor += size; } else { if (memory.State != MEM_COMMIT) throw std::runtime_error("guest memory is not committed"); - const auto mapped = views.find(cursor & ~(pageBytes - 1)); - if (mapped != views.end()) { + for (auto mapped = views.lower_bound(cursor & ~(pageBytes - 1)); mapped != views.end() && mapped->first < stop; ++mapped) { mapped->second.protection = protection; mapped->second.armed = 0; invalidate(*mapped->second.page); diff --git a/core/libs/prx/libc/include/specifics/linux/HostMapping.hpp b/core/libs/prx/libc/include/specifics/linux/HostMapping.hpp index 11c5e5cb45..7eed449734 100644 --- a/core/libs/prx/libc/include/specifics/linux/HostMapping.hpp +++ b/core/libs/prx/libc/include/specifics/linux/HostMapping.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,8 @@ static_assert(sizeof(procmap_query) == 104); enum : std::uint64_t { PROCMAP_QUERY_VMA_READABLE = 0x01, PROCMAP_QUERY_VMA_WRITABLE = 0x02, + PROCMAP_QUERY_VMA_EXECUTABLE = 0x04, + PROCMAP_QUERY_VMA_SHARED = 0x08, PROCMAP_QUERY_COVERING_OR_NEXT_VMA = 0x10, }; #define PROCMAP_QUERY _IOWR('f', 17, struct procmap_query) @@ -48,6 +51,11 @@ struct Mapping { std::uint64_t start; std::uint64_t end; std::string path; + bool readable = false; + bool writable = false; + bool executable = false; + bool shared = false; + std::uint64_t inode = 0; }; inline int QueryFd() { @@ -65,28 +73,40 @@ inline int QueryFd() { return fd; } +inline std::optional ParseMapsLine(const std::string& line) { + std::uint64_t start = 0; + std::uint64_t end = 0; + char perms[8] = {}; + std::uint64_t offset = 0; + unsigned int devMajor = 0; + unsigned int devMinor = 0; + std::uint64_t inode = 0; + char path[4096] = {}; + const int parsed = std::sscanf(line.c_str(), "%llx-%llx %7s %llx %x:%x %llu %4095s", + (unsigned long long*)&start, (unsigned long long*)&end, perms, + (unsigned long long*)&offset, &devMajor, &devMinor, (unsigned long long*)&inode, path); + if (parsed < 7 || std::strlen(perms) < 4) return std::nullopt; + return Mapping{start, end, parsed >= 8 ? path : "", perms[0] == 'r', perms[1] == 'w', perms[2] == 'x', perms[3] == 's', inode}; +} + inline std::optional ScanMaps(std::uint64_t address, const char* caller) { std::ifstream maps("/proc/self/maps"); if (!maps) throw std::runtime_error(std::string(caller) + ": failed to open /proc/self/maps"); std::string line; while (std::getline(maps, line)) { - std::uint64_t start = 0; - std::uint64_t end = 0; - char perms[8] = {}; - std::uint64_t offset = 0; - unsigned int devMajor = 0; - unsigned int devMinor = 0; - std::uint64_t inode = 0; - char path[4096] = {}; - const int parsed = std::sscanf(line.c_str(), "%llx-%llx %7s %llx %x:%x %llu %4095s", - (unsigned long long*)&start, (unsigned long long*)&end, perms, - (unsigned long long*)&offset, &devMajor, &devMinor, (unsigned long long*)&inode, path); - if (parsed < 7 || address < start || address >= end) continue; - return Mapping{start, end, parsed >= 8 ? path : ""}; + auto mapping = ParseMapsLine(line); + if (!mapping || address < mapping->start || address >= mapping->end) continue; + return mapping; } return std::nullopt; } +inline Mapping FromQuery(const procmap_query& query, const char* name) { + return Mapping{query.vma_start, query.vma_end, query.vma_name_size != 0 ? name : "", (query.vma_flags & PROCMAP_QUERY_VMA_READABLE) != 0, + (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) != 0, (query.vma_flags & PROCMAP_QUERY_VMA_EXECUTABLE) != 0, + (query.vma_flags & PROCMAP_QUERY_VMA_SHARED) != 0, query.inode}; +} + inline std::optional Find(std::uint64_t address, const char* caller) { const int fd = QueryFd(); if (fd < 0) return ScanMaps(address, caller); @@ -100,7 +120,39 @@ inline std::optional Find(std::uint64_t address, const char* caller) { if (errno == ENOENT) return std::nullopt; throw std::runtime_error(std::string(caller) + ": PROCMAP_QUERY failed"); } - return Mapping{query.vma_start, query.vma_end, query.vma_name_size != 0 ? name : ""}; + return FromQuery(query, name); +} + +template +void Walk(std::uint64_t begin, std::uint64_t end, const char* caller, Visit&& visit) { + if (begin >= end) return; + if (const int fd = QueryFd(); fd >= 0) { + for (auto cursor = begin; cursor < end;) { + procmap_query query{}; + query.size = sizeof(query); + query.query_flags = PROCMAP_QUERY_COVERING_OR_NEXT_VMA; + query.query_addr = cursor; + if (ioctl(fd, PROCMAP_QUERY, &query) != 0) { + if (errno == ENOENT) return; + throw std::runtime_error(std::string(caller) + ": PROCMAP_QUERY failed"); + } + if (query.vma_start >= end) return; + if (query.vma_end <= cursor) throw std::runtime_error(std::string(caller) + ": PROCMAP_QUERY went backwards"); + visit(FromQuery(query, "")); + cursor = query.vma_end; + } + return; + } + std::ifstream maps("/proc/self/maps"); + if (!maps) throw std::runtime_error(std::string(caller) + ": failed to open /proc/self/maps"); + std::string line; + while (std::getline(maps, line)) { + const auto mapping = ParseMapsLine(line); + if (!mapping) throw std::runtime_error(std::string(caller) + ": unexpected /proc/self/maps line: " + line); + if (mapping->end <= begin) continue; + if (mapping->start >= end) return; + visit(*mapping); + } } } diff --git a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp index 5efa1f00d3..8a556bd0e0 100644 --- a/core/libs/prx/libc/src/ApplicationProcessParameters.cpp +++ b/core/libs/prx/libc/src/ApplicationProcessParameters.cpp @@ -10,7 +10,7 @@ #define NOMINMAX #endif #include -#elif defined(__linux__) +#elif defined(__linux__) || defined(__APPLE__) #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include #endif @@ -77,7 +77,7 @@ const void* ApplicationProcessParameters_nid_no_patch() { } if (result == nullptr) throw std::runtime_error("application heap: process metadata is missing; relink the executable"); return result; -#elif defined(__linux__) +#elif defined(__linux__) || defined(__APPLE__) struct Search { const void* parameters = nullptr; bool invalid = false; diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index e023b0eaf4..364ba40805 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -536,6 +536,7 @@ size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t coun if (count > std::numeric_limits::max() / size) throw std::overflow_error("fwrite: buffer size overflow"); const auto bytes = size * count; if (bytes > std::numeric_limits::max() - reinterpret_cast(buffer)) throw std::overflow_error("fwrite: buffer address overflow"); + if (!stream->SwitchToWriting()) return 0; GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buffer, bytes); const auto result = std::fwrite(buffer, size, count, handle); stream->SyncStatus(); @@ -601,7 +602,9 @@ void APS5_VABI rewind_nid_postfix(FileStream* stream) { int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream) { if (!str) throw std::runtime_error("fputs: null string"); if (stream && !stream->IsOpen()) { errno = 9; return -1; } - const int result = std::fputs(str, GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (*str != '\0' && !stream->SwitchToWriting()) return -1; + const int result = std::fputs(str, native); stream->SyncStatus(); return result; } @@ -749,6 +752,43 @@ int APS5_VABI mergesort_nid_postfix(void* base, size_t count, size_t size, Guest return 0; } +int APS5_VABI heapsort_nid_postfix(void* base, size_t count, size_t size, GuestCompare compare) { + if (count <= 1) return 0; + if (size == 0) { + errno = 22; + return -1; + } + if (!compare) throw std::invalid_argument("heapsort: null comparator"); + if (!base) throw std::invalid_argument("heapsort: null base"); + if (count > std::numeric_limits::max() / size) throw std::overflow_error("heapsort: array size overflow"); + + auto* const bytes = static_cast(base); + const auto siftDown = [bytes, size, compare](size_t root, size_t n) { + size_t current = root; + while (2 * current + 1 < n) { + size_t child = 2 * current + 1; + if (child + 1 < n && compare(bytes + (child + 1) * size, bytes + child * size) > 0) { + child = child + 1; + } + if (compare(bytes + child * size, bytes + current * size) <= 0) { + break; + } + SwapElements(bytes + current * size, bytes + child * size, size); + current = child; + } + }; + + for (size_t i = count / 2; i > 0; --i) { + siftDown(i - 1, count); + } + for (size_t i = count - 1; i > 0; --i) { + SwapElements(bytes, bytes + i * size, size); + siftDown(0, i); + } + + return 0; +} + void APS5_VABI qsort_nid_postfix(void* base, size_t count, size_t size, int (APS5_VABI *compare)(const void*, const void*)) { if (!compare) throw std::invalid_argument("qsort: null comparator"); if (size == 0) throw std::invalid_argument("qsort: zero element size"); diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 1e908eee91..ac280da5ab 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -409,6 +409,7 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaList* args) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); + if (!stream->SwitchToWriting()) return -1; #ifdef _WIN32 std::string buffer; const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &buffer); @@ -661,6 +662,7 @@ static int WriteSecure(FileStream* stream, const char* format, const void* args) if (stream == nullptr || format == nullptr || args == nullptr) return -1; if (!stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); + if (!stream->SwitchToWriting()) return -1; std::string text; int count; try { @@ -768,6 +770,7 @@ int APS5_VABI printf_s_nid_postfix(const char* format, ...) { int APS5_VABI puts_nid_postfix(const char* s) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(&_Stdout_nid_postfix); + if (!_Stdout_nid_postfix.SwitchToWriting()) return EOF; if (std::fputs(s, native) < 0 || std::fputc('\n', native) == EOF) return EOF; return 0; } diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index b4d7d13bf0..6306b56cad 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -84,6 +84,12 @@ template void AppendNumber(std::u16string& out, const std::string& out.append(text.begin(), text.begin() + size); } +int AppendDecimalDigit(int value, char16_t digit) { + const int next = digit - u'0'; + if (value > (INT_MAX - next) / 10) throw std::invalid_argument("Format width or precision out of range"); + return value * 10 + next; +} + std::u16string WideNanText(const std::string& spec, bool negative, bool upper) { char16_t sign = negative ? u'-' : u'\0'; for (std::size_t index = 1; !negative && index < spec.size() && std::strchr("-+ #0", spec[index]); ++index) { @@ -138,10 +144,11 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = if (*format == u'*') { ++format; width = star(); + if (width == INT_MIN) throw std::invalid_argument("Format width out of range"); if (width < 0) { left = true; spec += '-'; width = -width; } spec += std::to_string(width); } else { - while (*format >= u'0' && *format <= u'9') { width = width * 10 + (*format - u'0'); spec += static_cast(*format++); } + while (*format >= u'0' && *format <= u'9') { width = AppendDecimalDigit(width, *format); spec += static_cast(*format++); } } if (*format == u'.') { ++format; @@ -152,7 +159,7 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = if (precision >= 0) spec += "." + std::to_string(precision); } else { spec += '.'; - while (*format >= u'0' && *format <= u'9') { precision = precision * 10 + (*format - u'0'); spec += static_cast(*format++); } + while (*format >= u'0' && *format <= u'9') { precision = AppendDecimalDigit(precision, *format); spec += static_cast(*format++); } } } std::string length; @@ -357,6 +364,7 @@ int APS5_VABI vsnwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { if (!__stdoutp_nid_postfix->IsOpen()) { errno = 9; return -1; } + if (!__stdoutp_nid_postfix->SwitchToWriting()) return -1; APS5_VA_BEGIN(format); int result = -1; try { @@ -373,6 +381,7 @@ int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { int APS5_VABI vfwprintf_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { if (stream == nullptr || format == nullptr) { errno = 22; return -1; } if (!stream->IsOpen()) { errno = 9; return -1; } + if (!stream->SwitchToWriting()) return -1; int result = -1; try { const std::u16string text = FormatWide(format, args); @@ -399,6 +408,7 @@ int APS5_VABI vwprintf_nid_postfix(const char16_t* format, VaList* args) { int APS5_VABI vfwprintf_s_nid_postfix(FileStream* stream, const char16_t* format, VaList* args) { if (stream == nullptr || format == nullptr || args == nullptr) return -1; if (!stream->IsOpen()) { errno = 9; return -1; } + if (!stream->SwitchToWriting()) return -1; int result = -1; try { const std::u16string text = FormatWide(format, args, true); @@ -435,6 +445,7 @@ int APS5_VABI fputwc_nid_postfix(char16_t value, FileStream* stream) { try { const std::string bytes = ToUtf8(std::u16string(1, value)); auto* native = GetNativeStream(stream); + if (!stream->SwitchToWriting()) return -1; if (std::fwrite(bytes.data(), 1, bytes.size(), native) != bytes.size()) throw std::runtime_error("stream write failed"); stream->SyncStatus(); return value; @@ -451,6 +462,7 @@ int APS5_VABI fputws_nid_postfix(const char16_t* str, FileStream* stream) { const std::u16string text(str); const std::string bytes = ToUtf8(text); auto* native = GetNativeStream(stream); + if (!stream->SwitchToWriting()) return -1; if (std::fwrite(bytes.data(), 1, bytes.size(), native) != bytes.size()) throw std::runtime_error("stream write failed"); stream->SyncStatus(); return static_cast(text.size()); diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index a127ca1b60..59c3892085 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -688,6 +688,42 @@ Lease GuestAllocationsAcquire_nid_postfix() { return Lease(std::move(ranges)); } +Lease GuestAllocationsAcquireMapped_nid_no_patch(const Mapped& mapped) { + std::lock_guard lock(registry().mutex); + auto& registered = registry().ranges; + std::vector targets; + targets.reserve(mapped.size()); + for (const auto& weak : mapped) { + const auto range = weak.lock(); + if (range == nullptr || !range->readable || range->bytes == 0) continue; + const auto found = registered.find(range->address); + if (found != registered.end() && found->second == range) targets.push_back(found); + } + std::sort(targets.begin(), targets.end(), [](const auto& a, const auto& b) { return a->first < b->first; }); + auto ranges = std::make_shared(); + const auto continues = [](const Range& before, const Range& after) { + return before.address + before.bytes == after.address && before.readable && after.readable && before.writable == after.writable && before.gpu == after.gpu; + }; + for (const auto target : targets) { + if (!ranges->empty() && target->first < ranges->back()->address + ranges->back()->bytes) continue; + auto first = target; + while (first != registered.begin()) { + auto previous = std::prev(first); + while (previous != registered.begin() && previous->second->bytes == 0) --previous; + if (previous->second->bytes == 0 || !continues(*previous->second, *first->second)) break; + first = previous; + } + for (auto current = first;;) { + ranges->push_back(current->second); + auto next = std::next(current); + while (next != registered.end() && next->second->bytes == 0) ++next; + if (next == registered.end() || !continues(*current->second, *next->second)) break; + current = next; + } + } + return Lease(std::move(ranges)); +} + std::shared_ptr GuestAllocationsLookup_nid_postfix(std::uintptr_t address, bool next) { std::lock_guard lock(registry().mutex); const auto& ranges = registry().ranges; diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index b475260fb5..eecd5815e7 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -91,6 +91,14 @@ float SinhfExp(float x) { return y * std::bit_cast(static_cast(0x7f + k) << 23); } +float GuestTgammaf(float x) { + const float result = static_cast(std::tgamma(static_cast(x))); + if (result != 0.0f || !std::isfinite(x) || x >= 0.0f || std::trunc(x) == x) return result; + const double interval = std::floor(-static_cast(x)); + const bool negative = std::fmod(interval, 2.0) == 0.0; + return std::copysign(0.0f, negative ? -1.0f : 1.0f); +} + float AtanhfLog1p(float x) { constexpr float Ln2High = 6.9313812256e-01f, Ln2Low = 9.0580006145e-06f; constexpr float Lp1 = 6.6666668653e-01f, Lp2 = 4.0000000596e-01f, Lp3 = 2.8571429849e-01f, Lp4 = 2.2222198546e-01f; @@ -357,7 +365,6 @@ float APS5_VABI acosf_nid_postfix(float x) { const float correction = (z - high * high) / (s + high); return 2.0f * (high + (AcosfRational(z) * s + correction)); } -float APS5_VABI atan2f_nid_postfix(float y, float x) { return std::atan2(y, x); } float APS5_VABI hypotf_nid_postfix(float x, float y) { return static_cast(std::hypot(static_cast(x), static_cast(y))); } double APS5_VABI hypot_nid_postfix(double x, double y) { const std::uint64_t hx = std::bit_cast(x); @@ -370,7 +377,20 @@ double APS5_VABI hypot_nid_postfix(double x, double y) { return std::hypot(x, y); } float APS5_VABI log10f_nid_postfix(float x) { return std::log10(x); } -float APS5_VABI logbf_nid_postfix(float x) { return std::logb(x); } +float APS5_VABI logbf_nid_postfix(float x) { + const std::uint32_t bits = std::bit_cast(x) & 0x7fffffffu; + if (bits == 0) { + return -1.0f / std::fabs(x); + } + if (bits >= 0x7f800000u) { + return x * x; + } + if (bits < 0x00800000u) { + const std::uint32_t scaled = std::bit_cast(x * 0x1p25f) & 0x7fffffffu; + return static_cast(static_cast(scaled >> 23) - 127 - 25); + } + return static_cast(static_cast(bits >> 23) - 127); +} float APS5_VABI exp2f_nid_postfix(float x) { static constexpr double exp2ft[16] = { 0x1.6a09e667f3bcdp-1, 0x1.7a11473eb0187p-1, 0x1.8ace5422aa0dbp-1, 0x1.9c49182a3f090p-1, @@ -427,7 +447,22 @@ long double APS5_VABI scalbnl_nid_postfix(long double x, int exponent) { return float APS5_VABI significandf_nid_postfix(float x) { return x == 0.0f || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } double APS5_VABI significand_nid_postfix(double x) { return x == 0.0 || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } double APS5_VABI frexp_nid_postfix(double x, int* exponent) { return std::frexp(x, exponent); } -float APS5_VABI frexpf_nid_postfix(float x, int* exponent) { return std::frexp(x, exponent); } +float APS5_VABI frexpf_nid_postfix(float x, int* exponent) { + auto bits = std::bit_cast(x); + auto magnitude = bits & 0x7fffffffu; + *exponent = 0; + if (magnitude == 0 || magnitude >= 0x7f800000u) return x; + int adjustment = 0; + if (magnitude < 0x00800000u) { + volatile float subnormal = x; + x = subnormal * 0x1p25f; + bits = std::bit_cast(x); + magnitude = bits & 0x7fffffffu; + adjustment = -25; + } + *exponent = static_cast(magnitude >> 23) - 126 + adjustment; + return std::bit_cast((bits & 0x807fffffu) | 0x3f000000u); +} // Guest long is 64-bit, including on Windows where native long is 32-bit. std::int64_t APS5_VABI lround_nid_postfix(double x) { return RoundToInt64(x); } std::int64_t APS5_VABI lroundf_nid_postfix(float x) { return RoundToInt64(x); } @@ -492,7 +527,7 @@ long double APS5_VABI log1pl_nid_postfix(long double x) { return std::log1p(x); long double APS5_VABI log2l_nid_postfix(long double x) { return std::log2(x); } long double APS5_VABI lgammal_nid_postfix(long double x) { return std::lgamma(x); } long double APS5_VABI tgammal_nid_postfix(long double x) { return std::tgamma(x); } -float APS5_VABI tgammaf_nid_postfix(float x) { return static_cast(std::tgamma(static_cast(x))); } +float APS5_VABI tgammaf_nid_postfix(float x) { return GuestTgammaf(x); } long double APS5_VABI hypotl_nid_postfix(long double x, long double y) { if (std::isinf(x) || std::isinf(y)) return std::numeric_limits::infinity(); return std::hypot(x, y); @@ -1169,10 +1204,233 @@ float APS5_VABI atanf_nid_postfix(float x) { const float result = atanhi[id] - ((x * (s1 + s2) - atanlo[id]) - x); return hx < 0 ? -result : result; } + +float APS5_VABI atan2f_nid_postfix(float y, float x) { + static volatile float tiny = 1.0e-30f; + static volatile float pi_lo = static_cast(-8.7422776573e-08); + const float pi_o_4 = static_cast(7.8539818525e-01); + const float pi_o_2 = static_cast(1.5707963705e+00); + const float pi = static_cast(3.1415927410e+00); + const std::int32_t hx = std::bit_cast(x); + const std::int32_t ix = hx & 0x7fffffff; + const std::int32_t hy = std::bit_cast(y); + const std::int32_t iy = hy & 0x7fffffff; + if (iy > 0x7f800000) { + return y + y; + } + if (ix > 0x7f800000) { + return x + x; + } + if (hx == 0x3f800000) { + return atanf_nid_postfix(y); + } + int m = ((hy >> 31) & 1) | ((hx >> 30) & 2); + if (iy == 0) { + if (m < 2) { + return y; + } + return m == 2 ? pi + tiny : -pi - tiny; + } + if (ix == 0) { + return hy < 0 ? -pi_o_2 - tiny : pi_o_2 + tiny; + } + if (ix == 0x7f800000) { + if (iy == 0x7f800000) { + switch (m) { + case 0: return pi_o_4 + tiny; + case 1: return -pi_o_4 - tiny; + case 2: return 3.0f * pi_o_4 + tiny; + default: return -3.0f * pi_o_4 - tiny; + } + } + switch (m) { + case 0: return 0.0f; + case 1: return -0.0f; + case 2: return pi + tiny; + default: return -pi - tiny; + } + } + if (iy == 0x7f800000) { + return hy < 0 ? -pi_o_2 - tiny : pi_o_2 + tiny; + } + const std::int32_t k = (iy - ix) >> 23; + float z; + if (k > 26) { + z = pi_o_2 + 0.5f * pi_lo; + m &= 1; + } else if (k < -26 && hx < 0) { + z = 0.0f; + } else { + z = atanf_nid_postfix(std::fabs(y / x)); + } + switch (m) { + case 0: return z; + case 1: return -z; + case 2: return pi - (z - pi_lo); + default: return (z - pi_lo) - pi; + } +} double APS5_VABI atan2_nid_postfix(double y, double x) { return std::atan2(y, x); } -float APS5_VABI powf_nid_postfix(float base, float exp) { - if (base == 1.0f) return 1.0f; - return std::pow(base, exp); +static inline std::int32_t PowfWord(float f) { return std::bit_cast(f); } +static inline float PowfFloat(std::int32_t w) { return std::bit_cast(w); } + +float APS5_VABI powf_nid_postfix(float x, float y) { + static const float bp[] = {1.0f, 1.5f}, dp_h[] = {0.0f, static_cast(5.84960938e-01)}, dp_l[] = {0.0f, static_cast(1.56322085e-06)}; + const float zero = 0.0f, half = 0.5f, qrtr = 0.25f, thrd = static_cast(3.33333343e-01), one = 1.0f, two = 2.0f, two24 = 16777216.0f; + const float huge = 1.0e30f, tiny = 1.0e-30f; + const float L1 = static_cast(6.0000002384e-01), L2 = static_cast(4.2857143283e-01), L3 = static_cast(3.3333334327e-01); + const float L4 = static_cast(2.7272811532e-01), L5 = static_cast(2.3066075146e-01), L6 = static_cast(2.0697501302e-01); + const float P1 = static_cast(1.6666667163e-01), P2 = static_cast(-2.7777778450e-03), P3 = static_cast(6.6137559770e-05); + const float P4 = static_cast(-1.6533901999e-06), P5 = static_cast(4.1381369442e-08); + const float lg2 = static_cast(6.9314718246e-01), lg2_h = static_cast(6.93145752e-01), lg2_l = static_cast(1.42860654e-06); + const float ovt = static_cast(4.2995665694e-08), cp = static_cast(9.6179670095e-01), cp_h = static_cast(9.6191406250e-01); + const float cp_l = static_cast(-1.1736857402e-04), ivln2 = static_cast(1.4426950216e+00), ivln2_h = static_cast(1.4426879883e+00); + const float ivln2_l = static_cast(7.0526075433e-06); + float z, ax, z_h, z_l, p_h, p_l, y1, t1, t2, r, s, sn, t, u, v, w; + std::int32_t i, j, k, yisint, n, is; + const std::int32_t hx = PowfWord(x), hy = PowfWord(y); + std::int32_t ix = hx & 0x7fffffff; + const std::int32_t iy = hy & 0x7fffffff; + if (iy == 0) return one; + if (ix > 0x7f800000 || iy > 0x7f800000) { + if ((ix > 0x7f800000 && (ix & 0x00400000) == 0) || + (iy > 0x7f800000 && (iy & 0x00400000) == 0)) std::feraiseexcept(FE_INVALID); + return PowfFloat((ix > 0x7f800000 ? hx : hy) | 0x00400000); + } + yisint = 0; + if (hx < 0) { + if (iy >= 0x4b800000) yisint = 2; + else if (iy >= 0x3f800000) { + k = (iy >> 23) - 0x7f; + j = iy >> (23 - k); + if ((j << (23 - k)) == iy) yisint = 2 - (j & 1); + } + } + if (iy == 0x7f800000) { + if (ix == 0x3f800000) return y - y; + else if (ix > 0x3f800000) return hy >= 0 ? y : zero; + else return hy < 0 ? -y : zero; + } + if (iy == 0x3f800000) return hy < 0 ? one / x : x; + if (hy == 0x40000000) return x * x; + if (hy == 0x3f000000) { + if (hx >= 0) return std::sqrt(x); + } + ax = std::fabs(x); + if (ix == 0x7f800000 || ix == 0 || ix == 0x3f800000) { + z = ax; + if (hy < 0) z = one / z; + if (hx < 0) { + if (((ix - 0x3f800000) | yisint) == 0) { + errno = EDOM; + z = (z - z) / (z - z); + } + else if (yisint == 1) z = -z; + } + return z; + } + n = static_cast((static_cast(hx) >> 31) - 1); + if ((n | yisint) == 0) { + errno = EDOM; + return (x - x) / (x - x); + } + sn = one; + if ((n | (yisint - 1)) == 0) sn = -one; + if (iy > 0x4d000000) { + if (ix < 0x3f7ffff8) return hy < 0 ? sn * huge * huge : sn * tiny * tiny; + if (ix > 0x3f800007) return hy > 0 ? sn * huge * huge : sn * tiny * tiny; + t = ax - 1; + w = (t * t) * (half - t * (thrd - t * qrtr)); + u = ivln2_h * t; + v = t * ivln2_l - w * ivln2; + t1 = u + v; + is = PowfWord(t1); + t1 = PowfFloat(is & static_cast(0xfffff000)); + t2 = v - (t1 - u); + } else { + float s2, s_h, s_l, t_h, t_l; + n = 0; + if (ix < 0x00800000) { ax *= two24; n -= 24; ix = PowfWord(ax); } + n += (ix >> 23) - 0x7f; + j = ix & 0x007fffff; + ix = j | 0x3f800000; + if (j <= 0x1cc471) k = 0; + else if (j < 0x5db3d7) k = 1; + else { k = 0; n += 1; ix -= 0x00800000; } + ax = PowfFloat(ix); + u = ax - bp[k]; + v = one / (ax + bp[k]); + s = u * v; + s_h = s; + is = PowfWord(s_h); + s_h = PowfFloat(is & static_cast(0xfffff000)); + is = ((ix >> 1) & static_cast(0xfffff000)) | 0x20000000; + t_h = PowfFloat(is + 0x00400000 + (k << 21)); + t_l = ax - (t_h - bp[k]); + s_l = v * ((u - s_h * t_h) - s_h * t_l); + s2 = s * s; + r = s2 * s2 * (L1 + s2 * (L2 + s2 * (L3 + s2 * (L4 + s2 * (L5 + s2 * L6))))); + r += s_l * (s_h + s); + s2 = s_h * s_h; + t_h = 3 + s2 + r; + is = PowfWord(t_h); + t_h = PowfFloat(is & static_cast(0xfffff000)); + t_l = r - ((t_h - 3) - s2); + u = s_h * t_h; + v = s_l * t_h + t_l * s; + p_h = u + v; + is = PowfWord(p_h); + p_h = PowfFloat(is & static_cast(0xfffff000)); + p_l = v - (p_h - u); + z_h = cp_h * p_h; + z_l = cp_l * p_h + p_l * cp + dp_l[k]; + t = static_cast(n); + t1 = (((z_h + z_l) + dp_h[k]) + t); + is = PowfWord(t1); + t1 = PowfFloat(is & static_cast(0xfffff000)); + t2 = z_l - (((t1 - t) - dp_h[k]) - z_h); + } + is = PowfWord(y); + y1 = PowfFloat(is & static_cast(0xfffff000)); + p_l = (y - y1) * t1 + y * t2; + p_h = y1 * t1; + z = p_l + p_h; + j = PowfWord(z); + if (j > 0x43000000 || (j == 0x43000000 && p_l + ovt > z - p_h)) { + errno = ERANGE; + return sn * huge * huge; + } + if ((j & 0x7fffffff) > 0x43160000 || (static_cast(j) == 0xc3160000u && p_l <= z - p_h)) { + errno = ERANGE; + return sn * tiny * tiny; + } + i = j & 0x7fffffff; + k = (i >> 23) - 0x7f; + n = 0; + if (i > 0x3f000000) { + n = j + (0x00800000 >> (k + 1)); + k = ((n & 0x7fffffff) >> 23) - 0x7f; + t = PowfFloat(n & ~(0x007fffff >> k)); + n = ((n & 0x007fffff) | 0x00800000) >> (23 - k); + if (j < 0) n = -n; + p_h -= t; + } + t = p_l + p_h; + is = PowfWord(t); + t = PowfFloat(is & static_cast(0xffff8000)); + u = t * lg2_h; + v = (p_l - (t - p_h)) * lg2 + t * lg2_l; + z = u + v; + w = v - (z - u); + t = z * z; + t1 = z - t * (P1 + t * (P2 + t * (P3 + t * (P4 + t * P5)))); + r = (z * t1) / (t1 - two) - (w + z * w); + z = one - (r - z); + j = PowfWord(z); + j += static_cast(static_cast(n) << 23); + if ((j >> 23) <= 0) z = std::scalbn(z, n); + else z = PowfFloat(j); + return sn * z; } double APS5_VABI pow_nid_postfix(double base, double exp) { return std::pow(base, exp); } float APS5_VABI __powisf2_nid_postfix(float base, int exponent) { @@ -1367,8 +1625,43 @@ double APS5_VABI nan_nid_postfix(const char* tagp) { return std::bit_cast(0x7ff8000000000000ull); } -double APS5_VABI modf_nid_postfix(double x, double* integral) { return std::modf(x, integral); } -float APS5_VABI modff_nid_postfix(float x, float* integral) { return std::modf(x, integral); } +double APS5_VABI modf_nid_postfix(double x, double* integral) { + const auto bits = std::bit_cast(x); + const auto exponent = (bits >> 52u) & 0x7ffull; + const auto zero = std::bit_cast(bits & 0x8000000000000000ull); + if (exponent < 1023u) { + *integral = zero; + return x; + } + if (exponent >= 1075u) { + *integral = x; + return exponent == 0x7ffu && (bits & 0xfffffffffffffull) != 0 ? x : zero; + } + const auto mask = (std::uint64_t{1} << (1075u - exponent)) - 1u; + *integral = std::bit_cast(bits & ~mask); + return (bits & mask) == 0 ? zero : x - *integral; +} +float APS5_VABI modff_nid_postfix(float x, float* integral) { + const auto bits = std::bit_cast(x); + const auto exponent = (bits >> 23u) & 0xffu; + const auto zero = std::bit_cast(bits & 0x80000000u); + if (exponent < 127u) { + *integral = zero; + return x; + } + if (exponent >= 150u) { + *integral = x; + if (exponent == 0xffu && (bits & 0x7fffffu) != 0) { + volatile int unordered = _mm_ucomieq_ss(_mm_set_ss(x), _mm_setzero_ps()); + (void)unordered; + return x; + } + return zero; + } + const auto mask = (std::uint32_t{1} << (150u - exponent)) - 1u; + *integral = std::bit_cast(bits & ~mask); + return (bits & mask) == 0 ? zero : x - *integral; +} double APS5_VABI tanh_nid_postfix(double x) { static volatile double tiny = 1.0e-300; const double huge = 1.0e300; @@ -1411,7 +1704,20 @@ long double APS5_VABI ldexpl_nid_postfix(long double x, int exp) { return std::l long double APS5_VABI truncl_nid_postfix(long double x) { return std::trunc(x); } long double APS5_VABI ceill_nid_postfix(long double x) { return std::ceil(x); } long double APS5_VABI floorl_nid_postfix(long double x) { return std::floor(x); } -double APS5_VABI logb_nid_postfix(double x) { return std::logb(x); } +double APS5_VABI logb_nid_postfix(double x) { + const std::uint64_t bits = std::bit_cast(x) & 0x7fffffffffffffffull; + if (bits == 0) { + return -1.0 / std::fabs(x); + } + if (bits >= 0x7ff0000000000000ull) { + return x * x; + } + if (bits < 0x0010000000000000ull) { + const std::uint64_t scaled = std::bit_cast(x * 0x1p54) & 0x7fffffffffffffffull; + return static_cast(static_cast(scaled >> 52) - 1023 - 54); + } + return static_cast(static_cast(bits >> 52) - 1023); +} int APS5_VABI ilogb_nid_postfix(double x) { if (x == 0.0) { return INT_MIN; @@ -1460,16 +1766,7 @@ int APS5_VABI __isnanf_nid_postfix(float x) { return std::isnan(x) ? 1 : 0; } int APS5_VABI isnanf_nid_postfix(float x) { return __isnanf_nid_postfix(x); } int APS5_VABI isnan_nid_postfix(double x) { return __isnan_nid_postfix(x); } int APS5_VABI isinf_nid_postfix(double x) { return __isinf_nid_postfix(x); } -int APS5_VABI __fpclassifyf_nid_postfix(float x) { - constexpr int Infinite = 0x01, NotANumber = 0x02, Normal = 0x04, Subnormal = 0x08, Zero = 0x10; - std::uint32_t bits; - std::memcpy(&bits, &x, sizeof(bits)); - const auto exponent = bits & 0x7f800000u; - const auto fraction = bits & 0x007fffffu; - if (exponent == 0) return fraction != 0 ? Subnormal : Zero; - if (exponent == 0x7f800000u) return fraction != 0 ? NotANumber : Infinite; - return Normal; -} +int APS5_VABI __fpclassifyf_nid_postfix(float x) { return _FDtest_nid_postfix(&x); } int APS5_VABI __signbitf_nid_postfix(float x) { return std::signbit(x) ? 1 : 0; } int APS5_VABI __signbitl_nid_postfix(long double x) { static_assert(sizeof(long double) == 16, "guest long double requires x87 extended precision storage"); diff --git a/core/libs/prx/libc/src/RealPath.cpp b/core/libs/prx/libc/src/RealPath.cpp index 92c58204d9..2404bbcfbd 100644 --- a/core/libs/prx/libc/src/RealPath.cpp +++ b/core/libs/prx/libc/src/RealPath.cpp @@ -2,6 +2,11 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#ifdef _WIN32 +#include +#include +#endif +#include #include #include #include @@ -22,6 +27,7 @@ constexpr int NotDirectory = 20; constexpr int InvalidArgument = 22; constexpr int TooManyLinks = 62; constexpr int NameTooLong = 63; +constexpr unsigned MaxSymbolicLinks = 32; int StatusError(const std::error_code& error) { if (error == std::errc::no_such_file_or_directory) return NoEntry; @@ -48,11 +54,70 @@ void DropLastComponent(std::string& result) { bool IsSeparator(char character) { return Separators.find(character) != std::string_view::npos; } +bool ReadGuestLink(const std::filesystem::path& host, std::string& target, bool& isLink, bool& absolute) { + isLink = false; + absolute = false; +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(host.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return true; + const HANDLE handle = CreateFileW(host.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return true; + struct ReparseHeader { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + ULONG flags; + } header{}; + alignas(8) std::array data{}; + DWORD size = 0; + const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast(data.size()), &size, nullptr); + CloseHandle(handle); + if (!read || size < sizeof(header)) return true; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_SYMLINK) return true; + constexpr ULONG relativeFlag = 1; + if ((header.flags & relativeFlag) == 0) { + absolute = true; + return true; + } + isLink = true; + const std::size_t begin = sizeof(header) + header.substituteNameOffset; + if (header.substituteNameLength % sizeof(wchar_t) != 0 || begin + header.substituteNameLength > size) return false; + std::wstring name(header.substituteNameLength / sizeof(wchar_t), L'\0'); + if (!name.empty()) std::memcpy(name.data(), data.data() + begin, header.substituteNameLength); + if (!name.empty()) { + const int length = WideCharToMultiByte(CP_UTF8, 0, name.data(), static_cast(name.size()), nullptr, 0, nullptr, nullptr); + target.assign(static_cast(length), '\0'); + WideCharToMultiByte(CP_UTF8, 0, name.data(), static_cast(name.size()), target.data(), length, nullptr, nullptr); + } +#else + std::error_code code; + if (!std::filesystem::is_symlink(std::filesystem::symlink_status(host, code))) return true; + isLink = true; + target = std::filesystem::read_symlink(host, code).generic_string(); + if (code) return false; +#endif + absolute = !target.empty() && IsSeparator(target.front()); +#ifdef _WIN32 + absolute = absolute || (target.size() > 1 && target[1] == ':'); +#endif + if (absolute) { + isLink = false; + target.clear(); + } + return true; +} + bool ResolveGuest(const char* path, std::string& result, int& error) { - std::string_view left(path); + std::string left(path); if (IsSeparator(left.front())) { result = "/"; - left.remove_prefix(1); + left.erase(0, 1); } else { char current[PathMax]; if (getcwd_nid_postfix(current, sizeof(current)) == nullptr) { @@ -66,11 +131,13 @@ bool ResolveGuest(const char* path, std::string& result, int& error) { error = NameTooLong; return false; } + unsigned links = 0; + bool expandLinks = true; while (!left.empty()) { const auto separator = left.find_first_of(Separators); - const auto token = left.substr(0, separator); - const bool moreFollows = separator != std::string_view::npos; - left = moreFollows ? left.substr(separator + 1) : std::string_view(); + const std::string token = left.substr(0, separator); + const bool moreFollows = separator != std::string::npos; + left = moreFollows ? left.substr(separator + 1) : std::string(); if (result.size() > 1 && result.size() + 1 >= PathMax) { error = NameTooLong; return false; @@ -88,8 +155,48 @@ bool ResolveGuest(const char* path, std::string& result, int& error) { error = NameTooLong; return false; } + const auto host = ResolvePath_nid_no_patch(candidate.c_str()); + std::string target; + bool isLink = false; + bool absolute = false; + if (expandLinks && !ReadGuestLink(host, target, isLink, absolute)) { + error = InputOutput; + return false; + } + if (absolute) expandLinks = false; + if (isLink) { + if (links++ > MaxSymbolicLinks) { + error = TooManyLinks; + return false; + } + if (target.empty()) { + error = NoEntry; + return false; + } + if (target.size() >= PathMax) { + error = NameTooLong; + return false; + } + DropLastComponent(result); + if (moreFollows) { + if (!IsSeparator(target.back())) { + if (target.size() + 1 >= PathMax) { + error = NameTooLong; + return false; + } + target += '/'; + } + target += left; + if (target.size() >= PathMax) { + error = NameTooLong; + return false; + } + } + left = target; + continue; + } std::error_code code; - const auto status = std::filesystem::status(ResolvePath_nid_no_patch(candidate.c_str()), code); + const auto status = std::filesystem::status(host, code); if (code && code != std::errc::no_such_file_or_directory && code != std::errc::not_a_directory) { error = StatusError(code); return false; diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index dbaf710627..0b95021e58 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -62,6 +62,11 @@ std::FILE* GuestStandardOutputFile_nid_no_patch() { return GetNativeStream(&_Stdout_nid_postfix); } +std::FILE* GuestStandardOutputForWriting_nid_no_patch() { + auto* native = GetNativeStream(&_Stdout_nid_postfix); + return _Stdout_nid_postfix.SwitchToWriting() ? native : nullptr; +} + int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle) { std::lock_guard lock(g_exitMutex); g_exitDestructors.push_back({func, arg, dsoHandle}); diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index b40f822753..de2549b946 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -184,17 +184,21 @@ std::int32_t APS5_VABI getwchar_nid_postfix() { return fgetwc_nid_postfix(__stdi int APS5_VABI getchar_unlocked_nid_postfix() { return getc_unlocked_nid_postfix(__stdinp_nid_postfix); } int APS5_VABI fputc_nid_postfix(int value, FileStream* stream) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } - const int result = std::fputc(value, GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (!stream->SwitchToWriting()) return -1; + const int result = std::fputc(value, native); stream->SyncStatus(); return result; } int APS5_VABI putc_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } + auto* native = GetNativeStream(stream); + if (!stream->SwitchToWritingUnlocked()) return -1; #ifdef _WIN32 - const int result = _fputc_nolock(value, GetNativeStream(stream)); + const int result = _fputc_nolock(value, native); #else - const int result = putc_unlocked(value, GetNativeStream(stream)); + const int result = putc_unlocked(value, native); #endif stream->SyncUnlockedStatus(); return result; diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 45677ee534..08292776eb 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -204,6 +204,10 @@ int APS5_VABI _Mtx_init_with_name_nid_postfix(void** handle, int type, const cha return _Mtx_init_nid_postfix(handle, type); } +int APS5_VABI _Mtx_init_with_default_name_override_nid_postfix(void** handle, int type, const char* name) { + return _Mtx_init_with_name_nid_postfix(handle, type, name); +} + void APS5_VABI _Mtx_destroy_nid_postfix(void** handle) { if (!handle) throw std::invalid_argument("_Mtx_destroy: null mutex handle pointer"); diff --git a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp index 5794602639..e990c12f0b 100644 --- a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp +++ b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp @@ -88,6 +88,22 @@ struct CompactReadFileGatherCommand { }; static_assert(sizeof(CompactReadFileGatherCommand) == 12); +inline constexpr std::uint32_t NopHeaderBytes = 4; +inline constexpr std::uint32_t NopHeaderMask = 0xffff00ffu; +inline constexpr std::uint32_t NopHeaderTag = 0x5452000fu; + +constexpr std::uint32_t NopHeader(std::uint32_t payloadDwords, std::uint32_t type = 0) { + return NopHeaderTag | (payloadDwords & 0xfu) << 8u | (type & 0xfu) << 12u; +} + +constexpr bool IsNopHeader(std::uint32_t header) { + return (header & NopHeaderMask) == NopHeaderTag; +} + +constexpr std::uint32_t NopDwords(std::uint32_t header) { + return (header >> 8u & 0xfu) + 1u; +} + struct ResetGatherScatterStateCommand { CommandHeader header; }; diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 02e86bf63a..dda99c8fbf 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -589,10 +589,19 @@ struct Deferred { std::optional _execute(const Apr::CommandBufferObject& buffer, std::uint32_t start = 0, ReadCursor read = {}, bool defer = false) { std::uint32_t cursor = start; while (cursor < buffer.offset) { + if (buffer.offset - cursor < Apr::NopHeaderBytes) throw std::runtime_error("APR: truncated command buffer"); + std::uint32_t first; + std::memcpy(&first, buffer.base + cursor, sizeof(first)); + if (Apr::IsNopHeader(first)) { + const std::uint32_t bytes = Apr::NopDwords(first) * Apr::NopHeaderBytes; + if (bytes > buffer.offset - cursor) throw std::runtime_error("APR: malformed command"); + cursor += bytes; + continue; + } if (cursor + sizeof(Apr::CommandHeader) > buffer.offset) throw std::runtime_error("APR: truncated command buffer"); Apr::CommandHeader header; std::memcpy(&header, buffer.base + cursor, sizeof(header)); - if ((static_cast(header.opcode) & 0xffu) == static_cast(Apr::Opcode::CompactReadFileGather)) { + if ((first & 0xffu) == static_cast(Apr::Opcode::CompactReadFileGather)) { if (sizeof(Apr::CompactReadFileGatherCommand) > buffer.offset - cursor) throw std::runtime_error("APR: truncated compact gather command"); Apr::CompactReadFileGatherCommand command; std::memcpy(&command, buffer.base + cursor, sizeof(command)); @@ -604,7 +613,6 @@ std::optional _execute(const Apr::CommandBufferObject& buffer, std::ui } if (header.bytes < sizeof(header) || header.bytes > buffer.offset - cursor) throw std::runtime_error("APR: malformed command"); switch (header.opcode) { - case Apr::Opcode::Nop: case Apr::Opcode::PushMarker: case Apr::Opcode::PopMarker: case Apr::Opcode::SetMarker: diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 31bf8a68fa..974df3b88c 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -35,6 +35,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Rwlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/MainThreadMacOs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Atfork.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Attr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Barrier.cpp @@ -85,6 +86,9 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamJsonParser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamSfoParser.cpp ) +if(APPLE) + target_link_libraries(libkernel PRIVATE "-framework CoreFoundation") +endif() target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}) target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}/nid/include) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 544bc31ff9..22e5631f8b 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -207,6 +207,7 @@ namespace { constexpr int GuestMapFixedFlag = 0x10; constexpr int GuestProtGpuReadWrite = 0x30; +constexpr int GuestProtCpuExecute = 0x4; #if defined(__linux__) void* MapAtOrAbove(std::uintptr_t start, size_t len, int prot, size_t alignment) { @@ -1045,6 +1046,7 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, if (physStart < 0 || (static_cast(physStart) & (PS5_PAGE_SIZE - 1)) != 0 || static_cast(physStart) >= DIRECT_MEMORY_SIZE || len > DIRECT_MEMORY_SIZE - static_cast(physStart)) { return SCE_KERNEL_ERROR_EINVAL; } + if ((prot & ~0x3F7) == 0 && (prot & GuestProtCpuExecute) != 0) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; flags = PlaceHintInReservation(*addr, len, flags); if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 75fe04fd67..1b6b1d3dba 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -21,6 +21,10 @@ #include #include #include +#ifdef __APPLE__ +#include +#include +#endif #endif #include #include @@ -690,6 +694,20 @@ int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { int LockHostPages(std::uintptr_t start, std::uintptr_t end) { auto* const address = reinterpret_cast(start); const std::size_t bytes = end - start; +#ifdef __APPLE__ + for (mach_vm_address_t cursor = start; cursor < end;) { + mach_vm_address_t region = cursor; + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + const kern_return_t status = mach_vm_region(mach_task_self(), ®ion, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object); + if (status == KERN_INVALID_ADDRESS) return SCE_KERNEL_ERROR_ENOMEM; + if (status != KERN_SUCCESS) throw std::runtime_error("mach_vm_region failed"); + if (region > cursor || (info.protection & (VM_PROT_READ | VM_PROT_EXECUTE)) == 0) return SCE_KERNEL_ERROR_ENOMEM; + cursor = region + size; + } +#endif if (::mlock(address, bytes) == 0) { RecordLockedRange(start, end); return 0; diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index bafe1bc884..81295e330c 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -251,9 +251,10 @@ int APS5_VABI munmap_nid_postfix(void* address, std::size_t length) noexcept { return -1; }; std::size_t rounded; - const auto start = reinterpret_cast(address); - if (!RoundLength(length, rounded) || start == 0 || - (start & (PS5_PAGE_SIZE - 1)) != 0 || + const auto offset = reinterpret_cast(address) & (PS5_PAGE_SIZE - 1); + const auto start = reinterpret_cast(address) - offset; + if (length == 0 || length > std::numeric_limits::max() - offset || + !RoundLength(length + offset, rounded) || start == 0 || rounded > std::numeric_limits::max() - start) return failed(GuestInvalid); try { diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index c1a1d1cfcd..fa7e9ac461 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1472,11 +1472,15 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* guestIov, int iovcn } if (!write && offset == nullptr && File::IsDirectoryDescriptor(d)) return SceErrorFromErrno(GUEST_EISDIR); if (write && iovcnt > 1 && total <= static_cast(std::numeric_limits::max()) && File::Appends(d)) { - std::vector gathered; - gathered.reserve(total); + std::vector gathered(total); + std::size_t gatheredOffset = 0; for (int i = 0; i < iovcnt; ++i) { - const auto* base = static_cast(iov[i].base); - gathered.insert(gathered.end(), base, base + iov[i].length); + if (iov[i].length == 0) continue; + SIZE_T copied = 0; + if (!ReadProcessMemory(GetCurrentProcess(), iov[i].base, gathered.data() + gatheredOffset, iov[i].length, &copied) || copied != iov[i].length) { + return SceErrorFromErrno(GUEST_EFAULT); + } + gatheredOffset += iov[i].length; } const auto result = offset != nullptr ? NativePositionedTransfer(d, gathered.data(), gathered.size(), *offset, true) : NativeTransferBuffer(d, gathered.data(), gathered.size(), true); diff --git a/core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp b/core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp new file mode 100644 index 0000000000..52543e433f --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp @@ -0,0 +1,62 @@ +#ifdef __APPLE__ + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct GuestStart { + void (*entry)(void*, void*); + void* block; +}; + +std::atomic mainLoopRunning {false}; + +void* RunGuest(void* argument) { + const auto start = *static_cast(argument); + start.entry(start.block, nullptr); + std::fprintf(stderr, "guest entry returned\n"); + std::abort(); +} + +void KeepAlive(CFRunLoopTimerRef, void*) {} + +} + +extern "C" { + +[[noreturn]] void Aps5StartGuest_nid_no_patch(void (*entry)(void*, void*), void* block) { + static GuestStart start; + start = {entry, block}; + pthread_attr_t attributes; + if (pthread_attr_init(&attributes) != 0) std::abort(); + const auto stackSize = std::max(pthread_get_stacksize_np(pthread_self()), 8u << 20); + if (pthread_attr_setstacksize(&attributes, stackSize) != 0) std::abort(); + mainLoopRunning.store(true, std::memory_order_release); + pthread_t thread; + if (pthread_create(&thread, &attributes, RunGuest, &start) != 0) { + std::fprintf(stderr, "cannot start the guest thread\n"); + std::abort(); + } + pthread_attr_destroy(&attributes); + CFRunLoopTimerRef timer = CFRunLoopTimerCreate(nullptr, 1.0e10, 1.0e10, 0, 0, KeepAlive, nullptr); + CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes); + for (;;) CFRunLoopRun(); +} + +void Aps5RunOnMainThread_nid_no_patch(void (*function)(void*), void* context) { + if (pthread_main_np() != 0 || !mainLoopRunning.load(std::memory_order_acquire)) { + function(context); + return; + } + dispatch_sync_f(dispatch_get_main_queue(), context, function); +} + +} + +#endif diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index a83f3e3c2d..358c534bae 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -131,25 +131,55 @@ bool WaitOnKeyUntil(std::uintptr_t key, TReady ready, std::optional -bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { - return WaitOnKeyUntil(reinterpret_cast(address), [&] { return std::atomic_ref(*address).load() != expected; }, deadlineNanos, caller); +TValue LoadAddressValue(TValue* address) { + if (reinterpret_cast(address) % alignof(TValue) == 0) return std::atomic_ref(*address).load(); + TValue value; + asm volatile("mov %1, %0" : "=r"(value) : "m"(*reinterpret_cast(address))); + return value; } template -int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) { - const auto deadline = timeout == nullptr ? std::nullopt : std::optional(TimedWait::DeadlineNanos(*timeout)); - return WaitOnAddressUntil(address, expected, deadline, caller) ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT; +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller, bool& differed) { + return WaitOnKeyUntil(reinterpret_cast(address), [&] { return differed = LoadAddressValue(address) != expected; }, deadlineNanos, caller); } -void WakeAddressLocked(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { +template +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { + bool differed = false; + return WaitOnAddressUntil(address, expected, deadlineNanos, caller, differed); +} + +template +int WaitOnAddress(TValue* address, TValue expected, KernelUseconds* timeout, const void* caller) { + if (timeout != nullptr && *timeout == 0) { + return SCE_KERNEL_ERROR_ETIMEDOUT; + } + if (address == nullptr) { + return SCE_KERNEL_ERROR_EFAULT; + } + const auto deadline = timeout == nullptr ? std::nullopt : std::optional(TimedWait::DeadlineNanos(*timeout)); + bool differed = false; + const bool woken = WaitOnAddressUntil(address, expected, deadline, caller, differed); + if (differed) { + return SCE_KERNEL_ERROR_EAGAIN; + } + if (timeout != nullptr) { + const std::uint64_t now = TimedWait::NowNanos(); + *timeout = woken && *deadline > now ? static_cast((*deadline - now) / 1000) : 0; + } + return woken ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT; +} + +std::int32_t WakeAddressLocked(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { auto& bucket = BucketOf(key); std::lock_guard lock(bucket.mutex); const auto entry = bucket.waiters.find(key); if (entry == bucket.waiters.end()) { - return; + return 0; } auto& queue = entry->second; - for (std::int32_t woken = 0; !queue.empty() && (woken < count || (wakesAtLeastOne && woken == 0)); ++woken) { + std::int32_t woken = 0; + for (; !queue.empty() && (woken < count || (wakesAtLeastOne && woken == 0)); ++woken) { AddressWaiter* waiter = queue.front(); queue.pop_front(); waiter->woken = true; @@ -158,10 +188,13 @@ void WakeAddressLocked(std::uintptr_t key, std::int32_t count, bool wakesAtLeast if (queue.empty()) { bucket.waiters.erase(entry); } + return woken; } -void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { - DeferringDelivery([&] { WakeAddressLocked(key, count, wakesAtLeastOne); }); +std::int32_t WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { + std::int32_t woken = 0; + DeferringDelivery([&] { woken = WakeAddressLocked(key, count, wakesAtLeastOne); }); + return woken; } std::uint64_t SaturatingNanos(std::int64_t seconds, std::int64_t nanos) { @@ -306,54 +339,33 @@ int SemWake(UserSemaphore* semaphore) { extern "C" { -int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name) { +int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, KernelUseconds* timeout, const char* name) { (void)name; - if (!IsAlignedAddress(reinterpret_cast(address))) { - APS5_INVALID_ARG_EX; - } return WaitOnAddress(address, expected, timeout, __builtin_return_address(0)); } -int APS5_VABI sceKernelSyncOnAddressWait8(std::uint8_t* address, std::uint8_t expected, const KernelUseconds* timeout, const char* name) { +int APS5_VABI sceKernelSyncOnAddressWait8(std::uint8_t* address, std::uint8_t expected, KernelUseconds* timeout, const char* name) { (void)name; - if (!IsAlignedAddress(reinterpret_cast(address))) { - APS5_INVALID_ARG_EX; - } return WaitOnAddress(address, expected, timeout, __builtin_return_address(0)); } -int APS5_VABI sceKernelSyncOnAddressWait16(std::uint16_t* address, std::uint16_t expected, const KernelUseconds* timeout, const char* name) { +int APS5_VABI sceKernelSyncOnAddressWait16(std::uint16_t* address, std::uint16_t expected, KernelUseconds* timeout, const char* name) { (void)name; - if (!IsAlignedAddress(reinterpret_cast(address))) { - APS5_INVALID_ARG_EX; - } return WaitOnAddress(address, expected, timeout, __builtin_return_address(0)); } -int APS5_VABI sceKernelSyncOnAddressWait32(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name) { +int APS5_VABI sceKernelSyncOnAddressWait32(std::uint32_t* address, std::uint32_t expected, KernelUseconds* timeout, const char* name) { (void)name; - if (!IsAlignedAddress(reinterpret_cast(address))) { - APS5_INVALID_ARG_EX; - } return WaitOnAddress(address, expected, timeout, __builtin_return_address(0)); } -int APS5_VABI sceKernelSyncOnAddressWait64(std::uint64_t* address, std::uint64_t expected, const KernelUseconds* timeout, const char* name) { +int APS5_VABI sceKernelSyncOnAddressWait64(std::uint64_t* address, std::uint64_t expected, KernelUseconds* timeout, const char* name) { (void)name; - if (!IsAlignedAddress(reinterpret_cast(address))) { - APS5_INVALID_ARG_EX; - } return WaitOnAddress(address, expected, timeout, __builtin_return_address(0)); } int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count) { - const auto key = reinterpret_cast(address); - if (!IsAlignedAddress(key) || count < 0) { - APS5_INVALID_ARG_EX; - } - - WakeAddress(key, count, false); - return SYNC_ON_ADDRESS_OK; + return WakeAddress(reinterpret_cast(address), count, true); } int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t value, void* sizeArgument, void* timeArgument) { diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 852bb34a1c..103338609e 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -8,6 +8,7 @@ #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" #include @@ -39,7 +40,11 @@ #include #include #include -#endif // __linux__ +#include +#elif defined(__APPLE__) +#include +#include +#endif #endif static constexpr int GUEST_EFAULT = 14; @@ -48,6 +53,29 @@ static constexpr int GUEST_CLOCK_REALTIME = 0; static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; static constexpr int GUEST_CLOCK_PROCESS_CPUTIME_ID = 15; +static bool ReadGuestTime(void* destination, const void* source, std::size_t bytes) { + if (bytes == 0) return true; + const auto start = reinterpret_cast(source); + if (start == 0 || bytes > UINTPTR_MAX - start) return false; + GuestArena::GuestArenaPrepareHostRead_nid_no_patch(source, bytes); +#ifdef _WIN32 + SIZE_T copied = 0; + return ReadProcessMemory(GetCurrentProcess(), source, destination, bytes, &copied) != 0 && copied == bytes; +#elif defined(__linux__) + iovec local{destination, bytes}; + iovec remote{const_cast(source), bytes}; + const auto copied = ::process_vm_readv(::getpid(), &local, 1, &remote, 1, 0); + if (copied < 0 && errno != EFAULT) throw std::runtime_error("time: process_vm_readv failed with errno " + std::to_string(errno)); + return copied >= 0 && static_cast(copied) == bytes; +#elif defined(__APPLE__) + mach_vm_size_t copied = 0; + return ::mach_vm_read_overwrite(::mach_task_self(), reinterpret_cast(source), bytes, + reinterpret_cast(destination), &copied) == KERN_SUCCESS && copied == bytes; +#else + throw std::runtime_error("time: checked guest reads are not implemented on this host"); +#endif +} + static bool IsCpuClock(int clockId) { return (static_cast(clockId) & CPU_CLOCK_BIT) != 0; } @@ -654,32 +682,43 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { } int APS5_VABI clock_settime_nid_postfix(int clockId, const KernelTimespec* tp) { - if (tp == nullptr) { + KernelTimespec time{}; + if (tp == nullptr || !ReadGuestTime(&time, tp, sizeof(time))) { *__error_nid_postfix() = GUEST_EFAULT; return -1; } - if (clockId != GUEST_CLOCK_REALTIME || tp->tv_nsec < 0 || tp->tv_nsec >= 1000000000 || tp->tv_sec < 0) { + if (clockId != GUEST_CLOCK_REALTIME || time.tv_nsec < 0 || time.tv_nsec >= 1000000000 || time.tv_sec < 0) { *__error_nid_postfix() = GUEST_EINVAL; return -1; } char requested[48]; - std::snprintf(requested, sizeof(requested), "%lld.%09lld", static_cast(tp->tv_sec), static_cast(tp->tv_nsec)); + std::snprintf(requested, sizeof(requested), "%lld.%09lld", static_cast(time.tv_sec), static_cast(time.tv_nsec)); throw std::runtime_error(std::string("clock_settime: setting CLOCK_REALTIME to ") + requested + " is not supported, the host clock is not changed"); } int SetTimeOfDay_nid_no_patch(const KernelTimeval* tv, const KernelTimezone* tz) { - if (tv != nullptr && (tv->tv_usec < 0 || tv->tv_usec >= 1000000 || tv->tv_sec < 0)) { + KernelTimeval time{}; + KernelTimezone zone{}; + if (tv != nullptr && !ReadGuestTime(&time, tv, sizeof(time))) { + *__error_nid_postfix() = GUEST_EFAULT; + return -1; + } + if (tz != nullptr && !ReadGuestTime(&zone, tz, sizeof(zone))) { + *__error_nid_postfix() = GUEST_EFAULT; + return -1; + } + if (tv != nullptr && (time.tv_usec < 0 || time.tv_usec >= 1000000 || time.tv_sec < 0)) { *__error_nid_postfix() = GUEST_EINVAL; return -1; } if (tv != nullptr) { char requested[48]; - std::snprintf(requested, sizeof(requested), "%lld.%06lld", static_cast(tv->tv_sec), static_cast(tv->tv_usec)); + std::snprintf(requested, sizeof(requested), "%lld.%06lld", static_cast(time.tv_sec), static_cast(time.tv_usec)); throw std::runtime_error(std::string("settimeofday: setting the time to ") + requested + " is not supported, the host clock is not changed"); } if (tz != nullptr) { std::lock_guard lock(guestTimezoneMutex); - guestTimezone = *tz; + guestTimezone = zone; } return 0; } diff --git a/core/libs/prx/libkernel/include/Api.hpp b/core/libs/prx/libkernel/include/Api.hpp index e36c9a773d..5451da0393 100644 --- a/core/libs/prx/libkernel/include/Api.hpp +++ b/core/libs/prx/libkernel/include/Api.hpp @@ -626,11 +626,11 @@ std::int64_t APS5_VABI _sendto_nid_postfix(int descriptor, const void* buffer, s std::int64_t APS5_VABI _recvfrom_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags, void* address, std::uint32_t* addressLength); std::int64_t APS5_VABI _sendmsg_nid_postfix(int descriptor, const GuestMsghdr* message, int flags); std::int64_t APS5_VABI _recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, int flags); -int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name); -int APS5_VABI sceKernelSyncOnAddressWait8(std::uint8_t* address, std::uint8_t expected, const KernelUseconds* timeout, const char* name); -int APS5_VABI sceKernelSyncOnAddressWait16(std::uint16_t* address, std::uint16_t expected, const KernelUseconds* timeout, const char* name); -int APS5_VABI sceKernelSyncOnAddressWait32(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name); -int APS5_VABI sceKernelSyncOnAddressWait64(std::uint64_t* address, std::uint64_t expected, const KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWait8(std::uint8_t* address, std::uint8_t expected, KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWait16(std::uint16_t* address, std::uint16_t expected, KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWait32(std::uint32_t* address, std::uint32_t expected, KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWait64(std::uint64_t* address, std::uint64_t expected, KernelUseconds* timeout, const char* name); int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count); int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t value, void* sizeArgument, void* timeArgument); int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stackSize, uint64_t context); diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 9c3d945fbc..db9612410e 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -240,11 +240,6 @@ int APS5_VABI openintr_nid_postfix(void) { return 0; } -int APS5_VABI profil_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI pthread_attr_getaffinity_np_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 6f728cbb70..195204da66 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -326,6 +326,16 @@ int APS5_VABI setsid_nid_postfix(void) { return static_cast(Fail(GuestEperm)); } +int APS5_VABI profil_nid_postfix(void* samples, std::size_t size, std::size_t offset, unsigned int scale) { + (void)samples; + (void)size; + (void)offset; + constexpr unsigned int MaxScale = 1u << 16; + if (scale > MaxScale) return static_cast(Fail(GuestEinval)); + if (scale != 0) NotImplemented_nid_no_patch("profil: the profiling clock"); + return 0; +} + std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask) { return File::ExchangeCreationMask_nid_no_patch(mask); } diff --git a/core/libs/prx/libkernel_sys/include/Api.hpp b/core/libs/prx/libkernel_sys/include/Api.hpp index b6d73831b3..6659f0e8f4 100644 --- a/core/libs/prx/libkernel_sys/include/Api.hpp +++ b/core/libs/prx/libkernel_sys/include/Api.hpp @@ -138,7 +138,7 @@ int APS5_VABI mdbg_call_nid_postfix(void); int APS5_VABI mdbg_service_nid_postfix(void); int APS5_VABI mount_nid_postfix(void); int APS5_VABI openintr_nid_postfix(void); -int APS5_VABI profil_nid_postfix(void); +int APS5_VABI profil_nid_postfix(void* samples, std::size_t size, std::size_t offset, unsigned int scale); int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void); int APS5_VABI pthread_attr_getsolosched_np_nid_postfix(void); int APS5_VABI pthread_attr_setaffinity_np_nid_postfix(void); diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 8c02b16146..27894e4131 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -20,6 +20,8 @@ #include #include #include +#elif defined(__APPLE__) +#include "ProcessParametersFixture.hpp" #endif extern "C" { @@ -100,6 +102,8 @@ void installProcessParameters() { return 1; }, &installed); require(installed); +#elif defined(__APPLE__) + require(ProcessParametersFixture::Install(process.data(), process.size())); #endif } diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index fc8af8e721..7bbec0c6a4 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -198,10 +198,79 @@ static void CheckUpdateStreamReadsAfterWrites(const std::string& base) { Require(std::remove(path.c_str()) == 0); } +template +static void CheckWriteAfterRead(const std::string& path, const char* mode, const char* expected, TAccess access) { + auto* stream = fopen_nid_postfix(path.c_str(), "wb"); + Require(stream != nullptr && fputs_nid_postfix("0123456789", stream) >= 0 && fclose_nid_postfix(stream) == 0, "Creating the written file"); + stream = fopen_nid_postfix(path.c_str(), mode); + Require(stream != nullptr, "fopen of the written file"); + access(stream); + Require(fclose_nid_postfix(stream) == 0, "fclose after a write after read"); + Require(ReadWhole(path) == expected, "Write after read wrote other bytes"); +} + +static void CheckStreamWritesAfterReads(const std::string& base) { + const std::string path = base + ".written"; + constexpr char appended[] = "0123456789XY"; +#if defined(__GLIBC__) + constexpr char afterReadAhead[] = "0XY3456789"; + constexpr char afterPushBack[] = "XY23456789"; +#else + constexpr char afterReadAhead[] = "0123456789XY"; + constexpr char afterPushBack[] = "0123456789XY"; +#endif + using Write = void (*)(FileStream*); + for (const Write write : { + +[](FileStream* stream) { Require(fwrite_nid_postfix("XY", 1, 2, stream) == 2, "fwrite after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fputs_nid_postfix("XY", stream) >= 0, "fputs after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fputc_nid_postfix('X', stream) == 'X' && fputc_nid_postfix('Y', stream) == 'Y', "fputc after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(putc_unlocked_nid_postfix('X', stream) == 'X' && putc_unlocked_nid_postfix('Y', stream) == 'Y', "putc_unlocked after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fprintf_nid_postfix(stream, "%s", "XY") == 2, "fprintf after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fprintf_s_nid_postfix(stream, "%s", "XY") == 2, "fprintf_s after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fputwc_nid_postfix(u'X', stream) == u'X' && fputwc_nid_postfix(u'Y', stream) == u'Y', "fputwc after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fputws_nid_postfix(u"XY", stream) >= 0, "fputws after fgetc on an update stream"); }, + +[](FileStream* stream) { Require(fwprintf_nid_postfix(stream, u"XY") == 2, "fwprintf after fgetc on an update stream"); }, + }) { + for (const char* mode : {"r+b", "a+b"}) { + CheckWriteAfterRead(path, mode, std::string_view(mode) == "a+b" ? appended : afterReadAhead, [write](FileStream* stream) { + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0 && fgetc_nid_postfix(stream) == '0', "fgetc on an update stream"); + write(stream); + Require(ferror_nid_postfix(stream) == 0, "Write after fgetc on an update stream set the error indicator"); + }); + } + } + CheckWriteAfterRead(path, "r+b", appended, [](FileStream* stream) { + std::array buffer{}; + Require(fread_nid_postfix(buffer.data(), 1, buffer.size(), stream) == buffer.size() && feof_nid_postfix(stream) == 0, "fread of every byte of an r+ stream"); + Require(fwrite_nid_postfix("XY", 1, 2, stream) == 2 && ferror_nid_postfix(stream) == 0, "fwrite after reading every byte of an r+ stream"); + }); + CheckWriteAfterRead(path, "r+b", afterReadAhead, [](FileStream* stream) { + Require(fgetc_nid_postfix(stream) == '0' && fflush_nid_postfix(stream) == 0, "fflush after fgetc on an r+ stream"); + Require(fputs_nid_postfix("XY", stream) >= 0 && ferror_nid_postfix(stream) == 0, "fputs after fgetc and fflush on an r+ stream"); + }); + CheckWriteAfterRead(path, "r+b", afterPushBack, [](FileStream* stream) { + Require(fgetc_nid_postfix(stream) == '0' && ungetc_nid_postfix('Z', stream) == 'Z', "ungetc after fgetc on an r+ stream"); + Require(fputs_nid_postfix("XY", stream) >= 0 && ferror_nid_postfix(stream) == 0, "fputs after ungetc on an r+ stream"); + }); + CheckWriteAfterRead(path, "rb", "0123456789", [](FileStream* stream) { + Require(fgetc_nid_postfix(stream) == '0', "fgetc on a read-only stream"); + errno = 0; + Require(fputc_nid_postfix('X', stream) == EOF && errno == EBADF && ferror_nid_postfix(stream) != 0, "fputc after fgetc on a read-only stream"); + clearerr_nid_postfix(stream); + errno = 0; + Require(fwrite_nid_postfix("XY", 1, 2, stream) == 0 && errno == EBADF && ferror_nid_postfix(stream) != 0, "fwrite after fgetc on a read-only stream"); + clearerr_nid_postfix(stream); + errno = 0; + Require(fprintf_nid_postfix(stream, "%s", "XY") == -1 && errno == EBADF && ferror_nid_postfix(stream) != 0, "fprintf after fgetc on a read-only stream"); + }); + Require(std::remove(path.c_str()) == 0); +} + int main(int argc, char** argv) { Require(argc == 2); CheckExclusiveCppOpen(argv[1]); CheckUpdateStreamReadsAfterWrites(argv[1]); + CheckStreamWritesAfterReads(argv[1]); Require(_Stdout_nid_postfix.GetHandle() == stdout); Require(_Stderr_nid_postfix.GetHandle() == stderr); Require(fputs_nid_postfix("stdout object works\n", &_Stdout_nid_postfix) >= 0); diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 7b5bc20439..2795ed3172 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -54,9 +54,20 @@ struct Recorder { std::uint32_t Commands() const { return sceAmprCommandBufferGetNumCommands(&buffer); } }; +std::uint32_t HeaderAt(const Recorder& recorder, std::uint32_t offset) { + std::uint32_t first; + std::memcpy(&first, recorder.memory.data() + offset, sizeof(first)); + return first; +} + void RequireAppended(const Recorder& recorder, std::uint32_t offset, std::uint32_t commands, Apr::Opcode opcode, std::uint64_t measured) { Require(recorder.Offset() == offset + measured); Require(recorder.Commands() == commands + 1); + if (opcode == Apr::Opcode::Nop) { + const std::uint32_t first = HeaderAt(recorder, offset); + Require(Apr::IsNopHeader(first) && Apr::NopDwords(first) * 4u == measured); + return; + } Apr::CommandHeader header; std::memcpy(&header, recorder.memory.data() + offset, sizeof(header)); if (opcode == Apr::Opcode::CompactReadFileGather) { @@ -338,19 +349,24 @@ void TestNops() { for (std::uint32_t dwords = 1; dwords <= 16; ++dwords) { const auto offset = recorder.Offset(); const auto commands = recorder.Commands(); + std::memset(recorder.memory.data() + offset, 0x5A, dwords * 4u); Require(sceAmprCommandBufferNop(&recorder.buffer, dwords) == 0); RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNop(dwords)); + Require(HeaderAt(recorder, offset) == (0x5452000Fu | (dwords - 1u) << 8u)); + for (std::size_t index = Apr::NopHeaderBytes; index < dwords * 4u; ++index) Require(recorder.memory[offset + index] == 0x5A); } const auto offset = recorder.Offset(); const auto commands = recorder.Commands(); Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 3, data) == 0); RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); - Require(std::memcmp(recorder.memory.data() + offset + sizeof(Apr::CommandHeader), data, sizeof(data)) == 0); + Require(HeaderAt(recorder, offset) == 0x5452030Fu); + Require(std::memcmp(recorder.memory.data() + offset + Apr::NopHeaderBytes, data, sizeof(data)) == 0); Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 0, nullptr) == 0); + Require(HeaderAt(recorder, offset + 16u) == 0x5452000Fu); const auto emptyPayloadOffset = recorder.Offset(); std::memset(recorder.memory.data() + emptyPayloadOffset, 0x5A, sceAmprMeasureCommandSizeNopWithData(4)); Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 3, nullptr) == 0); - for (std::size_t index = 0; index < 12; ++index) Require(recorder.memory[emptyPayloadOffset + sizeof(Apr::CommandHeader) + index] == 0x5A); + for (std::size_t index = 0; index < 12; ++index) Require(recorder.memory[emptyPayloadOffset + Apr::NopHeaderBytes + index] == 0x5A); Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); const auto rejected = static_cast(static_cast(invalidArgument)); @@ -359,6 +375,45 @@ void TestNops() { Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 16, data) == invalidArgument); Require(sceAmprMeasureCommandSizeNop(0) == rejected && sceAmprMeasureCommandSizeNop(17) == rejected); Require(sceAmprMeasureCommandSizeNopWithData(0) == rejected && sceAmprMeasureCommandSizeNopWithData(17) == rejected); + + Apr::CommandBufferObject unbound{}; + Require(sceAmprCommandBufferConstructor(&unbound) == 0); + Require(sceAmprCommandBufferNop(&unbound, 0) == permissionDenied); + Require(sceAmprCommandBufferNop(&unbound, 17) == permissionDenied); + Require(sceAmprCommandBufferNopWithData(&unbound, 16, data) == permissionDenied); + Require(sceAmprCommandBufferConstructNop(&unbound, 0, data, 61, nullptr) == permissionDenied); + Require(unbound.offset == 0 && unbound.numCommands == 0); +} + +void TestNopPadding() { + Require(sceAmprMeasureCommandSizeNop(1) == 4 && sceAmprMeasureCommandSizeNop(16) == 64); + Require(sceAmprMeasureCommandSizeNopWithData(1) == 4 && sceAmprMeasureCommandSizeNopWithData(16) == 64); + + Recorder recorder(64); + alignas(8) std::uint64_t done = 0; + Require(sceAmprCommandBufferPushMarker(&recorder.buffer, "pad") == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&recorder.buffer, &done, 1) == 0); + const std::uint32_t remaining = 64 - recorder.Offset(); + Require(remaining % 4 == 0 && remaining / 4 >= 2); + Require(sceAmprCommandBufferNop(&recorder.buffer, remaining / 4 + 1) == busy); + Require(recorder.Offset() == 64 - remaining && recorder.Commands() == 2); + Require(sceAmprCommandBufferNop(&recorder.buffer, remaining / 4) == 0); + Require(recorder.Offset() == 64 && recorder.Commands() == 3); + Require(sceAmprCommandBufferNop(&recorder.buffer, 1) == busy); + Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 0, nullptr) == busy); + Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); + Require(done == 1); + + Recorder odd; + done = 0; + Require(sceAmprCommandBufferNop(&odd.buffer, 1) == 0); + Require(odd.Offset() == 4); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&odd.buffer, &done, 2) == 0); + Require(sceAmprCommandBufferNopWithData(&odd.buffer, 2, nullptr) == 0); + Require(sceAmprCommandBufferNop(&odd.buffer, 1) == 0); + Require(odd.Offset() == 4 + sizeof(Apr::WriteAddressCommand) + 12 + 4); + Require(sceKernelAprSubmitCommandBuffer(&odd.buffer, 0) == 0); + Require(done == 2); } void TestVersionedCommands() { @@ -476,11 +531,20 @@ void TestConstructed() { const std::uint32_t word = 0xCAFEF00Du; auto offset = recorder.Offset(); auto commands = recorder.Commands(); + std::memset(recorder.memory.data() + offset, 0x5A, 20); Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 7, payload, sizeof(payload), &word) == 0); RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); - const std::uint8_t* data = recorder.memory.data() + offset + sizeof(Apr::CommandHeader); + Require(HeaderAt(recorder, offset) == 0x5452730Fu); + const std::uint8_t* data = recorder.memory.data() + offset + Apr::NopHeaderBytes; const std::uint8_t padding[3] = {}; Require(std::memcmp(data, &word, sizeof(word)) == 0 && std::memcmp(data + 4, payload, sizeof(payload)) == 0 && std::memcmp(data + 9, padding, sizeof(padding)) == 0); + Require(recorder.memory[offset + 16] == 0x5A); + offset = recorder.Offset(); + Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 0x15, payload, 4, nullptr) == 0); + Require(HeaderAt(recorder, offset) == 0x5452510Fu); + offset = recorder.Offset(); + Require(sceAmprCommandBufferConstructNop(&recorder.buffer, -1, payload, 4, nullptr) == 0); + Require(HeaderAt(recorder, offset) == 0x5452F10Fu); const std::array large{}; offset = recorder.Offset(); @@ -652,10 +716,9 @@ void TestResolveFailures() { } void TestMalformedCommandSizes() { - const auto requireFailure = [](Apr::Opcode opcode, std::uint32_t recordBytes, std::uint32_t bufferBytes, const char* expected) { + const auto requireFailure = [](const void* record, std::size_t recordBytes, std::uint32_t bufferBytes, const char* expected) { std::array memory{}; - const Apr::CommandHeader header{opcode, recordBytes}; - std::memcpy(memory.data(), &header, sizeof(header)); + std::memcpy(memory.data(), record, recordBytes); Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, bufferBytes, 1, bufferBytes, memory.data()}; bool rejected = false; try { @@ -666,14 +729,25 @@ void TestMalformedCommandSizes() { } Require(rejected); }; + const auto requireHeaderFailure = [&](Apr::Opcode opcode, std::uint32_t recordBytes, std::uint32_t bufferBytes, const char* expected) { + const Apr::CommandHeader header{opcode, recordBytes}; + requireFailure(&header, sizeof(header), bufferBytes, expected); + }; - requireFailure(Apr::Opcode::ReadFile, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); - requireFailure(Apr::Opcode::WriteAddress, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); - requireFailure(Apr::Opcode::Nop, std::numeric_limits::max(), 2u * sizeof(Apr::CommandHeader), "APR: malformed command"); + requireHeaderFailure(Apr::Opcode::ReadFile, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); + requireHeaderFailure(Apr::Opcode::WriteAddress, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); + requireHeaderFailure(Apr::Opcode::PopMarker, std::numeric_limits::max(), 2u * sizeof(Apr::CommandHeader), "APR: malformed command"); + + const std::uint32_t overflowingNop = Apr::NopHeader(15); + requireFailure(&overflowingNop, sizeof(overflowingNop), sizeof(overflowingNop), "APR: malformed command"); + const Apr::CommandHeader eightByteNop{Apr::Opcode::Nop, sizeof(Apr::CommandHeader)}; + requireFailure(&eightByteNop, sizeof(eightByteNop), sizeof(eightByteNop), "APR: unknown opcode 0"); + const std::uint32_t nops[2] = {Apr::NopHeader(0), Apr::NopHeader(0)}; + requireFailure(nops, sizeof(nops), 7u, "APR: truncated command buffer"); std::array memory{}; - const Apr::CommandHeader first{Apr::Opcode::Nop, sizeof(Apr::CommandHeader)}; - const Apr::CommandHeader overflowing{Apr::Opcode::Nop, std::numeric_limits::max()}; + const Apr::CommandHeader first{Apr::Opcode::PopMarker, sizeof(Apr::CommandHeader)}; + const Apr::CommandHeader overflowing{Apr::Opcode::PopMarker, std::numeric_limits::max()}; std::memcpy(memory.data(), &first, sizeof(first)); std::memcpy(memory.data() + sizeof(first), &overflowing, sizeof(overflowing)); Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 2, sizeof(memory), memory.data()}; @@ -1377,6 +1451,7 @@ int main() { TestCounters(); TestRejectedWaitsAndCounters(); TestNops(); + TestNopPadding(); TestVersionedCommands(); TestVersionedCounters(); TestClearBuffer(); diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index e1197f4a94..709b87bd6a 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -41,6 +41,15 @@ int main() { Require(sceAppContentAddcontDelete(0, nullptr) == notInitialized); Require(sceAppContentAddcontEnqueueDownload(0, nullptr) == notInitialized); Require(sceAppContentGetAddcontDownloadProgress(0, nullptr, nullptr) == notInitialized); + Require(sceAppContentAddcontEnqueueDownloadByEntitlemetId(nullptr) == notInitialized); + Require(sceAppContentAddcontEnqueueDownloadSp(0, nullptr) == notInitialized); + Require(sceAppContentAddcontMountByEntitlemetId(nullptr, nullptr) == notInitialized); + Require(sceAppContentAddcontShrink(0, nullptr) == notInitialized); + Require(sceAppContentGetAddcontInfoByEntitlementId(nullptr, nullptr) == notInitialized); + Require(sceAppContentGetAddcontInfoListByIroTag(0, nullptr, 0, nullptr) == notInitialized); + std::uint32_t pftFlag = 0x5a5a5a5a; + Require(sceAppContentGetPftFlag(&pftFlag) == notInitialized); + Require(pftFlag == 0x5a5a5a5a); const auto original = std::filesystem::current_path(); const auto directory = original / ("appcontent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(directory)); @@ -196,6 +205,29 @@ int main() { Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); Require(sceAppContentGetAddcontInfo(0, nullptr, reinterpret_cast(info)) == ErrorParameter); Require(sceAppContentGetAddcontInfo(0, &label, nullptr) == ErrorParameter); + const char entitlementId[] = "UP0000-PPSA00000_00-ADDCONT000000001"; + Require(sceAppContentAddcontEnqueueDownloadByEntitlemetId(entitlementId) == ErrorNotFound); + Require(sceAppContentAddcontEnqueueDownloadByEntitlemetId(nullptr) == ErrorParameter); + Require(sceAppContentAddcontEnqueueDownloadSp(0, &label) == ErrorNotFound); + Require(sceAppContentAddcontEnqueueDownloadSp(7, &label) == ErrorNotFound); + Require(sceAppContentAddcontEnqueueDownloadSp(0, nullptr) == ErrorParameter); + Require(sceAppContentAddcontShrink(0, &label) == ErrorNotFound); + Require(sceAppContentAddcontShrink(7, &label) == ErrorNotFound); + Require(sceAppContentAddcontShrink(0, nullptr) == ErrorParameter); + std::memset(&mountPoint, 0x5a, sizeof(mountPoint)); + Require(sceAppContentAddcontMountByEntitlemetId(entitlementId, &mountPoint) == ErrorNotFound); + Require(std::memcmp(&mountPoint, &untouched, sizeof(mountPoint)) == 0); + Require(sceAppContentAddcontMountByEntitlemetId(nullptr, &mountPoint) == ErrorParameter); + Require(sceAppContentAddcontMountByEntitlemetId(entitlementId, nullptr) == ErrorParameter); + Require(std::memcmp(&mountPoint, &untouched, sizeof(mountPoint)) == 0); + Require(sceAppContentGetAddcontInfoByEntitlementId(entitlementId, reinterpret_cast(info)) == ErrorDrmNoEntitlement); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(sceAppContentGetAddcontInfoByEntitlementId(nullptr, reinterpret_cast(info)) == ErrorParameter); + Require(sceAppContentGetAddcontInfoByEntitlementId(entitlementId, nullptr) == ErrorParameter); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(sceAppContentGetPftFlag(&pftFlag) == 0); + Require(pftFlag == 0); + Require(sceAppContentGetPftFlag(nullptr) == ErrorParameter); AppContentEntitlementKey key; std::memset(&key, 0x5a, sizeof(key)); const AppContentEntitlementKey untouchedKey = key; @@ -221,6 +253,10 @@ int main() { Require(sceAppContentAddcontEnqueueDownload(0, &label) == earlyError); Require(sceAppContentAddcontDelete(0, &label) == lateError); Require(sceAppContentGetAddcontDownloadProgress(0, &label, progress) == lateError); + if (version >= 0x03500000) { + Require(sceAppContentAddcontEnqueueDownloadSp(0, &label) == ErrorParameter); + Require(sceAppContentAddcontShrink(0, &label) == ErrorParameter); + } Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); Require(std::memcmp(progress, progressBefore, sizeof(progress)) == 0); padding = 0; @@ -244,6 +280,17 @@ int main() { Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, nullptr) == ErrorParameter); Require(sceAppContentGetAddcontInfoList(0, reinterpret_cast(info), 0, nullptr) == ErrorParameter); Require(sceAppContentGetAddcontInfoList(0, nullptr, 1, nullptr) == ErrorParameter); + hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoListByIroTag(1, nullptr, 0, &hitNum) == 0); + Require(hitNum == 0); + hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoListByIroTag(1, reinterpret_cast(info), 1, &hitNum) == 0); + Require(hitNum == 0); + Require(sceAppContentGetAddcontInfoListByIroTag(1, reinterpret_cast(info), 1, nullptr) == 0); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(sceAppContentGetAddcontInfoListByIroTag(1, nullptr, 0, nullptr) == ErrorParameter); + Require(sceAppContentGetAddcontInfoListByIroTag(1, reinterpret_cast(info), 0, nullptr) == ErrorParameter); + Require(sceAppContentGetAddcontInfoListByIroTag(1, nullptr, 1, nullptr) == ErrorParameter); std::filesystem::current_path(original); std::filesystem::remove_all(directory); } diff --git a/core/libs/tests/GuestAppendWrites.cpp b/core/libs/tests/GuestAppendWrites.cpp index 806611cae1..353fffb2f0 100644 --- a/core/libs/tests/GuestAppendWrites.cpp +++ b/core/libs/tests/GuestAppendWrites.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/include/Api.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -13,6 +14,9 @@ #include #include #include +#ifdef _WIN32 +#include +#endif static void Check(bool condition, int line) { if (!condition) { @@ -110,6 +114,36 @@ int main() { Require(appended.substr(0, LineLength) == head); Require(appended.substr(static_cast(size) + LineLength) == positioned + gathered); +#ifdef _WIN32 + void* unreadable = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_NOACCESS); + Require(unreadable != nullptr); + char validChar = 'x'; + const KernelIovec badPieces[2]{{&validChar, 1}, {unreadable, 10}}; + const KernelIovec badPiecesFirst[2]{{unreadable, 10}, {&validChar, 1}}; + const KernelIovec badPiecesZeroLength[2]{{unreadable, 0}, {&validChar, 1}}; + + *__error_nid_postfix() = 0; + Require(writev_nid_postfix(first, badPieces, 2) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(pwritev_nid_postfix(first, badPieces, 2, 0) == -1 && *__error_nid_postfix() == 14); + Require(sceKernelWritev(first, badPieces, 2) == static_cast(0x8002000eu)); + Require(sceKernelPwritev(first, badPieces, 2, 0) == static_cast(0x8002000eu)); + + *__error_nid_postfix() = 0; + Require(writev_nid_postfix(first, badPiecesFirst, 2) == -1 && *__error_nid_postfix() == 14); + *__error_nid_postfix() = 0; + Require(pwritev_nid_postfix(first, badPiecesFirst, 2, 0) == -1 && *__error_nid_postfix() == 14); + Require(sceKernelWritev(first, badPiecesFirst, 2) == static_cast(0x8002000eu)); + Require(sceKernelPwritev(first, badPiecesFirst, 2, 0) == static_cast(0x8002000eu)); + + Require(Contents().size() == static_cast(size) + 3 * LineLength); + + Require(writev_nid_postfix(first, badPiecesZeroLength, 2) == 1); + Require(Contents().size() == static_cast(size) + 3 * LineLength + 1); + + VirtualFree(unreadable, 0, MEM_RELEASE); +#endif + for (const int fd : descriptors) Require(sceKernelClose(fd) == 0); std::remove(Path); } diff --git a/core/libs/tests/GuestBluetoothHid.cpp b/core/libs/tests/GuestBluetoothHid.cpp index beb7f81f59..56965517eb 100644 --- a/core/libs/tests/GuestBluetoothHid.cpp +++ b/core/libs/tests/GuestBluetoothHid.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace { @@ -94,6 +95,22 @@ int main() { Require(threw, "set feature report did not throw"); Require(sceBluetoothHidUnregisterDevice(0x44f, 0x1000) == 0, "device unregistration failed"); Require(sceBluetoothHidUnregisterCallback() == 0, "callback unregistration failed"); + BluetoothHidThreadParam thread{}; + BluetoothHidParam filled; + std::memset(&filled, 0xa5, sizeof(filled)); + Require(sceBluetoothHidParamInitialize(&filled, &thread) == 0, "parameter initialization did not return 0"); + unsigned char bytes[sizeof(BluetoothHidParam)]; + std::memcpy(bytes, &filled, sizeof(bytes)); + const BluetoothHidThreadParam* stored = nullptr; + std::memcpy(&stored, bytes, sizeof(stored)); + std::uint32_t cleared = 1; + std::memcpy(&cleared, bytes + 8, sizeof(cleared)); + Require(stored == &thread, "the thread parameter was not stored at offset 0"); + Require(cleared == 0, "the word at offset 8 was not cleared"); + Require(bytes[12] == 0xa5 && bytes[13] == 0xa5 && bytes[14] == 0xa5 && bytes[15] == 0xa5, "bytes 12 to 15 of the parameter block changed"); + std::memset(&filled, 0x5a, sizeof(filled)); + Require(sceBluetoothHidParamInitialize(&filled, nullptr) == 0 && filled.threadParam == nullptr && filled.reserved == 0 && filled.padding == 0x5a5a5a5au, "a null thread parameter was not stored as given"); + Require(sceBluetoothHidParamInitialize(nullptr, &thread) == invalidArgument, "a null parameter block was not refused"); std::puts("BluetoothHid tests passed"); return 0; } diff --git a/core/libs/tests/GuestCesChinese.cpp b/core/libs/tests/GuestCesChinese.cpp index 424a2a27bd..d5c3ba5365 100644 --- a/core/libs/tests/GuestCesChinese.cpp +++ b/core/libs/tests/GuestCesChinese.cpp @@ -1,10 +1,13 @@ +#include #include "prx/libSceCesCs/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include -static void Require(bool value) { if (!value) std::abort(); } +static void RequireImpl(bool value, int line) { if (!value) { std::fprintf(stderr, "REQFAIL line %d\n", line); std::fflush(stderr); std::abort(); } } +#define Require(v) RequireImpl((v), __LINE__) static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(text); } @@ -44,24 +47,18 @@ static constexpr Gb18030Sequence GB18030_BOUNDARIES[88] = { {0x8336CF39, 0xE863, 0xE863}, {0x8336D030, 0xE865, 0xE865}, {0x84308534, 0xF92B, 0xF92B}, {0x84308535, 0xF92D, 0xF92D}, {0x84309C37, 0xFA26, 0xFA26}, {0x84309C38, 0xFA2A, 0xFA2A}, {0x84318537, 0xFE2F, 0xFE2F}, {0x84318538, 0xFE32, 0xFE32}, {0x8431A233, 0xFFDF, 0xFFDF}, {0x8431A234, 0xFFE6, 0xFFE6}, {0x8431A439, 0xFFFF, 0xFFFF}, {0x8431A530, 0x0, 0x0}, - {0x8F39FE39, 0x0, 0x0}, {0x90308130, 0x10000, 0x10000}, {0x95328235, 0x1FFFF, 0x1FFFF}, {0x95328236, 0x20000, 0x20000}, - {0x9A348431, 0x2FFFF, 0x2FFFF}, {0x9A348432, 0x30000, 0x30000}, {0x9F368537, 0x3FFFF, 0x3FFFF}, {0x9F368538, 0x40000, 0x40000}, - {0xA4388733, 0x4FFFF, 0x4FFFF}, {0xA4388734, 0x50000, 0x50000}, {0xAA308839, 0x5FFFF, 0x5FFFF}, {0xAA308930, 0x60000, 0x60000}, - {0xAF328A35, 0x6FFFF, 0x6FFFF}, {0xAF328A36, 0x70000, 0x70000}, {0xB4348C31, 0x7FFFF, 0x7FFFF}, {0xB4348C32, 0x80000, 0x80000}, - {0xB9368D37, 0x8FFFF, 0x8FFFF}, {0xB9368D38, 0x90000, 0x90000}, {0xBE388F33, 0x9FFFF, 0x9FFFF}, {0xBE388F34, 0xA0000, 0xA0000}, - {0xC4309039, 0xAFFFF, 0xAFFFF}, {0xC4309130, 0xB0000, 0xB0000}, {0xC9329235, 0xBFFFF, 0xBFFFF}, {0xC9329236, 0xC0000, 0xC0000}, - {0xCE349431, 0xCFFFF, 0xCFFFF}, {0xCE349432, 0xD0000, 0xD0000}, {0xD3369537, 0xDFFFF, 0xDFFFF}, {0xD3369538, 0xE0000, 0xE0000}, - {0xD8389733, 0xEFFFF, 0xEFFFF}, {0xD8389734, 0xF0000, 0xF0000}, {0xDE309839, 0xFFFFF, 0xFFFFF}, {0xDE309930, 0x100000, 0x100000}, - {0xE3329A35, 0x10FFFF, 0x10FFFF}, {0xE3329A36, 0x0, 0x0}, {0xFD308130, 0x0, 0x0}, {0xFE39FE39, 0x0, 0x0}, + {0x8F39FE39, 0x0, 0x0}, {0x90308130, 0x0, 0x0}, {0x95328235, 0x0, 0x0}, {0x95328236, 0x0, 0x20000}, + {0x9A348431, 0x0, 0x2FFFF}, {0x9A348432, 0x0, 0x0}, {0x9F368537, 0x0, 0x0}, {0x9F368538, 0x0, 0x0}, + {0xA4388733, 0x0, 0x0}, {0xA4388734, 0x0, 0x0}, {0xAA308839, 0x0, 0x0}, {0xAA308930, 0x0, 0x0}, + {0xAF328A35, 0x0, 0x0}, {0xAF328A36, 0x0, 0x0}, {0xB4348C31, 0x0, 0x0}, {0xB4348C32, 0x0, 0x0}, + {0xB9368D37, 0x0, 0x0}, {0xB9368D38, 0x0, 0x0}, {0xBE388F33, 0x0, 0x0}, {0xBE388F34, 0x0, 0x0}, + {0xC4309039, 0x0, 0x0}, {0xC4309130, 0x0, 0x0}, {0xC9329235, 0x0, 0x0}, {0xC9329236, 0x0, 0x0}, + {0xCE349431, 0x0, 0x0}, {0xCE349432, 0x0, 0x0}, {0xD3369537, 0x0, 0x0}, {0xD3369538, 0x0, 0x0}, + {0xD8389733, 0x0, 0x0}, {0xD8389734, 0x0, 0x0}, {0xDE309839, 0x0, 0x0}, {0xDE309930, 0x0, 0x0}, + {0xE3329A35, 0x0, 0x0}, {0xE3329A36, 0x0, 0x0}, {0xFD308130, 0x0, 0x0}, {0xFE39FE39, 0x0, 0x0}, }; -static constexpr Gb18030Difference GB18030_DIFFERENCES[26] = { - {0xA6D9, 0xE78D, 0xFE10, true}, {0xA6DA, 0xE78E, 0xFE12, true}, {0xA6DB, 0xE78F, 0xFE11, true}, {0xA6DC, 0xE790, 0xFE13, true}, - {0xA6DD, 0xE791, 0xFE14, true}, {0xA6DE, 0xE792, 0xFE15, true}, {0xA6DF, 0xE793, 0xFE16, true}, {0xA6EC, 0xE794, 0xFE17, true}, - {0xA6ED, 0xE795, 0xFE18, true}, {0xA6F3, 0xE796, 0xFE19, true}, {0xA8BC, 0xE7C7, 0x1E3F, false}, {0xFE51, 0xE816, 0x20087, true}, - {0xFE52, 0xE817, 0x20089, true}, {0xFE53, 0xE818, 0x200CC, true}, {0xFE59, 0xE81E, 0x9FB4, true}, {0xFE61, 0xE826, 0x9FB5, true}, - {0xFE66, 0xE82B, 0x9FB6, true}, {0xFE67, 0xE82C, 0x9FB7, true}, {0xFE6C, 0xE831, 0x215D7, true}, {0xFE6D, 0xE832, 0x9FB8, true}, - {0xFE76, 0xE83B, 0x2298F, true}, {0xFE7E, 0xE843, 0x9FB9, true}, {0xFE90, 0xE854, 0x9FBA, true}, {0xFE91, 0xE855, 0x241FE, true}, - {0xFEA0, 0xE864, 0x9FBB, true}, {0x8135F437, 0x1E3F, 0xE7C7, false}, +static constexpr Gb18030Difference GB18030_DIFFERENCES[2] = { + {0xA8BC, 0xE7C7, 0x1E3F, false}, {0x8135F437, 0x1E3F, 0xE7C7, false}, }; static std::uint32_t Gb18030Bytes(std::uint32_t linear) { return (0x81 + linear / 12600) << 24 | (0x30 + linear / 1260 % 10) << 16 | (0x81 + linear / 10 % 126) << 8 | (0x30 + linear % 10); @@ -92,7 +89,9 @@ static std::uint32_t Gb18030Decode(const void* gb, std::uint32_t packed) { static std::uint32_t Gb18030Encode(const void* gb, std::uint32_t code) { std::uint8_t bytes[4] = {}; std::uint32_t size = 0; - Require(sceCesUtf32ToGb(code, gb, bytes, sizeof(bytes), &size) == 0 && (size == 2 || size == 4)); + const int result = sceCesUtf32ToGb(code, gb, bytes, sizeof(bytes), &size); + if (result == unassignedCode) return 0; + Require(result == 0 && (size == 2 || size == 4)); std::uint32_t packed = 0; for (std::uint32_t i = 0; i < size; ++i) packed = packed << 8 | bytes[i]; return packed; @@ -149,10 +148,10 @@ static void CheckGb18030(const void* gb, bool revision2005, std::uint32_t twoByt Fnv(hash, Gb18030Decode(gb, Gb18030Bytes(linear))); ++samples; } - Require(hash == 0x9E77B552 && samples == 1052); + Require(hash == (revision2005 ? 0x0F212E49u : 0x9832CC85u) && samples == 1052); hash = 0x811C9DC5; for (std::uint32_t code = 0x10000; code < 0x110000; code += 997) Fnv(hash, Gb18030Encode(gb, code)); - Require(hash == 0x3D088D7E); + Require(hash == (revision2005 ? 0x2EB90A7Bu : 0x9832CC85u)); for (const Gb18030Sequence& entry : GB18030_BOUNDARIES) { const std::uint32_t code = revision2005 ? entry.code2005 : entry.code2000; @@ -188,55 +187,70 @@ static void CheckGb18030(const void* gb, bool revision2005, std::uint32_t twoByt std::uint32_t consumed = 0x55; std::uint32_t produced = 0x55; std::uint16_t ucs2 = 0xBEEF; - Require(sceCesGbToUcs2(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, &ucs2) == unassignedCode && consumed == 0x55 && ucs2 == 0xBEEF); std::uint8_t utf8[4] = {1, 2, 3, 4}; - Require(sceCesGbToUtf8(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf8, 3, &produced) == dstBufferEnd && consumed == 0x55 && produced == 0x55); - Require(std::memcmp(utf8, "\x01\x02\x03\x04", 4) == 0); - Require(sceCesGbToUtf8(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf8, 4, &produced) == 0 && consumed == 4 && produced == 4); - Require(std::memcmp(utf8, "\xF0\x90\x80\x80", 4) == 0); std::uint16_t utf16[2] = {0xBEEF, 0xBEEF}; - produced = 0x55; - Require(sceCesGbToUtf16(gb, Bytes("\xE3\x32\x9A\x35"), 4, &consumed, utf16, 1, &produced) == dstBufferEnd && produced == 0x55 && utf16[0] == 0xBEEF); - Require(sceCesGbToUtf16(gb, Bytes("\xE3\x32\x9A\x35"), 4, &consumed, utf16, 2, &produced) == 0 && produced == 2 && utf16[0] == 0xDBFF && utf16[1] == 0xDFFF); - Require(sceCesGbToUtf16be(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf16, 2, &produced) == 0 && std::memcmp(utf16, "\xD8\x00\xDC\x00", 4) == 0); - Require(sceCesMbcToUtf16le(gb, Bytes("\x94\x39\xFC\x36"), 4, &consumed, utf16, 2, &produced) == 0 && consumed == 4 && std::memcmp(utf16, "\x3D\xD8\x00\xDE", 4) == 0); + for (const char* refused : {"\x90\x30\x81\x30", "\x94\x39\xFC\x36", "\xE3\x32\x9A\x35"}) { + Require(sceCesGbToUtf8(gb, Bytes(refused), 4, &consumed, utf8, 4, &produced) == unassignedCode && consumed == 4 && produced == 0); + } + consumed = 0x55; + ucs2 = 0xBEEF; + Require(sceCesGbToUcs2(gb, Bytes("\x95\x32\x82\x36"), 4, &consumed, &ucs2) == unassignedCode); + if (revision2005) { + Require(consumed == 0x55 && ucs2 == 0xBEEF); + consumed = 0x55; + produced = 0x55; + Require(sceCesGbToUtf8(gb, Bytes("\x95\x32\x82\x36"), 4, &consumed, utf8, 3, &produced) == dstBufferEnd && consumed == 0x55 && produced == 0x55); + Require(sceCesGbToUtf8(gb, Bytes("\x95\x32\x82\x36"), 4, &consumed, utf8, 4, &produced) == 0 && consumed == 4 && produced == 4); + Require(std::memcmp(utf8, "\xF0\xA0\x80\x80", 4) == 0); + produced = 0x55; + Require(sceCesGbToUtf16(gb, Bytes("\x9A\x34\x84\x31"), 4, &consumed, utf16, 1, &produced) == dstBufferEnd && produced == 0x55 && utf16[0] == 0xBEEF); + Require(sceCesGbToUtf16(gb, Bytes("\x9A\x34\x84\x31"), 4, &consumed, utf16, 2, &produced) == 0 && produced == 2 && utf16[0] == 0xD87F && utf16[1] == 0xDFFF); + Require(sceCesGbToUtf16be(gb, Bytes("\x95\x32\x82\x36"), 4, &consumed, utf16, 2, &produced) == 0 && std::memcmp(utf16, "\xD8\x40\xDC\x00", 4) == 0); + } else { + Require(sceCesGbToUtf8(gb, Bytes("\x95\x32\x82\x36"), 4, &consumed, utf8, 4, &produced) == unassignedCode && consumed == 4 && produced == 0); + } Require(sceCesGbToUcs2(gb, Bytes("\xA2\xE3"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x20AC); std::uint8_t mbcs[4] = {1, 2, 3, 4}; produced = 0x55; - Require(sceCesUtf32ToGb(0x10000, gb, mbcs, 3, &produced) == dstBufferEnd && produced == 4 && std::memcmp(mbcs, "\x01\x02\x03\x04", 4) == 0); + Require(sceCesUtf32ToGb(0x10000, gb, mbcs, sizeof(mbcs), &produced) == unassignedCode && produced == 0); + produced = 0x55; + Require(sceCesUtf32ToGb(0x20000, gb, mbcs, 3, &produced) == (revision2005 ? dstBufferEnd : unassignedCode) && produced == (revision2005 ? 4u : 0u) && std::memcmp(mbcs, "\x01\x02\x03\x04", 4) == 0); Require(sceCesUtf32ToGb(0xD800, gb, mbcs, sizeof(mbcs), &produced) == outOfCodeSpace); Require(sceCesUtf32ToGb(0x110000, gb, mbcs, sizeof(mbcs), &produced) == invalidEncode); Require(sceCesUcs2ToGb(0x80, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 4 && std::memcmp(mbcs, "\x81\x30\x81\x30", 4) == 0); Require(sceCesUcs2ToGb(0x20AC, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\xA2\xE3", 2) == 0); - Require(sceCesUtf8ToGb(Bytes("\xF0\x9F\x98\x80"), 4, &consumed, gb, mbcs, sizeof(mbcs), &produced) == 0 && consumed == 4 && produced == 4); - Require(std::memcmp(mbcs, "\x94\x39\xFC\x36", 4) == 0); - const std::uint16_t pair[2] = {0xD83D, 0xDE00}; - Require(sceCesUtf16ToGb(pair, 2, &consumed, gb, mbcs, sizeof(mbcs), &produced) == 0 && consumed == 2 && produced == 4 && std::memcmp(mbcs, "\x94\x39\xFC\x36", 4) == 0); + Require(sceCesUtf8ToGb(Bytes("\xF0\x9F\x98\x80"), 4, &consumed, gb, mbcs, sizeof(mbcs), &produced) == unassignedCode); + const std::uint16_t pair[2] = {0xD840, 0xDC00}; + if (revision2005) Require(sceCesUtf16ToGb(pair, 2, &consumed, gb, mbcs, sizeof(mbcs), &produced) == 0 && consumed == 2 && produced == 4 && std::memcmp(mbcs, "\x95\x32\x82\x36", 4) == 0); + else Require(sceCesUtf16ToGb(pair, 2, &consumed, gb, mbcs, sizeof(mbcs), &produced) == unassignedCode); Require(sceCesUtf16ToGb(pair, 1, &consumed, gb, mbcs, sizeof(mbcs), &produced) == srcBufferEnd); alignas(8) std::uint8_t context[64] = {}; Require(sceCesMbcsUcsContextInit(context, gb) == 0); - const char* utf8Text = "A\xE4\xB8\xAD\xC2\x80\xF0\x9F\x98\x80\xEF\xB8\x90"; - const char* gbText = revision2005 ? "A\xD6\xD0\x81\x30\x81\x30\x94\x39\xFC\x36\xA6\xD9" : "A\xD6\xD0\x81\x30\x81\x30\x94\x39\xFC\x36\x84\x31\x82\x36"; - const std::uint32_t gbSize = revision2005 ? 13 : 15; + const char* utf8Text = revision2005 ? "A\xE4\xB8\xAD\xC2\x80\xF0\xA0\x80\x80\xEF\xB8\x90" : "A\xE4\xB8\xAD\xC2\x80\xEF\xB8\x90"; + const std::uint32_t utf8Size = revision2005 ? 13 : 9; + const char* gbText = revision2005 ? "A\xD6\xD0\x81\x30\x81\x30\x95\x32\x82\x36\x84\x31\x82\x36" : "A\xD6\xD0\x81\x30\x81\x30\x84\x31\x82\x36"; + const std::uint32_t gbSize = revision2005 ? 15 : 11; + const std::uint32_t units = revision2005 ? 6 : 4; std::uint8_t text[16] = {}; - Require(sceCesUtf8StrToMbcsStr(context, Bytes(utf8Text), 13, &consumed, text, sizeof(text), &produced) == 0 && consumed == 13 && produced == gbSize); + Require(sceCesUtf8StrToMbcsStr(context, Bytes(utf8Text), utf8Size, &consumed, text, sizeof(text), &produced) == 0 && consumed == utf8Size && produced == gbSize); Require(std::memcmp(text, gbText, gbSize) == 0); std::uint8_t backUtf8[16] = {}; - Require(sceCesMbcsStrToUtf8Str(context, text, gbSize, &consumed, backUtf8, sizeof(backUtf8), &produced) == 0 && consumed == gbSize && produced == 13); - Require(std::memcmp(backUtf8, utf8Text, 13) == 0); + Require(sceCesMbcsStrToUtf8Str(context, text, gbSize, &consumed, backUtf8, sizeof(backUtf8), &produced) == 0 && consumed == gbSize && produced == utf8Size); + Require(std::memcmp(backUtf8, utf8Text, utf8Size) == 0); std::uint16_t backUtf16[8] = {}; - Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, backUtf16, 8, &produced) == 0 && consumed == gbSize && produced == 6); - Require(backUtf16[0] == 'A' && backUtf16[1] == 0x4E2D && backUtf16[2] == 0x80 && backUtf16[3] == 0xD83D && backUtf16[4] == 0xDE00 && backUtf16[5] == 0xFE10); - std::uint16_t shortUtf16[4] = {}; - Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, shortUtf16, 4, &produced) == dstBufferEnd); - Require(sceCesMbcsStrGetUtf16Len(context, text, gbSize, &consumed, &produced) == 0 && consumed == gbSize && produced == 6); + Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, backUtf16, 8, &produced) == 0 && consumed == gbSize && produced == units); + Require(backUtf16[0] == 'A' && backUtf16[1] == 0x4E2D && backUtf16[2] == 0x80 && backUtf16[units - 1] == 0xFE10); + if (revision2005) Require(backUtf16[3] == 0xD840 && backUtf16[4] == 0xDC00); + std::uint16_t shortUtf16[8] = {}; + Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, shortUtf16, units - 2, &produced) == dstBufferEnd); + Require(sceCesMbcsStrGetUtf16Len(context, text, gbSize, &consumed, &produced) == 0 && consumed == gbSize && produced == units); std::uint8_t again[16] = {}; - Require(sceCesUtf16StrToMbcsStr(context, backUtf16, 6, &consumed, again, sizeof(again), &produced) == 0 && consumed == 6 && produced == gbSize); + Require(sceCesUtf16StrToMbcsStr(context, backUtf16, units, &consumed, again, sizeof(again), &produced) == 0 && consumed == units && produced == gbSize); Require(std::memcmp(again, gbText, gbSize) == 0); std::uint16_t ucs2Text[8] = {}; - Require(sceCesMbcsStrToUcs2Str(context, text, gbSize, &consumed, ucs2Text, 8, &produced) == unassignedCode); + if (revision2005) Require(sceCesMbcsStrToUcs2Str(context, text, gbSize, &consumed, ucs2Text, 8, &produced) == unassignedCode); Require(sceCesUtf8StrToMbcsStr(context, Bytes("\xC2\x80"), 2, &consumed, text, 3, &produced) == dstBufferEnd); } @@ -345,6 +359,6 @@ int main() { Require(sceCesEucCnToUcs2(gb2000, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == invalidParameter); Require(sceCesGbToUcs2(gb2005, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x4E2D); CheckGb18030(gb2000, false, 0xE61D3BD3, 0x83476043, 0x469CFCE5, 0, 0); - CheckGb18030(gb2005, true, 0x83EDDA69, 0x8D188DFC, 0x31CD0B55, 18, 24); + CheckGb18030(gb2005, true, 0xE4CF78F0, 0x8D188DFC, 0xD675EFA1, 0, 0); return 0; } diff --git a/core/libs/tests/GuestCommonDialog.cpp b/core/libs/tests/GuestCommonDialog.cpp index b0d74a3b47..c326b83ea2 100644 --- a/core/libs/tests/GuestCommonDialog.cpp +++ b/core/libs/tests/GuestCommonDialog.cpp @@ -1,6 +1,7 @@ #include "prx/libSceCommonDialog/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceCommonDialog/CommonDialog.hpp" +#include "prx/libSceCommonDialog/include/DialogUse.hpp" #include namespace { @@ -15,9 +16,17 @@ constexpr int kErrAlreadySystemInitialized = static_cast(0x80B80002); int main() { Require(!sceCommonDialogIsUsed()); + Require(!CommonDialogSystemInitialized_nid_no_patch()); Require(sceCommonDialogInitialize() == COMMON_DIALOG_OK); + Require(CommonDialogSystemInitialized_nid_no_patch()); Require(!sceCommonDialogIsUsed()); Require(sceCommonDialogInitialize() == kErrAlreadySystemInitialized); Require(sceCommonDialogInitialize() == kErrAlreadySystemInitialized); Require(!sceCommonDialogIsUsed()); + Require(CommonDialogAcquire_nid_no_patch() && sceCommonDialogIsUsed()); + Require(!CommonDialogAcquire_nid_no_patch() && sceCommonDialogIsUsed()); + CommonDialogRelease_nid_no_patch(); + Require(!sceCommonDialogIsUsed()); + Require(CommonDialogAcquire_nid_no_patch()); + CommonDialogRelease_nid_no_patch(); } diff --git a/core/libs/tests/GuestContentDelete.cpp b/core/libs/tests/GuestContentDelete.cpp index caafe4a5bd..34fb9129d7 100644 --- a/core/libs/tests/GuestContentDelete.cpp +++ b/core/libs/tests/GuestContentDelete.cpp @@ -21,8 +21,6 @@ std::string LogicErrorMessage(TAction action) { return "returned"; } -bool Contains(const std::string& text, const std::string& part) { return text.find(part) != std::string::npos; } - } int main() { @@ -42,9 +40,6 @@ int main() { Require(sceContentDeleteByPath("") == static_cast(0x809D5001u)); Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); - Require(Contains(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }), "sceContentDeleteByPath: /user/av_contents/photo/a.png is not in the host content library")); - Require(Contains(LogicErrorMessage([] { sceContentDeleteById(0x123456789); }), "sceContentDeleteById: content id 4886718345 is not in the host content library")); - Require(Contains(LogicErrorMessage([] { sceContentDeleteById(-1); }), "sceContentDeleteById: content id -1 is not in the host content library")); Require(sceContentDeleteTerminate() == 0); Require(sceContentDeleteTerminate() == static_cast(0x809D5002u)); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index ce6a1d5c0a..e303ad72f9 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -578,6 +578,15 @@ int main() { { const auto listed = root / "listed.txt"; std::ofstream(listed, std::ios::binary) << "listed"; + Require(fopen_nid_postfix(listed.string().c_str(), "wx") == nullptr && *__error_nid_postfix() == 17); + for (const char* invalidMode : {"z", "", "q"}) { + *__error_nid_postfix() = 0; + Require(fopen_nid_postfix(listed.string().c_str(), invalidMode) == nullptr && *__error_nid_postfix() == 22); + } + auto* readWrite = fopen_nid_postfix(listed.string().c_str(), "rw"); + char word[8] = {}; + Require(readWrite != nullptr && fread_nid_postfix(word, 1, 6, readWrite) == 6 && std::string(word) == "listed"); + Require(fclose_nid_postfix(readWrite) == 0); for (const char* openMode : {"r", "rb", "re"}) { auto* directoryStream = fopen_nid_postfix(root.string().c_str(), openMode); Require(directoryStream != nullptr); diff --git a/core/libs/tests/GuestFreeTypeBitmapInit.cpp b/core/libs/tests/GuestFreeTypeBitmapInit.cpp new file mode 100644 index 0000000000..f9b13b1c23 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeBitmapInit.cpp @@ -0,0 +1,48 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/include/Api.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include FT_BITMAP_H + +#include +#include +#include +#include + +using namespace FreeType; + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType bitmap init check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +int main() { + GuestBitmap guest{7, 8, -9, reinterpret_cast(&guest), 10, 11, 12, &guest}; + FT_Bitmap native{7, 8, -9, reinterpret_cast(&native), 10, 11, 12, &native}; + FT_Bitmap_Init_nid_postfix(&guest); + FT_Bitmap_Init(&native); + Require(guest.rows == native.rows && guest.width == native.width && guest.pitch == native.pitch); + Require(guest.buffer == nullptr && native.buffer == nullptr && guest.palette == nullptr && native.palette == nullptr); + Require(guest.num_grays == native.num_grays && guest.pixel_mode == native.pixel_mode && guest.palette_mode == native.palette_mode); + Require(guest.rows == 0 && guest.width == 0 && guest.pitch == 0 && guest.num_grays == 0 && guest.pixel_mode == 0 && guest.palette_mode == 0); + FT_Bitmap_Init_nid_postfix(nullptr); + + GuestLibrary library{}; + Require(FT_Init_FreeType(&library.host) == 0); + const std::vector bytes = FontFixture::BuildFont(1, {}); + GuestFace face; + face.library = &library; + Require(FT_New_Memory_Face(library.host, bytes.data(), static_cast(bytes.size()), 0, &face.host) == 0); + SyncFace(face); + face.host->glyph->bitmap.rows = 9; + face.host->glyph->bitmap.width = 17; + face.slot.bitmap.rows = 9; + FT_Bitmap_Init_nid_postfix(&face.slot.bitmap); + Require(face.slot.bitmap.rows == 0 && face.host->glyph->bitmap.rows == 0 && face.host->glyph->bitmap.width == 0); + Require(FT_Done_Face(face.host) == 0); + Require(FT_Done_FreeType(library.host) == 0); +} diff --git a/core/libs/tests/GuestFreeTypeCurveBounds.cpp b/core/libs/tests/GuestFreeTypeCurveBounds.cpp new file mode 100644 index 0000000000..d10151a47c --- /dev/null +++ b/core/libs/tests/GuestFreeTypeCurveBounds.cpp @@ -0,0 +1,139 @@ +#include "prx/libSceFreeType/include/Api.hpp" +#include "prx/libSceFreeType/include/GuestFreeType.hpp" + +#include +#include FT_BBOX_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "FreeType curve bounds check failed at line %u\n", location.line()); + std::abort(); + } +} + +bool Equal(const GuestBBox& a, const GuestBBox& b) { + return a.xMin == b.xMin && a.yMin == b.yMin && a.xMax == b.xMax && a.yMax == b.yMax; +} + +struct GuardedBox { + std::uint64_t before = 0x123456789ABCDEF0; + GuestBBox box{101, -102, 103, -104}; + std::uint64_t after = 0xFEDCBA9876543210; +}; + +GuestBBox Check(std::vector points, std::vector tags, + std::vector contours, std::int32_t flags = 0, bool failure = false) { + Require(points.size() == tags.size() && points.size() <= 65535 && contours.size() <= 65535); + const auto originalPoints = points; + const auto originalTags = tags; + const auto originalContours = contours; + std::vector nativePoints; + for (const auto& point : points) nativePoints.push_back({static_cast(point.x), static_cast(point.y)}); + auto nativeTags = tags; + auto nativeContours = contours; + FT_Outline native{static_cast(contours.size()), static_cast(points.size()), + nativePoints.data(), nativeTags.data(), nativeContours.data(), flags}; + GuestOutline guest{native.n_contours, native.n_points, points.data(), tags.data(), contours.data(), flags}; + const auto original = guest; + FT_BBox expected{101, -102, 103, -104}; + GuardedBox actual; + const auto status = FT_Outline_Get_BBox(&native, &expected); + Require(FT_Outline_Get_BBox_nid_postfix(&guest, &actual.box) == status); + Require((status != 0) == failure); + Require(Equal(actual.box, {expected.xMin, expected.yMin, expected.xMax, expected.yMax})); + Require(actual.before == 0x123456789ABCDEF0 && actual.after == 0xFEDCBA9876543210); + Require(guest.n_points == original.n_points && guest.n_contours == original.n_contours && guest.flags == flags); + Require(guest.points == original.points && guest.tags == original.tags && guest.contours == original.contours); + Require(tags == originalTags && contours == originalContours); + for (std::size_t index = 0; index < points.size(); ++index) + Require(points[index].x == originalPoints[index].x && points[index].y == originalPoints[index].y); + return actual.box; +} + +int main() { + GuardedBox invalid; + const auto sentinel = invalid.box; + FT_BBox native{}; + Require(FT_Outline_Get_BBox_nid_postfix(nullptr, &invalid.box) == FT_Outline_Get_BBox(nullptr, &native)); + Require(Equal(invalid.box, sentinel)); + Require(FT_Outline_Get_BBox_nid_postfix(reinterpret_cast(std::uintptr_t{1}), nullptr) == FT_Outline_Get_BBox(nullptr, nullptr)); + Check({}, {}, {}); + GuestOutline empty{0, 3, reinterpret_cast(std::uintptr_t{1}), + reinterpret_cast(std::uintptr_t{1}), reinterpret_cast(std::uintptr_t{1}), 123}; + Require(FT_Outline_Get_BBox_nid_postfix(&empty, &invalid.box) == 0 && Equal(invalid.box, {})); + empty.n_contours = 1; + empty.n_points = 0; + Require(FT_Outline_Get_BBox_nid_postfix(&empty, &invalid.box) == 0 && Equal(invalid.box, {})); + Require(Equal(Check({{-20, 30}, {40, -50}, {70, 80}}, {1, 1, 1}, {2}), {-20, -50, 70, 80})); + Require(Equal(Check({{0, 0}, {100, 200}, {200, 0}}, {1, 0, 1}, {2}), {0, 0, 200, 100})); + Require(Equal(Check({{0, 0}, {0, 400}, {400, 400}, {400, 0}}, {1, 2, 2, 1}, {3}), {0, 0, 400, 300})); + Check({{0, 0}, {100, 200}, {200, 200}, {300, 0}}, {1, 0, 0, 1}, {3}, -1); + Check({{-100, 0}, {0, 200}, {100, 0}, {0, -200}}, {0, 0, 0, 0}, {3}); + Check({{0, 0}, {100, 200}, {200, 0}, {-30, -40}, {-20, -50}, {10, -10}}, {1, 0, 1, 1, 1, 1}, {2, 5}); + Check({{0, 100}, {100, 0}, {200, 0}}, {2, 1, 1}, {2}, 0, true); + Check({{0, 0}, {100, 200}}, {1, 2}, {1}, 0, true); + Check({{std::numeric_limits::min(), 1}, {std::numeric_limits::max(), -1}}, {1, 1}, {1}); + constexpr std::int64_t curveLimit = std::numeric_limits::max() / 4; + Require(Equal(Check({{-curveLimit, -curveLimit}, {0, curveLimit}, {curveLimit, -curveLimit}}, {1, 0, 1}, {2}), + {-curveLimit, -curveLimit, curveLimit, 0})); + + std::mt19937 random(0xB0A1D5); + for (unsigned iteration = 0; iteration < 4096; ++iteration) { + std::vector points; + for (unsigned index = 0; index < 4; ++index) + points.push_back({static_cast(random() % 100001) - 50000, static_cast(random() % 100001) - 50000}); + Check(points, {1, 2, 2, 1}, {3}); + points.pop_back(); + Check(points, {1, 0, 1}, {2}); + } + std::vector many(65535); + std::vector tags(many.size(), 1); + std::vector contours(many.size()); + for (std::size_t index = 0; index < many.size(); ++index) { + many[index] = {static_cast(index % 1000), static_cast(index)}; + contours[index] = static_cast(index); + } + Require(Equal(Check(many, tags, {65534}), {0, 0, 999, 65534})); + Require(Equal(Check(many, tags, contours), {0, 0, 999, 65534})); + + std::array points{{{0, 0}, {1, 1}}}; + std::array smallTags{1, 1}; + std::uint16_t end = 1; + GuestOutline missing{1, 2, nullptr, smallTags.data(), &end, 0}; + invalid.box = sentinel; + bool threw = false; + try { + FT_Outline_Get_BBox_nid_postfix(&missing, &invalid.box); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && Equal(invalid.box, sentinel)); + for (auto badContours : std::vector>{{2}, {0}, {1, 1}, {1, 0}, {0, 1, 1}}) { + GuestOutline malformed{static_cast(badContours.size()), 2, + points.data(), smallTags.data(), badContours.data(), 0}; + threw = false; + try { + FT_Outline_Get_BBox_nid_postfix(&malformed, &invalid.box); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).starts_with("FT_Outline_Get_BBox: inconsistent contour bounds"); + } + Require(threw && Equal(invalid.box, sentinel)); + } + if constexpr (sizeof(FT_Pos) >= sizeof(std::int64_t)) { + Check({{std::int64_t{1} << 40, -(std::int64_t{1} << 40)}, {0, 0}}, {1, 1}, {1}); + } + std::printf("FreeType curve bounds: 8192 generated curves, extrema, errors and count limits passed\n"); +} diff --git a/core/libs/tests/GuestFreeTypeModuleProperties.cpp b/core/libs/tests/GuestFreeTypeModuleProperties.cpp new file mode 100644 index 0000000000..adbdd36225 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeModuleProperties.cpp @@ -0,0 +1,170 @@ +#include "prx/libSceFreeType/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include FT_FREETYPE_H +#include FT_MODULE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +using FreeType::GuestLibrary; + +struct Api { + int (APS5_VABI *initialize)(GuestLibrary**); + int (APS5_VABI *done)(GuestLibrary*); + int (APS5_VABI *get)(GuestLibrary*, const char*, const char*, void*); + int (APS5_VABI *set)(GuestLibrary*, const char*, const char*, const void*); +}; + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Module property check failed at line %u\n", location.line()); + std::abort(); + } +} + +template +static auto NativePointer(TValue& value) { + if constexpr (requires { value.data(); }) return value.data(); + else return &value; +} + +template +static void Query(const Api& api, GuestLibrary* library, FT_Library native, const char* module, const char* property) { + TValue expected{}; + std::memset(&expected, 0xA5, sizeof(expected)); + const int expectedError = FT_Property_Get(native, module, property, NativePointer(expected)); + std::array storage{}; + storage.fill(0xA5); + const auto before = storage; + auto* output = storage.data() + 7; + Require(api.get(library, module, property, output) == expectedError); + if (expectedError) Require(storage == before); + else { + Require(std::memcmp(output, &expected, sizeof(expected)) == 0); + Require(std::all_of(storage.begin(), storage.begin() + 7, [](auto byte) { return byte == 0xA5; })); + Require(std::all_of(storage.begin() + 7 + sizeof(expected), storage.end(), [](auto byte) { return byte == 0xA5; })); + } +} + +template +static void Change(const Api& api, GuestLibrary* library, FT_Library native, GuestLibrary* other, + FT_Library nativeOther, const char* module, const char* property, TValue value) { + std::array input{}; + input.fill(0xCC); + std::memcpy(input.data() + 5, &value, sizeof(value)); + const auto before = input; + const int expectedError = FT_Property_Set(native, module, property, NativePointer(value)); + Require(api.set(library, module, property, input.data() + 5) == expectedError); + Require(input == before); + Query(api, library, native, module, property); + Query(api, other, nativeOther, module, property); +} + +static void Run(const Api& api) { + GuestLibrary* library = nullptr; + GuestLibrary* other = nullptr; + FT_Library native = nullptr; + FT_Library nativeOther = nullptr; + Require(api.initialize(&library) == 0 && api.initialize(&other) == 0); + Require(FT_Init_FreeType(&native) == 0 && FT_Init_FreeType(&nativeOther) == 0); + Query(api, library, native, "truetype", "interpreter-version"); + for (const auto version : {35u, 40u, 38u, 0u, 0xFFFFFFFFu}) { + Change(api, library, native, other, nativeOther, "truetype", "interpreter-version", version); + } + const std::array, 5> parameters{{ + {500, 300, 1000, 200, 1500, 100, 2000, 0}, + {600, 250, 1100, 150, 1600, 50, 2500, 0}, + {-1, 0, 1000, 0, 1500, 0, 2000, 0}, + {1000, 0, 500, 0, 1500, 0, 2000, 0}, + {500, 501, 1000, 0, 1500, 0, 2000, 0} + }}; + for (const char* module : {"cff", "type1", "t1cid", "autofitter"}) { + Query(api, library, native, module, "no-stem-darkening"); + for (const auto value : {0u, 1u, 2u, 255u}) { + Change(api, library, native, other, nativeOther, module, "no-stem-darkening", static_cast(value)); + } + Query>(api, library, native, module, "darkening-parameters"); + for (const auto& value : parameters) Change(api, library, native, other, nativeOther, module, "darkening-parameters", value); + if (std::strcmp(module, "autofitter") == 0) continue; + for (const auto value : {1u, 0u, 2u, 0xFFFFFFFFu}) { + Change(api, library, native, other, nativeOther, module, "hinting-engine", value); + } + for (const auto value : {std::numeric_limits::min(), -1, 0, 1, std::numeric_limits::max()}) { + Change(api, library, native, other, nativeOther, module, "random-seed", value); + } + } + std::array output{}; + output.fill(0xA5); + const auto before = output; + Require(api.get(nullptr, reinterpret_cast(1), reinterpret_cast(1), output.data()) == + FT_Property_Get(nullptr, reinterpret_cast(1), reinterpret_cast(1), output.data())); + Require(api.set(nullptr, reinterpret_cast(1), reinterpret_cast(1), reinterpret_cast(1)) == + FT_Property_Set(nullptr, reinterpret_cast(1), reinterpret_cast(1), reinterpret_cast(1))); + Require(api.get(library, nullptr, "interpreter-version", output.data()) == FT_Property_Get(native, nullptr, "interpreter-version", output.data())); + Require(api.get(library, "truetype", nullptr, output.data()) == FT_Property_Get(native, "truetype", nullptr, output.data())); + Require(api.get(library, reinterpret_cast(1), reinterpret_cast(1), nullptr) == + FT_Property_Get(native, reinterpret_cast(1), reinterpret_cast(1), nullptr)); + Require(api.set(library, nullptr, "interpreter-version", output.data()) == FT_Property_Set(native, nullptr, "interpreter-version", output.data())); + Require(api.set(library, "truetype", nullptr, output.data()) == FT_Property_Set(native, "truetype", nullptr, output.data())); + Require(api.set(library, reinterpret_cast(1), reinterpret_cast(1), nullptr) == + FT_Property_Set(native, reinterpret_cast(1), reinterpret_cast(1), nullptr)); + Require(output == before); + Query(api, library, native, "truetype", "interpreter-version"); + Query(api, other, nativeOther, "truetype", "interpreter-version"); + Require(api.done(library) == 0 && api.done(other) == 0); + Require(FT_Done_FreeType(native) == 0 && FT_Done_FreeType(nativeOther) == 0); +} + +template +static TFunction Import(TModule module, const char* name, const char* nid, bool patched) { + const std::string symbol = patched ? nid : std::string(name) + "_nid_postfix"; +#ifdef _WIN32 + const auto address = GetProcAddress(module, symbol.c_str()); +#else + const auto address = dlsym(module, symbol.c_str()); +#endif + Require(address != nullptr); + return std::bit_cast(address); +} + +int main(int argc, char** argv) { + Require(argc == 3); + const bool patched = std::strcmp(argv[2], "patched") == 0; + Require(patched || std::strcmp(argv[2], "unpatched") == 0); +#ifdef _WIN32 + auto module = LoadLibraryW(std::filesystem::path(argv[1]).c_str()); + if (!module) std::fprintf(stderr, "LoadLibraryW failed: %lu\n", GetLastError()); +#else + auto module = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + if (!module) std::fprintf(stderr, "%s\n", dlerror()); +#endif + Require(module != nullptr); + Api api{}; + api.initialize = Import(module, "FT_Init_FreeType", "GNsiLzm4SH8", patched); + api.done = Import(module, "FT_Done_FreeType", "dH02CpEEcYk", patched); + api.get = Import(module, "FT_Property_Get", "8fAMdZCnewA", patched); + api.set = Import(module, "FT_Property_Set", "LsB538+IPrE", patched); + Run(api); +#ifdef _WIN32 + Require(FreeLibrary(module) != 0); +#else + Require(dlclose(module) == 0); +#endif +} diff --git a/core/libs/tests/GuestFreeTypeSfnt.cpp b/core/libs/tests/GuestFreeTypeSfnt.cpp new file mode 100644 index 0000000000..c28e3c0d42 --- /dev/null +++ b/core/libs/tests/GuestFreeTypeSfnt.cpp @@ -0,0 +1,307 @@ +#include "FontFixture.hpp" +#include "prx/libSceFreeType/libSceFreeType.hpp" +#include "prx/libSceFreeType/include/GuestObjects.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" + +#include FT_TRUETYPE_TABLES_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace FreeType; + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "FreeType SFNT check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +struct FacePair { + std::vector bytes; + GuestFaceRec* guest = nullptr; + FT_Face native = nullptr; + + FacePair(GuestLibrary* library, FT_Library nativeLibrary, std::vector data) : bytes(std::move(data)) { + Require(FT_New_Memory_Face_nid_postfix(library, bytes.data(), static_cast(bytes.size()), 0, &guest) == 0); + Require(FT_New_Memory_Face(nativeLibrary, bytes.data(), static_cast(bytes.size()), 0, &native) == 0); + } + + ~FacePair() { + Require(FT_Done_Face_nid_postfix(guest) == 0 && FT_Done_Face(native) == 0); + } +}; + +static std::uint32_t Read32(const std::vector& bytes, std::size_t offset) { + std::uint32_t value = 0; + for (unsigned index = 0; index < 4; ++index) value = (value << 8) | bytes.at(offset + index); + return value; +} + +static void InfoMatches(GuestFaceRec* guest, FT_Face native, std::uint32_t index, bool withTag, bool withLength, int& comparisons) { + constexpr std::uint64_t sentinel = 0xFEDCBA9876543210ull; + std::array output{sentinel, sentinel, sentinel, sentinel}; + FT_ULong tag = 0; + FT_ULong length = 0; + const auto error = FT_Sfnt_Table_Info(native, index, withTag ? &tag : nullptr, withLength ? &length : nullptr); + Require(FT_Sfnt_Table_Info_nid_postfix(guest, index, withTag ? &output[1] : nullptr, withLength ? &output[2] : nullptr) == error); + Require(output[0] == sentinel && output[3] == sentinel); + Require(output[1] == (error == 0 && withTag ? static_cast(tag) : sentinel)); + Require(output[2] == (error == 0 && withLength ? static_cast(length) : sentinel)); + if (withTag && withLength) { + FT_ULong alias = 0; + std::uint64_t guestAlias = sentinel; + Require(FT_Sfnt_Table_Info(native, index, &alias, &alias) == error); + Require(FT_Sfnt_Table_Info_nid_postfix(guest, index, &guestAlias, &guestAlias) == error); + Require(guestAlias == (error == 0 ? static_cast(alias) : sentinel)); + } + ++comparisons; +} + +static FT_Error ReadMatches(GuestFaceRec* guest, FT_Face native, std::uint32_t tag, std::int64_t offset, std::uint64_t requested, bool withLength, int& comparisons) { + std::array actual{}; + actual.fill(0xA5); + auto expected = actual; + std::uint64_t actualLength = requested; + FT_ULong nativeLength = static_cast(std::min(requested, static_cast(std::numeric_limits::max()))); + const auto nativeOffset = static_cast(withLength && requested == 0 ? 0 : offset); + const auto error = FT_Load_Sfnt_Table(native, tag, nativeOffset, expected.data() + 8, withLength ? &nativeLength : nullptr); + Require(FT_Load_Sfnt_Table_nid_postfix(guest, tag, offset, actual.data() + 8, withLength ? &actualLength : nullptr) == error); + Require(actual == expected); + Require(actualLength == (withLength && requested == 0 && error == 0 ? static_cast(nativeLength) : requested)); + if (guest) Require(FaceOf(guest)->host->stream->pos == native->stream->pos); + ++comparisons; + return error; +} + +static FT_ULong VirtualRead(FT_Stream stream, FT_ULong offset, unsigned char* buffer, FT_ULong count) { + if (offset >= stream->size || !buffer) return 0; + const auto bytes = std::min(count, stream->size - offset); + for (FT_ULong index = 0; index < bytes; ++index) buffer[index] = static_cast((offset + index) ^ 0x5A); + return bytes; +} + +static void WideStreamAccess(FacePair& face, int& comparisons) { + if constexpr (sizeof(FT_Long) < sizeof(std::int64_t)) { + FT_Stream stream = FaceOf(face.guest)->host->stream; + FT_Stream native = face.native->stream; + const auto original = stream->size; + const auto originalNative = native->size; + const auto originalRead = stream->read; + const auto originalNativeRead = native->read; + stream->size = native->size = static_cast(std::numeric_limits::max()) + 64; + Require(ReadMatches(face.guest, face.native, 0, std::numeric_limits::max(), 0, true, comparisons) == 0); + Require(ReadMatches(face.guest, face.native, 0, 0, 1, true, comparisons) == 0); + Require(ReadMatches(face.guest, face.native, 0x54455354u, 0, 1, true, comparisons) == 0); + stream->read = native->read = VirtualRead; + const auto offset = static_cast(std::numeric_limits::max()) + 6; + Require(ReadMatches(face.guest, face.native, 0, offset, 3, true, comparisons) == 0); + std::array pixels{}; + std::uint64_t length = pixels.size(); + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, 0, offset, pixels.data(), &length) == 0 && length == pixels.size()); + for (std::size_t index = 0; index < pixels.size(); ++index) Require(pixels[index] == static_cast((offset + static_cast(index)) ^ 0x5A)); + stream->size = original; + native->size = originalNative; + stream->read = originalRead; + native->read = originalNativeRead; + } +} + +struct WatchedMetadataPages { + std::uintptr_t tag; + std::uintptr_t length; + bool tagWritten = false; + bool lengthWritten = false; + + static void Mark(void* context, std::uintptr_t begin, std::uintptr_t end) { + auto& pages = *static_cast(context); + if (begin <= pages.tag && pages.tag < end) pages.tagWritten = true; + if (begin <= pages.length && pages.length < end) pages.lengthWritten = true; + } +}; + +static void WatchedMetadata(FacePair& face) { + if (!GuestArena::GuestArenaAvailable_nid_postfix()) { + std::puts("FreeType SFNT watched metadata: guest arena unavailable, skipped"); + return; + } +#ifdef _WIN32 + if (!GuestArena::GuestArenaWriteWatched_nid_postfix()) { + std::puts("FreeType SFNT watched metadata: write watch unavailable, skipped"); + return; + } +#else + if (!GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) { + std::puts("FreeType SFNT watched metadata: write watch unavailable, skipped"); + return; + } +#endif + constexpr std::size_t span = 0x10000; + auto* block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(span, span)); + GuestArena::GuestArenaCommit_nid_postfix(block, span, 0x04, span); + auto* tag = reinterpret_cast(block + 0x100); + auto* length = reinterpret_cast(block + 0x4100); + constexpr std::uint64_t sentinel = 0xFEDCBA9876543210ull; + *tag = sentinel; + *length = sentinel; +#ifndef _WIN32 + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, span); + Require(GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(block), span)); +#endif + const auto collect = [&]() { + WatchedMetadataPages dirty{reinterpret_cast(tag), reinterpret_cast(length)}; +#ifdef _WIN32 + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(block), span, pages.data(), &count, true)); + for (std::size_t index = 0; index < count; ++index) { + const auto begin = reinterpret_cast(pages[index]); + WatchedMetadataPages::Mark(&dirty, begin, begin + 0x1000); + } +#else + Require(GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(reinterpret_cast(block), span, WatchedMetadataPages::Mark, &dirty)); +#endif + return dirty; + }; + const auto expect = [&](bool tagWritten, bool lengthWritten) { + const auto dirty = collect(); + Require(dirty.tagWritten == tagWritten && dirty.lengthWritten == lengthWritten); + }; + collect(); + expect(false, false); + FT_ULong nativeTag = 0; + FT_ULong nativeLength = 0; + Require(FT_Sfnt_Table_Info(face.native, 0, &nativeTag, &nativeLength) == 0); + Require(FT_Sfnt_Table_Info_nid_postfix(face.guest, 0, tag, length) == 0); + Require(*tag == nativeTag && *length == nativeLength); + expect(true, true); + FT_ULong nativeAlias = 0; + Require(FT_Sfnt_Table_Info(face.native, 0, &nativeAlias, &nativeAlias) == 0); + { + GuestArena::HostWrite write(tag, sizeof(*tag)); + *tag = sentinel; + } + collect(); + expect(false, false); + Require(FT_Sfnt_Table_Info_nid_postfix(face.guest, 0, tag, tag) == 0 && *tag == nativeAlias); + expect(true, false); + { + GuestArena::HostWrite write(length, sizeof(*length)); + *length = 0; + } + collect(); + expect(false, false); + FT_ULong queriedLength = 0; + Require(FT_Load_Sfnt_Table(face.native, nativeTag, 0, nullptr, &queriedLength) == 0); + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, nativeTag, 0, reinterpret_cast(length), length) == 0); + Require(*length == queriedLength); + expect(false, true); + const auto retainedTag = *tag; + const auto retainedLength = *length; + Require(FT_Sfnt_Table_Info_nid_postfix(nullptr, 0, tag, length) == FT_Err_Invalid_Face_Handle); + Require(*tag == retainedTag && *length == retainedLength); + expect(false, false); +#ifndef _WIN32 + Require(GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, span)); +#endif + GuestArena::GuestArenaReset_nid_postfix(block, span); + GuestArena::GuestArenaRelease_nid_postfix(block, span); + std::puts("FreeType SFNT watched metadata: split outputs, alias and zero-length query recorded"); +} + +int main() { + GuestLibrary* library = nullptr; + FT_Library nativeLibrary = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&nativeLibrary) == 0); + int information = 0; + int reads = 0; + { + const auto font = FontFixture::BuildFont(1, {{"TEST", {11, 22, 33, 44, 55, 66, 77}}, + {"TAIL", {7, 6, 5, 4, 3, 2, 1, 0, 255, 254, 253, 252, 251, 250, 249}}, {"ZERO", {}}, + {std::string("\xF1\x23\xAB\xCD", 4), {19, 29, 39, 49, 59, 69, 79, 89}}}); + FacePair face(library, nativeLibrary, font); + WatchedMetadata(face); + const unsigned tableCount = (static_cast(font[4]) << 8) | font[5]; + Require(font.size() + 16 < 1024); + std::uint64_t count = 0; + Require(FT_Sfnt_Table_Info_nid_postfix(face.guest, std::numeric_limits::max(), nullptr, &count) == 0 && count == tableCount); + for (const auto index : {0u, tableCount - 1, tableCount, std::numeric_limits::max()}) { + for (const bool withTag : {false, true}) { + for (const bool withLength : {false, true}) { + InfoMatches(face.guest, face.native, index, withTag, withLength, information); + InfoMatches(nullptr, nullptr, index, withTag, withLength, information); + } + } + } + for (unsigned index = 0; index < tableCount; ++index) { + const std::size_t directory = 12 + index * 16; + const auto tag = Read32(font, directory); + const auto size = Read32(font, directory + 12); + std::uint64_t actualTag = 0; + std::uint64_t actualLength = 0; + Require(FT_Sfnt_Table_Info_nid_postfix(face.guest, index, &actualTag, &actualLength) == 0 && actualTag == tag && actualLength == size); + InfoMatches(face.guest, face.native, index, true, true, information); + for (const auto offset : {0ll, 1ll, -1ll, static_cast(size), static_cast(size + 1), 2147483647ll, -2147483648ll}) { + ReadMatches(face.guest, face.native, tag, offset, 0, true, reads); + ReadMatches(face.guest, face.native, tag, offset, size, false, reads); + for (const auto requested : {1ull, 3ull, static_cast(size), static_cast(size + 3)}) { + ReadMatches(face.guest, face.native, tag, offset, requested, true, reads); + } + } + ReadMatches(face.guest, face.native, tag, std::numeric_limits::max(), 0, true, reads); + ReadMatches(face.guest, face.native, tag, std::numeric_limits::min(), 0, true, reads); + } + for (const auto offset : {0ll, 1ll, -1ll, static_cast(font.size()), static_cast(font.size() - 3)}) { + for (const auto requested : {0ull, 1ull, 16ull, static_cast(font.size()), std::numeric_limits::max()}) { + ReadMatches(face.guest, face.native, 0, offset, requested, true, reads); + } + ReadMatches(face.guest, face.native, 0, offset, 0, false, reads); + } + Require(ReadMatches(face.guest, face.native, 0, static_cast(font.size() - 3), 8, true, reads) != 0); + std::array alias{}; + alias.fill(0xA5A5A5A5A5A5A5A5ull); + alias[0] = 16; + auto nativeAlias = alias; + FT_ULong nativeAliasLength = 16; + Require(FT_Load_Sfnt_Table(face.native, 0, 0, reinterpret_cast(nativeAlias.data()), &nativeAliasLength) == 0); + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, 0, 0, reinterpret_cast(alias.data()), &alias[0]) == 0); + Require(alias == nativeAlias && std::memcmp(alias.data(), font.data(), 16) == 0 && alias[2] == 0xA5A5A5A5A5A5A5A5ull); + const auto missing = ReadMatches(face.guest, face.native, 0x4D495353u, 0, 0, true, reads); + Require(missing == FT_Err_Table_Missing); + for (const auto tag : {0x100000000ull, 0x168656164ull, std::numeric_limits::max()}) { + std::uint64_t length = 7; + unsigned char byte = 0xA5; + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, tag, 0, &byte, &length) == missing && length == 7 && byte == 0xA5); + Require(FT_Load_Sfnt_Table_nid_postfix(nullptr, tag, 0, &byte, &length) == FT_Err_Invalid_Face_Handle); + } + for (const auto offset : std::array{std::numeric_limits::min(), std::numeric_limits::max(), -2147483649ll, 2147483648ll}) { + std::uint64_t length = 1; + unsigned char byte = 0xA5; + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, 0, offset, &byte, &length) == FT_Err_Invalid_Stream_Operation && byte == 0xA5 && length == 1); + Require(FT_Load_Sfnt_Table_nid_postfix(face.guest, 0x4D495353u, offset, nullptr, nullptr) == missing); + } + const std::string bdf = "STARTFONT 2.1\nFONT -anyps5-sfnt-medium-r-normal--1-10-75-75-c-10-iso10646-1\nSIZE 1 75 75\n" + "FONTBOUNDINGBOX 1 1 0 0\nSTARTPROPERTIES 4\nFONT_ASCENT 1\nFONT_DESCENT 0\nCHARSET_REGISTRY \"ISO10646\"\nCHARSET_ENCODING \"1\"\n" + "ENDPROPERTIES\nCHARS 1\nSTARTCHAR A\nENCODING 65\nSWIDTH 1000 0\nDWIDTH 1 0\nBBX 1 1 0 0\nBITMAP\n80\nENDCHAR\nENDFONT\n"; + FacePair bitmap(library, nativeLibrary, {bdf.begin(), bdf.end()}); + Require(!FT_IS_SFNT(bitmap.native)); + InfoMatches(bitmap.guest, bitmap.native, 0, true, true, information); + InfoMatches(bitmap.guest, bitmap.native, 0, false, false, information); + ReadMatches(bitmap.guest, bitmap.native, 0, 0, 0, true, reads); + ReadMatches(bitmap.guest, bitmap.native, 0, 0, 0, false, reads); + ReadMatches(nullptr, nullptr, 0, 0, 0, true, reads); + WideStreamAccess(face, reads); + } + std::printf("FreeType SFNT: %d directory comparisons, %d read comparisons; wide inputs, aliases and short-read bytes checked\n", information, reads); + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(nativeLibrary) == 0); +} diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index 236c8b5dc9..420d5bd24d 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -246,6 +246,9 @@ static void SettimeValidatesLikeFreebsd() { const KernelTimespec valid{1767225600, 5}; RequireSettimeError(GUEST_CLOCK_REALTIME, nullptr, 14); RequireSettimeError(GUEST_CLOCK_MONOTONIC, nullptr, 14); + const auto unreadable = reinterpret_cast(1); + RequireSettimeError(GUEST_CLOCK_REALTIME, unreadable, 14); + RequireSettimeError(GUEST_CLOCK_MONOTONIC, unreadable, 14); for (const int clockId : {GUEST_CLOCK_VIRTUAL, GUEST_CLOCK_PROF, GUEST_CLOCK_MONOTONIC, GUEST_CLOCK_REALTIME_PRECISE, GUEST_CLOCK_SECOND, GUEST_CLOCK_THREAD_CPUTIME_ID, GUEST_CLOCK_PROCESS_CPUTIME_ID, -1, 16}) RequireSettimeError(clockId, &valid, 22); diff --git a/core/libs/tests/GuestG2PDialog.cpp b/core/libs/tests/GuestG2PDialog.cpp index afb2800b64..b0c5cc8adc 100644 --- a/core/libs/tests/GuestG2PDialog.cpp +++ b/core/libs/tests/GuestG2PDialog.cpp @@ -79,7 +79,13 @@ int main() { Require(sceG2PDialogClose() == static_cast(0x80B8000Bu)); Require(sceG2PDialogTerminate() == 0); Require(sceG2PDialogTerminate() == kErrNotInitialized); + Require(sceG2PDialogOpen(nullptr) == static_cast(0x80B8000Du)); + Require(sceG2PDialogOpen(¶m) == kErrNotInitialized); + Require(sceG2PDialogClose() == kErrNotInitialized); + Require(sceG2PDialogGetResult(nullptr) == static_cast(0x80B8000Du)); + Require(sceG2PDialogGetResult(reinterpret_cast(result)) == kErrNotInitialized); Require(sceG2PDialogGetStatus() == kStatusNone); + Require(sceG2PDialogUpdateStatus() == kStatusNone); Require(sceG2PDialogInitialize() == 0); Require(sceG2PDialogGetStatus() == kStatusInitialized); @@ -90,5 +96,13 @@ int main() { Require(result[0] == -1 && result[1] == -1 && result[3] == -1); Require(result[2] == -1); Require(sceG2PDialogTerminate() == 0); + Require(sceG2PDialogTerminate() == kErrNotInitialized); + Require(sceG2PDialogOpen(nullptr) == static_cast(0x80B8000Du)); + Require(sceG2PDialogOpen(¶m) == kErrNotInitialized); + Require(sceG2PDialogClose() == kErrNotInitialized); + Require(sceG2PDialogGetResult(nullptr) == static_cast(0x80B8000Du)); + Require(sceG2PDialogGetResult(reinterpret_cast(result)) == kErrNotInitialized); + Require(sceG2PDialogGetStatus() == kStatusNone); + Require(sceG2PDialogUpdateStatus() == kStatusNone); Require(sceUserServiceTerminate() == 0); } diff --git a/core/libs/tests/GuestGameCustomDataDialog.cpp b/core/libs/tests/GuestGameCustomDataDialog.cpp index cdf6474337..6c9228eeab 100644 --- a/core/libs/tests/GuestGameCustomDataDialog.cpp +++ b/core/libs/tests/GuestGameCustomDataDialog.cpp @@ -65,4 +65,12 @@ int main() { Require(sceGameCustomDataDialogGetResultA(result) == 0); Require(result[0] == kResultUserCanceled); Require(sceGameCustomDataDialogTerminate() == 0); + Require(sceGameCustomDataDialogTerminate() == kErrNotInitialized); + Require(sceGameCustomDataDialogOpenA(nullptr) == kErrNotInitialized); + Require(sceGameCustomDataDialogOpenA(¶m) == kErrNotInitialized); + Require(sceGameCustomDataDialogClose() == kErrNotInitialized); + Require(sceGameCustomDataDialogGetResultA(nullptr) == kErrNotInitialized); + Require(sceGameCustomDataDialogGetResultA(result) == kErrNotInitialized); + Require(sceGameCustomDataDialogGetStatus() == kStatusNone); + Require(sceGameCustomDataDialogUpdateStatus() == kStatusNone); } diff --git a/core/libs/tests/GuestHevcMetadata.cpp b/core/libs/tests/GuestHevcMetadata.cpp new file mode 100644 index 0000000000..55775f20db --- /dev/null +++ b/core/libs/tests/GuestHevcMetadata.cpp @@ -0,0 +1,802 @@ +#include "prx/libSceVideodec2/HevcMetadata.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "HevcFixture.hpp" +#include "HevcMetadataFixture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +using namespace SceVideodec2; + + +namespace { + +constexpr std::uint32_t codecHevc = 974921; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "HEVC metadata: %s\n", message); + std::abort(); +} + +template +void Rejects(TCall call, const char* message) { + try { call(); } + catch (const std::runtime_error&) { return; } + Require(false, message); +} + +template +bool TryProjection(TCall call) { + try { call(); return true; } + catch (const std::runtime_error&) { return false; } +} + +int QueryPicture(unsigned getter, const OutputInfo* output, void* first, void* second = nullptr) { + switch (getter) { + case 0: return sceVideodec2GetPictureInfo_nid_postfix(output, static_cast(first), second); + case 1: return sceVideodec2GetAvcPictureInfo_nid_postfix(output, static_cast(first), static_cast(second)); + case 2: return sceVideodec2GetHevcPictureInfo_nid_postfix(output, static_cast(first)); + default: return sceVideodec2GetVp9PictureInfo_nid_postfix(output, static_cast(first)); + } +} + +void ArgumentErrors() { + constexpr int pointerError = static_cast(0x811D0102u); + constexpr int sizeError = static_cast(0x811D0101u); + constexpr int alignmentError = static_cast(0x811D0108u); + constexpr int bufferError = static_cast(0x811D0107u); + constexpr int countError = static_cast(0x811D010Fu); + using Bytes = std::array; + const auto initialize = [](Bytes& bytes) { + bytes.fill(0xa5); + const std::uint64_t size = sizeof(Videodec2Hevc::PictureInfo); + std::memcpy(bytes.data() + 16, &size, sizeof(size)); + }; + for (unsigned getter = 0; getter < 4; ++getter) { + const bool generic = getter < 2; + const auto query = [getter](const OutputInfo* output, void* first, void* second) { + return QueryPicture(getter, output, first, second); + }; + for (const std::uint32_t codec : {codecHevc, 1u, 2382845u, 0u}) { + OutputInfo valid{sizeof(OutputInfo)}; + valid.codecType = codec; + valid.frameBuffer = reinterpret_cast(0x100000); + valid.frameBufferSize = 0x10000; + valid.pictureCount = 1; + Bytes first{}, second{}; + initialize(first); + initialize(second); + const auto secondBefore = second; + Require(query(nullptr, nullptr, reinterpret_cast(1)) == pointerError, "null first must return before touching second"); + Require(query(&valid, nullptr, second.data() + 16) == pointerError && second == secondBefore, "null first mutated second"); + const auto check = [&](const OutputInfo* output, int expected) { + initialize(first); + initialize(second); + auto expectedFirst = first; + auto expectedSecond = second; + std::memset(expectedFirst.data() + 24, 0, 4); + if (generic) std::memset(expectedSecond.data() + 24, 0, 4); + Require(query(output, first.data() + 16, second.data() + 16) == expected, "native picture argument result/order"); + Require(first == expectedFirst && second == expectedSecond, "native argument rejection must only clear validity words"); + }; + check(nullptr, pointerError); + for (const std::uint64_t size : {0u, 0x28u, 0x40u}) { + auto invalid = valid; + invalid.thisSize = size; + invalid.frameBuffer = reinterpret_cast(0x100001); + invalid.pictureCount = 0; + check(&invalid, sizeError); + invalid.frameBuffer = nullptr; + check(&invalid, sizeError); + } + for (const std::uintptr_t offset : {1u, 0x80u}) { + auto invalid = valid; + invalid.frameBuffer = reinterpret_cast(0x100000 + offset); + invalid.pictureCount = 0; + invalid.frameBufferSize = 0x400; + check(&invalid, alignmentError); + } + auto invalid = valid; + invalid.frameBuffer = nullptr; + invalid.pictureCount = 0; + check(&invalid, bufferError); + for (const std::uint8_t count : {0, 3, 255}) { + invalid = valid; + invalid.pictureCount = count; + invalid.frameBufferSize = 0x400; + check(&invalid, countError); + invalid.thisSize = 0x30; + invalid.frameBuffer = reinterpret_cast(0x100100); + check(&invalid, countError); + } + initialize(first); + auto both = first; + std::memset(both.data() + 24, 0, 4); + Require(query(nullptr, first.data() + 16, first.data() + 16) == pointerError && first == both, "identical metadata destinations"); + alignas(OutputInfo) std::array aliased{}; + std::memcpy(aliased.data() + 16, &valid, sizeof(valid)); + auto* output = reinterpret_cast(aliased.data() + 16); + Require(query(output, aliased.data() + 8, nullptr) == sizeError && output->thisSize == 0, "argument fields must be read after aliased validity clear"); + if (generic) { + initialize(first); + auto outputCopy = valid; + auto* secondAlias = reinterpret_cast(&outputCopy.frameBuffer) - 8; + Require(query(&outputCopy, first.data() + 16, secondAlias) == bufferError && outputCopy.frameBuffer == nullptr, "second early clear must precede frame-buffer validation"); + } + } + } +} + +void ArgumentSizes() { + constexpr int sizeError = static_cast(0x811D0101u); + constexpr int bufferError = static_cast(0x811D0106u); + using Bytes = std::array; + alignas(256) std::array backing{}; + for (unsigned getter = 0; getter < 4; ++getter) { + for (const std::uint32_t codec : {codecHevc, 1u, 2382845u, 0u}) { + for (const std::uint64_t outputSize : {0x30u, 0x38u}) { + OutputInfo output{outputSize}; + output.codecType = codec; + output.frameBuffer = backing.data(); + Bytes first{}, second{}; + const auto initialize = [&](std::uint64_t firstSize, std::uint64_t secondSize) { + first.fill(0xa5); + second.fill(0x5a); + std::memcpy(first.data() + 16, &firstSize, sizeof(firstSize)); + std::memcpy(second.data() + 16, &secondSize, sizeof(secondSize)); + }; + const auto check = [&](int expected, bool withSecond = true) { + auto expectedFirst = first; + auto expectedSecond = second; + std::memset(expectedFirst.data() + 24, 0, 4); + if (getter < 2 && withSecond) std::memset(expectedSecond.data() + 24, 0, 4); + Require(QueryPicture(getter, &output, first.data() + 16, withSecond ? second.data() + 16 : nullptr) == expected, "native buffer/version error order"); + Require(first == expectedFirst && second == expectedSecond, "buffer/version error changed more than validity words"); + }; + for (const std::uint8_t count : {1, 2}) { + output.pictureCount = count; + const auto boundary = std::uint64_t{count} * 0x400; + for (const auto bytes : {std::uint64_t{0}, boundary - 1, boundary}) { + for (const bool withSecond : {false, true}) { + output.frameBufferSize = bytes; + initialize(0, 0x60); + check(bufferError, withSecond); + } + } + for (const auto bytes : {boundary + 1, (std::uint64_t{1} << 63) + 0x400, UINT64_MAX}) { + output.frameBufferSize = bytes; + initialize(0, 0xb8); + check(sizeError); + for (const std::uint64_t rejected : {0u, 0x38u, 0x50u, 0x60u, 0x98u, 0xb0u, 0xc8u}) { + initialize(rejected, 0xb8); + check(sizeError); + } + for (const std::uint64_t accepted : {0x58u, 0x68u, 0x78u, 0x88u, 0xa8u, 0xb8u}) { + initialize(accepted, 0x60); + if (count == 2 && getter < 2) { + check(sizeError); + } else if (bytes == boundary + 1) { + auto expectedFirst = first; + auto expectedSecond = second; + std::memset(expectedFirst.data() + 24, 0, 4); + if (getter < 2) std::memset(expectedSecond.data() + 24, 0, 4); + const bool completed = TryProjection([&] { QueryPicture(getter, &output, first.data() + 16, second.data() + 16); }); + Require(std::memcmp(first.data(), expectedFirst.data(), 16) == 0 && std::memcmp(first.data() + 16 + accepted, expectedFirst.data() + 16 + accepted, first.size() - 16 - accepted) == 0, "accepted-version output crossed its declared boundary"); + if (!completed) Require(first == expectedFirst && second == expectedSecond, "unsupported projection changed more than validity words"); + } + } + } + } + if (getter < 2) { + output.pictureCount = 1; + output.frameBufferSize = 0x401; + initialize(0xb8, 0xb8); + auto expected = first; + std::memset(expected.data() + 16, 0, 4); + std::memset(expected.data() + 24, 0, 4); + Require(QueryPicture(getter, &output, first.data() + 16, first.data() + 8) == sizeError && first == expected, "second alias must be cleared before destination version read"); + initialize(0xb8, 0xb8); + output.frameBufferSize = 0x401; + auto* sizeAlias = reinterpret_cast(&output.frameBufferSize) - 8; + Require(QueryPicture(getter, &output, first.data() + 16, sizeAlias) == bufferError && output.frameBufferSize == 0, "buffer-size gate must read after secondary validity clear"); + output.pictureCount = 2; + output.frameBufferSize = 0x801; + for (const std::uint64_t secondSize : {0x58u, 0x68u, 0x78u, 0x88u, 0xa8u, 0xb8u}) { + initialize(0xb8, secondSize); + auto beforeFirst = first; + auto beforeSecond = second; + const bool completed = TryProjection([&] { QueryPicture(getter, &output, first.data() + 16, second.data() + 16); }); + Require(std::memcmp(first.data(), beforeFirst.data(), 16) == 0 && std::memcmp(first.data() + 16 + 0xb8, beforeFirst.data() + 16 + 0xb8, 16) == 0, "two-picture primary output canary"); + Require(std::memcmp(second.data(), beforeSecond.data(), 16) == 0 && std::memcmp(second.data() + 16 + secondSize, beforeSecond.data() + 16 + secondSize, second.size() - 16 - secondSize) == 0, "two-picture secondary output canary"); + if (!completed) { + std::memset(beforeFirst.data() + 24, 0, 4); + std::memset(beforeSecond.data() + 24, 0, 4); + Require(first == beforeFirst && second == beforeSecond, "unsupported two-picture projection changed more than validity words"); + } + } + initialize(0xb8, 0); + TryProjection([&] { QueryPicture(getter, &output, first.data() + 16); }); + } + } + } + } +} + +struct Decoder { + std::uint64_t handle = 0; + DecoderMemoryInfo memory{sizeof(memory)}; + std::vector storage; + std::span buffer; + FrameBuffer frame{sizeof(frame)}; + OutputInfo output{sizeof(output)}; + + explicit Decoder(unsigned profile) { + const DecoderConfigInfo config{sizeof(config), 1, codecHevc, profile, 0, 64, 48, 4, 1}; + Require(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "query memory"); + Require(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "create decoder"); + buffer = HevcFixture::AlignedFrameStorage(storage, memory.maxFrameBufferSize, memory.frameBufferAlignment); + frame.frameBuffer = buffer.data(); + frame.frameBufferSize = buffer.size(); + } + + ~Decoder() { if (handle) sceVideodec2DeleteDecoder_nid_postfix(handle); } + + void Decode(const std::vector& bytes, std::uint64_t tag) { + const InputData input{sizeof(input), bytes.data(), bytes.size(), 1000 + tag, 2000 + tag, 3000 + tag}; + Require(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "decode unit"); + } + + Videodec2Hevc::PictureInfo Query() { + struct Guarded { + Videodec2Hevc::PictureInfo info{sizeof(info)}; + std::uint64_t guard = 0x123456789abcdef0; + } result; + Require(sceVideodec2GetHevcPictureInfo_nid_postfix(&output, &result.info) == 0, "query HEVC info"); + Require(result.info.thisSize == sizeof(result.info) && result.info.isValid, "valid HEVC picture"); + Require(result.guard == 0x123456789abcdef0, "HEVC info overwrote adjacent memory"); + Videodec2Hevc::PictureInfo generic{sizeof(generic)}; + Require(sceVideodec2GetPictureInfo_nid_postfix(&output, reinterpret_cast(&generic), nullptr) == 0, "generic HEVC query"); + Require(generic.ptsData == result.info.ptsData && generic.dtsData == result.info.dtsData && generic.attachedData == result.info.attachedData && generic.width == result.info.width && generic.timeScale == result.info.timeScale && generic.spsPresent == result.info.spsPresent, "generic and dedicated info disagree"); + return result.info; + } +}; + +struct GuestMetadataMapping { + static constexpr std::size_t block = 0x10000; + unsigned char* memory = nullptr; + bool watched = false; +#ifdef _WIN32 + HANDLE section = nullptr; +#endif + + GuestMetadataMapping() { + Require(GuestArena::GuestArenaAvailable_nid_postfix(), "guest arena unavailable"); + memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(2 * block, block)); + Require(memory && GuestArena::GuestArenaContains_nid_postfix(memory, 2 * block), "allocate guest metadata mapping"); +#ifdef _WIN32 + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, 2 * block, nullptr); + Require(section != nullptr, "create guest metadata section"); + GuestArena::GuestArenaMap_nid_postfix(memory, 2 * block, section, 0, PAGE_READWRITE); +#else + GuestArena::GuestArenaCommit_nid_postfix(memory, 2 * block, 0x04, block); +#endif + watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix(); + if (watched) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, 2 * block); + } + + ~GuestMetadataMapping() { + if (watched) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, 2 * block); + Protect(memory, 2 * block, 0x04); + GuestArena::GuestArenaReset_nid_postfix(memory, 2 * block); + GuestArena::GuestArenaRelease_nid_postfix(memory, 2 * block); +#ifdef _WIN32 + GuestArena::GuestArenaForgetSection_nid_postfix(section); + Require(CloseHandle(section), "close guest metadata section"); +#endif + } + + Videodec2Hevc::PictureInfo* Info() const { + return reinterpret_cast(memory + block - 96); + } + + void Protect(void* pointer, std::size_t bytes, std::uint32_t protection) { +#ifdef _WIN32 + auto cursor = reinterpret_cast(pointer); + const auto end = cursor + bytes; + while (cursor < end) { + MEMORY_BASIC_INFORMATION region{}; + Require(VirtualQuery(reinterpret_cast(cursor), ®ion, sizeof(region)) == sizeof(region), "query guest metadata section"); + const auto regionEnd = reinterpret_cast(region.BaseAddress) + region.RegionSize; + Require(region.State == MEM_COMMIT && regionEnd > cursor, "guest metadata section is not committed"); + const auto span = std::min(end - cursor, regionEnd - cursor); + DWORD previous = 0; + Require(VirtualProtect(reinterpret_cast(cursor), span, protection, &previous), "protect guest metadata section"); + cursor += span; + } +#endif + GuestArena::GuestArenaSetProtection_nid_postfix(reinterpret_cast(pointer), bytes, protection); + } + + void Initialize() { + Protect(memory, 2 * block, 0x04); + GuestArena::HostWrite access(memory, 2 * block); + Require(access.Open(), "initialize guest metadata access"); + std::memset(memory, 0xa7, 2 * block); + Info()->thisSize = sizeof(*Info()); + Info()->isValid = true; + } + + std::size_t Collect() const { + std::size_t writes = 0; + if (watched) Require(GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(reinterpret_cast(memory), 2 * block, + [](void* context, std::uintptr_t begin, std::uintptr_t end) { *static_cast(context) += end - begin; }, &writes), "collect guest metadata writes"); + return writes; + } +}; + +void GuestArgumentWrites() { + GuestMetadataMapping mapping; + auto* second = reinterpret_cast(mapping.memory + GuestMetadataMapping::block + 32); + constexpr int pointerError = static_cast(0x811D0102u); + for (unsigned getter = 0; getter < 4; ++getter) { + const bool generic = getter < 2; + const auto query = [&] { + return QueryPicture(getter, nullptr, mapping.Info(), second); + }; + mapping.Initialize(); + auto expected = std::vector(mapping.memory, mapping.memory + 2 * GuestMetadataMapping::block); + std::memset(expected.data() + GuestMetadataMapping::block - 96 + 8, 0, 4); + if (generic) std::memset(expected.data() + GuestMetadataMapping::block + 32 + 8, 0, 4); + mapping.Collect(); + Require(query() == pointerError, "guest-mapped argument error result"); + Require(std::memcmp(mapping.memory, expected.data(), expected.size()) == 0, "guest-mapped argument prefix changed more than validity words"); + if (mapping.watched) Require(mapping.Collect() != 0, "guest argument prefix write not reported"); +#ifdef _WIN32 + for (const std::size_t block : {0u, 1u}) { + if (!generic && block == 1) continue; + mapping.Initialize(); + expected.assign(mapping.memory, mapping.memory + 2 * GuestMetadataMapping::block); + mapping.Protect(mapping.memory + block * GuestMetadataMapping::block, GuestMetadataMapping::block, 0x02); + Rejects(query, "read-only early argument destination did not refuse"); + Require(std::memcmp(mapping.memory, expected.data(), expected.size()) == 0, "rejected early scope partially cleared either destination"); + } +#endif + } +} + +void SecondaryVersionNoRead() { + GuestMetadataMapping mapping; + auto* second = mapping.memory + GuestMetadataMapping::block - 8; + OutputInfo output{sizeof(output)}; + alignas(256) std::array backing{}; + output.frameBuffer = backing.data(); + output.frameBufferSize = 0x401; + output.pictureCount = 1; + for (unsigned getter = 0; getter < 2; ++getter) { + mapping.Initialize(); + Videodec2Hevc::PictureInfo info{sizeof(info)}; + auto expected = std::vector(mapping.memory, mapping.memory + 2 * GuestMetadataMapping::block); + std::memset(expected.data() + GuestMetadataMapping::block, 0, 4); + mapping.Protect(mapping.memory, GuestMetadataMapping::block, 0x01); + TryProjection([&] { QueryPicture(getter, &output, &info, second); }); + mapping.Protect(mapping.memory, GuestMetadataMapping::block, 0x04); + Require(std::memcmp(mapping.memory, expected.data(), expected.size()) == 0, "count-one secondary version was accessed beyond validity word"); + } +} + +void GuestMetadataWrites() { + Decoder decoder(1); + decoder.Decode(HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()).front(), 91); + if (!decoder.output.isValid) Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "flush mapped metadata fixture"); + Require(decoder.output.isValid, "mapped metadata fixture produced no picture"); + const auto expected = decoder.Query(); + GuestMetadataMapping mapping; + for (unsigned getter = 0; getter < 4; ++getter) { + for (const bool owned : {false, true}) { + auto output = decoder.output; + if (!owned) output.frameBuffer = nullptr; + const auto query = [&] { + return QueryPicture(getter, &output, mapping.Info()); + }; + mapping.Initialize(); + auto* first = reinterpret_cast(mapping.Info()) - 16; + std::array initial{}; + std::memcpy(initial.data(), first, initial.size()); + mapping.Collect(); + Require(query() == (owned ? 0 : static_cast(0x811D0107u)), "guest-mapped metadata query"); + if (owned) { + Require(mapping.Info()->isValid && mapping.Info()->thisSize == sizeof(expected) && + mapping.Info()->ptsData == expected.ptsData && mapping.Info()->attachedData == expected.attachedData && + mapping.Info()->width == expected.width && mapping.Info()->profile == expected.profile, "guest-mapped metadata value"); + Require(std::memcmp(first, initial.data(), 16) == 0 && + std::memcmp(first + 16 + sizeof(expected), initial.data() + 16 + sizeof(expected), 16) == 0, "guest metadata canaries"); + } else { + std::memset(initial.data() + 16 + offsetof(Videodec2Hevc::PictureInfo, isValid), 0, 4); + Require(std::memcmp(first, initial.data(), initial.size()) == 0, "invalid-picture mapping changes only validity word"); + } + if (mapping.watched) Require(mapping.Collect() != 0, "guest metadata write not reported"); +#ifdef _WIN32 + mapping.Initialize(); + std::memcpy(initial.data(), first, initial.size()); + const auto refused = mapping.memory + (owned ? GuestMetadataMapping::block : 0); + mapping.Protect(refused, GuestMetadataMapping::block, 0x02); + { + GuestArena::HostWrite access(owned ? static_cast(mapping.Info()) : static_cast(&mapping.Info()->isValid), + owned ? sizeof(*mapping.Info()) : sizeof(std::uint32_t)); + Require(!access.Open(), "read-only metadata mapping unexpectedly writable"); + } + Rejects(query, "read-only metadata query did not reject access"); + if (owned) std::memset(initial.data() + 16 + offsetof(Videodec2Hevc::PictureInfo, isValid), 0, 4); + Require(std::memcmp(first, initial.data(), initial.size()) == 0, "rejected payload write changed more than its early validity word"); +#endif + } + } +} + +void CheckProjectionWrites(Decoder& decoder); + +void CheckCommon(const Videodec2Hevc::PictureInfo& info, unsigned bitDepth) { + Require(info.width == 64 && info.height == 40, "coded luma dimensions"); + Require(info.bitDepthLumaMinus8 == bitDepth && info.bitDepthChromaMinus8 == bitDepth, "source bit depths"); + Require(info.timingInfoPresent == 1 && info.numUnitsInTick == 1 && info.timeScale == 30, "VUI timing syntax"); + Require(info.aspectRatioInfoPresent == 1 && info.aspectRatioIdc == 1 && info.sarWidth == 1 && info.sarHeight == 1, "VUI sample aspect ratio"); + Require(info.videoSignalTypePresent == 1 && info.videoFormat == 5 && info.fullRange == 0 && info.colourDescriptionPresent == 0 && info.colourPrimaries == 2 && info.transferCharacteristics == 2 && info.matrixCoefficients == 2, "signalled video format and inferred colours"); + Require(info.frameFieldInfoPresent == 0 && info.picStruct == 0 && info.sourceScanType == 1 && info.duplicate == 0, "PTL progressive scan inference"); + Require(info.defaultDisplayWindow == 0 && info.displayLeft == 0 && info.displayRight == 0 && info.displayTop == 0 && info.displayBottom == 0, "default display window inference"); + Require(info.chromaLocInfoPresent == 0 && info.chromaLocTop == 0 && info.chromaLocBottom == 0 && info.fieldSequence == 0, "inferred chroma location and field sequence"); + Require(info.profileSpace == 0 && info.tier == 0 && info.profile == (bitDepth == 0 ? 1 : 2) && info.level == 30 && info.progressiveSource == 1 && info.interlacedSource == 0 && info.frameOnlyConstraint == 1, "general profile-tier-level syntax"); + Require(info.subLayerProfilePresent == 0 && info.subLayerLevelPresent == 0 && info.subLayerProfileSpace == 0 && info.subLayerTier == 0 && info.subLayerProfile == 0 && info.subLayerLevel == 0 && info.subLayerOrderingPresent == 1 && info.maxDecPicBufferingMinus1 == 4, "single-layer PTL and DPB syntax"); + Require(info.cropping == 0 && info.cropLeft == 0 && info.cropRight == 0 && info.cropTop == 0 && info.cropBottom == 0, "absent tail cropping fields"); +} + +void Clips(bool alternativeTransfer = false) { + Decoder decoder(1); + std::size_t pictures = 0; + const auto take = [&] { + if (!decoder.output.isValid) return; + const auto info = decoder.Query(); + CheckCommon(info, 0); + constexpr std::array order{0, 3, 2, 1, 5, 4}; + Require(pictures < order.size(), "too many pictures"); + const auto tag = order[pictures]; + Require(info.ptsData == 1000 + tag && info.dtsData == 2000 + tag && info.attachedData == 3000 + tag, "per-picture access-unit association through B-frame reordering"); + Require(info.vpsPresent == (pictures == 0) && info.spsPresent == (pictures == 0) && info.ppsPresent == (pictures == 0) && info.delimiterPresent == 1, "presence is from the emitted picture's unit"); + Require(info.conformanceWindow == 0 && info.confLeft == 0 && info.confRight == 0 && info.confTop == 0 && info.confBottom == 0, "no conformance window"); + Require(info.idr == (pictures == 0) && info.irap == (pictures == 0) && info.fillerPresent == 0, "per-picture IDR/IRAP and filler flags"); + Require(info.alternativeTransferPresent == (alternativeTransfer && pictures == 0) && info.preferredTransfer == (alternativeTransfer ? 16 : 0), "alternative transfer persists through B-frame reordering"); + ++pictures; + }; + auto units = HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()); + if (alternativeTransfer) { + const std::array sei{0, 0, 1, 0x4e, 1, 147, 1, 16, 0x80}; + units[0].insert(units[0].begin() + 7, sei.begin(), sei.end()); + } + for (std::size_t unit = 0; unit < units.size(); ++unit) { + decoder.Decode(units[unit], unit); + take(); + } + for (std::size_t drain = 0; drain <= units.size(); ++drain) { + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "drain clip"); + if (!decoder.output.isValid) break; + take(); + } + Require(pictures == 6, "decoded all six picture metadata records"); + Videodec2Hevc::PictureInfo invalid{sizeof(invalid)}; + invalid.isValid = true; + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset"); + Require(sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &invalid) == static_cast(0x811D010Fu) && !invalid.isValid, "reset output has no picture count"); + invalid.thisSize = 8; + invalid.isValid = true; + Require(sceVideodec2GetPictureInfo_nid_postfix(&decoder.output, reinterpret_cast(&invalid), nullptr) == static_cast(0x811D010Fu) && !invalid.isValid, "picture-count error must precede destination size"); + decoder.Decode(units.front(), 99); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode after reset"); + Require(decoder.Query().attachedData == 3099, "new metadata after reset"); +} + +void Single(std::span stream, unsigned bitDepth, unsigned bottomCrop, bool delimiter) { + Decoder decoder(bitDepth == 0 ? 1 : 2); + decoder.Decode({stream.begin(), stream.end()}, 7); + if (!decoder.output.isValid) Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "single-picture drain"); + if (bottomCrop != 0) { + Require(decoder.output.isValid, "cropped picture remains decodable"); + CheckProjectionWrites(decoder); + return; + } + const auto info = decoder.Query(); + CheckCommon(info, bitDepth); + Require(info.attachedData == 3007 && info.vpsPresent == 1 && info.spsPresent == 1 && info.ppsPresent == 1 && info.delimiterPresent == delimiter, "single-picture tags and presence"); + Require(info.idr == 1 && info.irap == 1 && info.preferredTransfer == 0 && info.pictureTimingPresent == 0, "single-picture tail metadata"); + Require(info.conformanceWindow == (bottomCrop != 0) && info.confBottom == bottomCrop && info.confLeft == 0 && info.confRight == 0 && info.confTop == 0, "raw chroma-unit conformance offsets"); + for (unsigned getter = 0; getter < 4; ++getter) { + alignas(8) std::array unaligned; + unaligned.fill(0xa5); + const std::uint64_t nativeSize = sizeof(info); + std::memcpy(unaligned.data() + 17, &nativeSize, sizeof(nativeSize)); + const auto guarded = unaligned; + Require(QueryPicture(getter, &decoder.output, unaligned.data() + 17) == 0, "unaligned metadata destination"); + Videodec2Hevc::PictureInfo aligned{}; + std::memcpy(&aligned, unaligned.data() + 17, sizeof(aligned)); + Require(aligned.isValid && aligned.ptsData == info.ptsData && aligned.width == info.width, "unaligned metadata values"); + Require(std::memcmp(unaligned.data(), guarded.data(), 17) == 0 && std::memcmp(unaligned.data() + 17 + sizeof(info), guarded.data() + 17 + sizeof(info), 15) == 0, "unaligned metadata canaries"); + for (const std::uint32_t codec : {codecHevc, 1u, 2382845u, 0u}) { + for (const std::uint64_t size : {0x30u, 0x38u}) { + auto output = decoder.output; + output.codecType = codec; + output.thisSize = size; + Videodec2Hevc::PictureInfo projected{sizeof(projected)}; + std::array second; + second.fill(0xa5); + const std::uint64_t secondSize = 0x60; + std::memcpy(second.data(), &secondSize, sizeof(secondSize)); + auto expected = second; + if (getter < 2) std::memset(expected.data() + 8, 0, 4); + Require(QueryPicture(getter, &output, &projected, second.data()) == 0, "all named wrappers must project owned HEVC metadata after native validation"); + Require(projected.thisSize == sizeof(projected) && projected.isValid && projected.ptsData == info.ptsData && projected.width == info.width, "mutable codec tag changed tracked metadata projection"); + Require(second == expected, "count-one secondary output must only clear its validity word"); + } + } + } +} + +void UnsupportedPicture() { + Decoder decoder(1); + auto units = HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()); + const std::array unknownSei{0, 0, 1, 0x4e, 1, 200, 1, 0x42, 0x80}; + units[0].insert(units[0].begin() + 7, unknownSei.begin(), unknownSei.end()); + decoder.Decode(units[0], 0); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "unsupported metadata retains decoded output"); + Videodec2Hevc::PictureInfo untouched; + std::memset(&untouched, 0x5a, sizeof(untouched)); + untouched.thisSize = sizeof(untouched); + std::array before; + std::memcpy(before.data(), &untouched, before.size()); + std::memset(before.data() + 8, 0, 4); + const bool typedCompleted = TryProjection([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &untouched); }); + if (!typedCompleted) Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "unsupported query changed more than validity word"); + std::memset(&untouched, 0x5a, sizeof(untouched)); + untouched.thisSize = sizeof(untouched); + const bool genericCompleted = TryProjection([&] { sceVideodec2GetPictureInfo_nid_postfix(&decoder.output, reinterpret_cast(&untouched), nullptr); }); + if (!genericCompleted) Require(std::memcmp(before.data(), &untouched, before.size()) == 0, "generic unsupported query changed more than validity word"); + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "clear unsupported metadata on reset"); + const auto clean = HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()); + decoder.Decode(clean[0], 80); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "clean replacement picture"); + Require(decoder.Query().ptsData == 1080, "replacement metadata does not retain unsupported reason"); + const auto previous = decoder.output; + Require(sceVideodec2DeleteDecoder_nid_postfix(decoder.handle) == 0, "delete owner"); + decoder.handle = 0; + untouched = {sizeof(untouched)}; + untouched.isValid = true; + const bool completed = TryProjection([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&previous, &untouched); }); + if (!completed) Require(!untouched.isValid, "unsupported untracked metadata retained validity"); +} + +std::vector Replacement(unsigned wanted) { + const auto& original = HevcFixture::HevcMain10Stream; + std::vector starts; + for (std::size_t index = 0; index + 2 < original.size(); ++index) + if (original[index] == 0 && original[index + 1] == 0 && original[index + 2] == 1) starts.push_back(index + 3); + for (std::size_t index = 0; index < starts.size(); ++index) { + const auto header = starts[index]; + if (((original[header] >> 1) & 63) != wanted) continue; + const auto end = index + 1 == starts.size() ? original.size() : starts[index + 1] - 3; + std::vector bits; + unsigned zeros = 0; + for (std::size_t byte = header + 2; byte < end; ++byte) { + const auto value = original[byte]; + if (zeros == 2 && value == 3) { zeros = 0; continue; } + for (int bit = 7; bit >= 0; --bit) bits.push_back(((value >> bit) & 1) != 0); + zeros = value == 0 ? zeros + 1 : 0; + } + while (!bits.empty() && !bits.back()) bits.pop_back(); + Require(bits.size() >= 3, "parameter-set fixture trailing bits"); + bits.pop_back(); + Require(!bits.back(), "fixture extension flag is zero"); + if (wanted == 32) { + bits.pop_back(); + Require(!bits.back(), "fixture VPS timing flag is zero"); + bits.pop_back(); + bits.push_back(true); + for (const unsigned value : std::array{1u, 30u}) + for (int bit = 31; bit >= 0; --bit) bits.push_back(((value >> bit) & 1) != 0); + bits.insert(bits.end(), {false, true, false}); + } else { + bits.back() = true; + bits.insert(bits.end(), 8, false); + } + bits.push_back(true); + while (bits.size() % 8 != 0) bits.push_back(false); + std::vector encoded; + zeros = 0; + for (std::size_t byte = 0; byte < bits.size(); byte += 8) { + std::uint8_t value = 0; + for (unsigned bit = 0; bit < 8; ++bit) value = static_cast((value << 1) | bits[byte + bit]); + if (zeros == 2 && value <= 3) { encoded.push_back(3); zeros = 0; } + encoded.push_back(value); + zeros = value == 0 ? zeros + 1 : 0; + } + std::vector result(original.begin(), original.begin() + header + 2); + result.insert(result.end(), encoded.begin(), encoded.end()); + result.insert(result.end(), original.begin() + end, original.end()); + return result; + } + Require(false, "fixture parameter set not found"); + return {}; +} + +void CheckProjectionWrites(Decoder& decoder) { + Videodec2Hevc::PictureInfo info; + std::memset(&info, 0x6b, sizeof(info)); + info.thisSize = sizeof(info); + std::array before; + std::memcpy(before.data(), &info, sizeof(info)); + std::memset(before.data() + 8, 0, 4); + const bool completed = TryProjection([&] { sceVideodec2GetHevcPictureInfo_nid_postfix(&decoder.output, &info); }); + if (!completed) Require(std::memcmp(before.data(), &info, sizeof(info)) == 0, "ambiguous replacement query changed more than validity word"); +} + +void ParameterReplacements() { + const std::vector original(HevcFixture::HevcMain10Stream.begin(), HevcFixture::HevcMain10Stream.end()); + for (const unsigned type : std::array{0u, 32u, 33u, 34u}) { + Decoder decoder(2); + decoder.Decode(original, 10); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "initial replacement-test picture"); + Require(decoder.Query().attachedData == 3010, "initial parameter metadata"); + const auto previous = decoder.output; + std::vector nextStorage; + const auto nextBuffer = HevcFixture::AlignedFrameStorage(nextStorage, decoder.buffer.size(), decoder.memory.frameBufferAlignment); + decoder.frame.frameBuffer = nextBuffer.data(); + decoder.Decode(type == 0 ? original : Replacement(type), 20); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode repeated or replaced parameter sets"); + if (type == 0) Require(decoder.Query().attachedData == 3020, "identical repeated headers remain supported"); + else CheckProjectionWrites(decoder); + Videodec2Hevc::PictureInfo old{sizeof(old)}; + Require(sceVideodec2GetHevcPictureInfo_nid_postfix(&previous, &old) == 0 && old.isValid && old.attachedData == 3010, "previous picture metadata remains immutable after parameter updates"); + if (type != 0) { + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset after replacement"); + decoder.Decode(original, 30); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode after replacement reset"); + CheckProjectionWrites(decoder); + } + } + Decoder recreated(2); + recreated.Decode(original, 40); + Require(sceVideodec2Flush_nid_postfix(recreated.handle, &recreated.frame, &recreated.output) == 0 && recreated.output.isValid, "recreated backend picture"); + Require(recreated.Query().attachedData == 3040, "new backend context recovers supported metadata"); +} + +void ExtendedMetadata() { + for (const bool interlaced : {false, true}) { + const auto bytes = interlaced ? std::span(HevcMetadataFixture::Interlaced) : std::span(HevcMetadataFixture::Rich); + Decoder decoder(1); + decoder.Decode({bytes.begin(), bytes.end()}, 50); + if (!decoder.output.isValid) Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "extended metadata flush"); + const auto info = decoder.Query(); + Require(info.width == 64 && info.height == 48 && info.numUnitsInTick == 1 && info.timeScale == 24, "extended dimensions and timing"); + if (interlaced) { + Require(info.fieldSequence == 1 && info.frameFieldInfoPresent == 1 && info.picStruct == 1 && info.sourceScanType == 0 && info.duplicate == 0, "picture-timing SEI and field metadata"); + Require(info.pictureTimingPresent == 1 && info.progressiveSource == 0 && info.interlacedSource == 1 && info.frameOnlyConstraint == 0, "interlaced PTL and SEI presence tail"); + } else { + Require(info.aspectRatioIdc == 2 && info.sarWidth == 12 && info.sarHeight == 11, "table-derived non-square sample aspect"); + Require(info.videoSignalTypePresent == 1 && info.videoFormat == 1 && info.fullRange == 1 && info.colourDescriptionPresent == 1 && info.colourPrimaries == 9 && info.transferCharacteristics == 16 && info.matrixCoefficients == 9, "explicit HDR color syntax"); + Require(info.chromaLocInfoPresent == 1 && info.chromaLocTop == 3 && info.chromaLocBottom == 3, "explicit chroma sample locations"); + Require(info.defaultDisplayWindow == 0 && info.cropping == 0, "unwindowed rich picture"); + } + Require(info.profile == 4 && info.level == 30 && info.subLayerOrderingPresent == 1 && info.maxDecPicBufferingMinus1 == 2 && info.idr == 1 && info.irap == 1, "extended PTL and DPB tail"); + } + Decoder windowed(1); + windowed.Decode({HevcMetadataFixture::Windowed.begin(), HevcMetadataFixture::Windowed.end()}, 51); + if (!windowed.output.isValid) Require(sceVideodec2Flush_nid_postfix(windowed.handle, &windowed.frame, &windowed.output) == 0, "windowed picture flush"); + Require(windowed.output.isValid, "windowed picture remains decodable"); + CheckProjectionWrites(windowed); +} + +void CabacPadding() { + Decoder decoder(2); + std::vector padded(HevcFixture::HevcMain10Stream.begin(), HevcFixture::HevcMain10Stream.end()); + padded.insert(padded.end(), {0, 0, 3}); + decoder.Decode(padded, 60); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "terminal CABAC prevention byte decode"); + Require(decoder.Query().attachedData == 3060, "terminal CABAC prevention byte metadata"); + Videodec2Hevc::State state; + padded.back() = 2; + const auto invalid = Videodec2Hevc::Parse_nid_no_patch(state, padded); + Require(!invalid.unsupported.empty(), "missing emulation prevention was accepted"); +} + +void MalformedParameterHistory() { + const std::vector original(HevcFixture::HevcMain10Stream.begin(), HevcFixture::HevcMain10Stream.end()); + Decoder decoder(2); + decoder.Decode(original, 70); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "initial malformed-history picture"); + Require(decoder.Query().attachedData == 3070, "initial malformed-history metadata"); + std::vector malformed{0, 0, 1, 0x44, 1, 0x80}; + malformed.insert(malformed.end(), original.begin(), original.end()); + bool failed = false; + try { decoder.Decode(malformed, 71); } + catch (const std::runtime_error&) { failed = true; } + if (!failed) { + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "malformed parameter output"); + CheckProjectionWrites(decoder); + } + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset malformed parameter state"); + decoder.Decode(original, 72); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "clean decode after malformed parameter reset"); + CheckProjectionWrites(decoder); +} + +void TransferAssociation() { + const auto original = HevcFixture::hevcUnits(HevcFixture::HevcStream.data(), HevcFixture::HevcStream.size()); + const std::array transfer{0, 0, 1, 0x4e, 1, 147, 1, 16, 0x80}; + { + Decoder decoder(1); + auto units = original; + units[0].insert(units[0].begin() + 7, transfer.begin(), transfer.end()); + auto conflict = transfer; + conflict[7] = 18; + units[0].insert(units[0].begin() + 7, conflict.begin(), conflict.end()); + unsigned pictures = 0; + for (std::size_t index = 0; index < units.size(); ++index) { + decoder.Decode(units[index], index); + if (decoder.output.isValid) { CheckProjectionWrites(decoder); ++pictures; } + } + for (;;) { + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0, "conflicting transfer drain"); + if (!decoder.output.isValid) break; + CheckProjectionWrites(decoder); + ++pictures; + } + Require(pictures == 6, "conflicting transfers retain all decoded pictures"); + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset conflicting transfer sequence"); + decoder.Decode(original[0], 90); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "new IRAP transfer sequence"); + Require(decoder.Query().preferredTransfer == 0, "new CLVS clears prior transfer ambiguity"); + } + { + Decoder decoder(1); + try { decoder.Decode({transfer.begin(), transfer.end()}, 91); } + catch (const std::runtime_error&) {} + Require(sceVideodec2Reset_nid_postfix(decoder.handle) == 0, "reset pictureless transfer packet"); + decoder.Decode(original[0], 92); + Require(sceVideodec2Flush_nid_postfix(decoder.handle, &decoder.frame, &decoder.output) == 0 && decoder.output.isValid, "decode after pictureless transfer"); + CheckProjectionWrites(decoder); + } +} + +} + +int main(int argc, char** argv) { + ArgumentErrors(); + ArgumentSizes(); + GuestArgumentWrites(); + SecondaryVersionNoRead(); + if (argc == 2 && std::strcmp(argv[1], "--arguments") == 0) return 0; + GuestMetadataWrites(); + Clips(); + Clips(true); + Single(HevcFixture::HevcMain10Stream, 2, 0, true); + Single(HevcFixture::HevcCroppedStream, 0, 2, false); + UnsupportedPicture(); + ParameterReplacements(); + ExtendedMetadata(); + CabacPadding(); + MalformedParameterHistory(); + TransferAssociation(); +} diff --git a/core/libs/tests/GuestHmdSetupDialog.cpp b/core/libs/tests/GuestHmdSetupDialog.cpp index 7e647a5875..347f8c4ad1 100644 --- a/core/libs/tests/GuestHmdSetupDialog.cpp +++ b/core/libs/tests/GuestHmdSetupDialog.cpp @@ -51,6 +51,8 @@ int main() { Require(sceHmdSetupDialogClose() == 0); Require(sceHmdSetupDialogTerminate() == 0); Require(sceHmdSetupDialogTerminate() == kErrNotInitialized); + Require(sceHmdSetupDialogOpen(nullptr) == kErrNotInitialized); + Require(sceHmdSetupDialogGetResult(nullptr) == kErrNotInitialized); Require(sceHmdSetupDialogGetStatus() == kStatusNone); Require(sceHmdSetupDialogUpdateStatus() == kStatusNone); diff --git a/core/libs/tests/GuestInvitationDialog.cpp b/core/libs/tests/GuestInvitationDialog.cpp new file mode 100644 index 0000000000..0552fd1e08 --- /dev/null +++ b/core/libs/tests/GuestInvitationDialog.cpp @@ -0,0 +1,153 @@ +#include "prx/libSceInvitationDialog/include/Api.hpp" +#include "prx/libSceCommonDialog/include/Api.hpp" +#include "prx/libSceCommonDialog/include/DialogUse.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int StatusNone = 0; +constexpr int StatusInitialized = 1; +constexpr int StatusFinished = 3; +constexpr int NotSystemInitialized = static_cast(0x80B80001u); +constexpr int NotInitialized = static_cast(0x80B80003u); +constexpr int AlreadyInitialized = static_cast(0x80B80004u); +constexpr int NotFinished = static_cast(0x80B80005u); +constexpr int Busy = static_cast(0x80B80008u); +constexpr int ArgNull = static_cast(0x80B8000Du); +constexpr std::int32_t ResultUserCanceled = 1; +constexpr std::int32_t ModeSend = 1; +constexpr std::int32_t ModeRecv = 2; + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "InvitationDialog: %s\n", message); + std::abort(); + } +} + +template +void RequireUnsettled(TCall call, const char* message) { + bool threw = false; + try { + call(); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).find("not implemented") == std::string::npos; + } + Require(threw, message); +} + +InvitationDialogParam MakeParam(std::int32_t mode, void* callbackArg) { + InvitationDialogParam param{}; + param.baseParam.size = sizeof(CommonDialogBaseParam); + param.size = sizeof(InvitationDialogParam); + param.mode = mode; + param.userId = 0x10000000; + param.callbackArg = callbackArg; + return param; +} + +void TestBeforeInitialize() { + InvitationDialogResult result{}; + Require(sceInvitationDialogUpdateStatus() == StatusNone, "status before initialization"); + Require(sceInvitationDialogGetResult(&result) == NotInitialized, "result before initialization"); + Require(sceInvitationDialogGetResult(nullptr) == NotInitialized, "null result before initialization"); + Require(sceInvitationDialogClose() == NotInitialized, "close before initialization"); + Require(sceInvitationDialogTerminate() == NotInitialized, "terminate before initialization"); + const InvitationDialogParam param = MakeParam(ModeSend, nullptr); + RequireUnsettled([&] { sceInvitationDialogOpen(¶m); }, "open before initialization"); +} + +void TestCommonDialogPreconditions() { + Require(sceInvitationDialogInitialize() == NotSystemInitialized, "initialize before sceCommonDialogInitialize"); + Require(sceInvitationDialogUpdateStatus() == StatusNone && !sceCommonDialogIsUsed(), "a failed initialization leaves no state"); + Require(sceCommonDialogInitialize() == 0, "initialize the common dialog system"); + + Require(CommonDialogAcquire_nid_no_patch(), "another common dialog takes the system"); + Require(sceInvitationDialogInitialize() == Busy, "initialize while another common dialog is in use"); + Require(sceInvitationDialogUpdateStatus() == StatusNone, "a busy initialization leaves the status"); + CommonDialogRelease_nid_no_patch(); + + Require(sceInvitationDialogInitialize() == 0 && sceCommonDialogIsUsed(), "the dialog takes the system"); + Require(!CommonDialogAcquire_nid_no_patch(), "another common dialog is busy while this one is initialized"); + Require(sceInvitationDialogTerminate() == 0 && !sceCommonDialogIsUsed(), "termination frees the system"); +} + +void TestOpenChecks() { + InvitationDialogResult result{}; + Require(sceInvitationDialogInitialize() == 0, "initialize"); + Require(sceInvitationDialogInitialize() == AlreadyInitialized, "initialize twice"); + Require(sceCommonDialogIsUsed(), "the system stays in use after a repeated initialization"); + Require(sceInvitationDialogUpdateStatus() == StatusInitialized, "status after initialization"); + Require(sceInvitationDialogGetResult(&result) == NotFinished, "result before the dialog opened"); + Require(sceInvitationDialogGetResult(nullptr) == NotFinished, "null result before the dialog finished"); + RequireUnsettled([] { sceInvitationDialogClose(); }, "close of a dialog that is not running"); + + Require(sceInvitationDialogOpen(nullptr) == ArgNull, "open without a param"); + InvitationDialogParam param = MakeParam(ModeSend, nullptr); + param.size = sizeof(InvitationDialogParam) - 8; + RequireUnsettled([&] { sceInvitationDialogOpen(¶m); }, "open with another param size"); + param = MakeParam(0, nullptr); + RequireUnsettled([&] { sceInvitationDialogOpen(¶m); }, "open in the invalid mode"); + param = MakeParam(3, nullptr); + RequireUnsettled([&] { sceInvitationDialogOpen(¶m); }, "open in an unknown mode"); + Require(sceInvitationDialogUpdateStatus() == StatusInitialized, "a rejected open leaves the status"); +} + +void TestCanceled() { + int first = 0; + int second = 0; + const InvitationDialogParam send = MakeParam(ModeSend, &first); + Require(sceInvitationDialogOpen(&send) == 0, "open to send"); + Require(sceInvitationDialogUpdateStatus() == StatusFinished, "the dialog finishes at once"); + RequireUnsettled([] { sceInvitationDialogClose(); }, "close of a finished dialog"); + + Require(sceInvitationDialogGetResult(nullptr) == ArgNull, "null result after the dialog finished"); + InvitationDialogResult result{}; + result.reserved[31] = 1; + RequireUnsettled([&] { sceInvitationDialogGetResult(&result); }, "result with a non-zero reserved byte"); + + InvitationDialogRecipients recipients{}; + recipients.count = 5; + std::memset(recipients.onlineIds, 'x', sizeof(recipients.onlineIds)); + result = InvitationDialogResult{}; + result.errorCode = 7; + result.result = 7; + result.recipients = &recipients; + Require(sceInvitationDialogGetResult(&result) == 0, "result of the canceled dialog"); + Require(result.callbackArg == &first, "the result carries the callback argument"); + Require(result.errorCode == 0 && result.result == ResultUserCanceled, "the user canceled"); + Require(result.recipients == &recipients && recipients.count == 0, "no invitation was sent"); + Require(recipients.onlineIds[0].data[0] == 'x' && recipients.onlineIds[15].pad[2] == 'x', "the online ids are left alone"); + for (const std::uint8_t value : result.reserved) Require(value == 0, "the reserved bytes stay zero"); + + result = InvitationDialogResult{}; + Require(sceInvitationDialogGetResult(&result) == 0 && result.recipients == nullptr, "result without a recipient list"); + + const InvitationDialogParam receive = MakeParam(ModeRecv, &second); + Require(sceInvitationDialogOpen(&receive) == 0, "open again to receive"); + result = InvitationDialogResult{}; + Require(sceInvitationDialogGetResult(&result) == 0 && result.callbackArg == &second, "the second open carries its own callback argument"); + + Require(sceInvitationDialogTerminate() == 0 && !sceCommonDialogIsUsed(), "terminate"); + Require(sceInvitationDialogUpdateStatus() == StatusNone, "status after termination"); + Require(sceInvitationDialogGetResult(&result) == NotInitialized, "result after termination"); + Require(sceInvitationDialogInitialize() == 0 && sceInvitationDialogGetResult(&result) == NotFinished, "a new initialization forgets the result"); + Require(sceInvitationDialogTerminate() == 0, "terminate again"); +} + +} + +int main() { + TestBeforeInitialize(); + TestCommonDialogPreconditions(); + TestOpenChecks(); + TestCanceled(); + return 0; +} diff --git a/core/libs/tests/GuestJscString.cpp b/core/libs/tests/GuestJscString.cpp new file mode 100644 index 0000000000..673bde7533 --- /dev/null +++ b/core/libs/tests/GuestJscString.cpp @@ -0,0 +1,170 @@ +#include "prx/libSceJsc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "JavaScriptCore string check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void Unicode() { + std::u32string scalars; + for (char32_t codepoint = 1; codepoint <= 0x10FFFF; ++codepoint) { + if (codepoint < 0xD800 || codepoint > 0xDFFF) scalars.push_back(codepoint); + } + std::codecvt_utf8 encode; + std::codecvt_utf8_utf16 decode; + std::mbstate_t state{}; + const char32_t* readScalars = nullptr; + std::string utf8(scalars.size() * 4, '\0'); + char* writtenBytes = nullptr; + Require(encode.out(state, scalars.data(), scalars.data() + scalars.size(), readScalars, + utf8.data(), utf8.data() + utf8.size(), writtenBytes) == std::codecvt_base::ok); + Require(readScalars == scalars.data() + scalars.size()); + utf8.resize(writtenBytes - utf8.data()); + state = {}; + const char* readBytes = nullptr; + std::u16string utf16(utf8.size(), 0); + char16_t* writtenUnits = nullptr; + Require(decode.in(state, utf8.data(), utf8.data() + utf8.size(), readBytes, + utf16.data(), utf16.data() + utf16.size(), writtenUnits) == std::codecvt_base::ok); + Require(readBytes == utf8.data() + utf8.size()); + utf16.resize(writtenUnits - utf16.data()); + auto* string = JSStringCreateWithUTF8CString(utf8.c_str()); + Require(JSStringGetLength(string) == utf16.size()); + const auto* characters = JSStringGetCharactersPtr(string); + Require(std::equal(utf16.begin(), utf16.end(), characters)); + const std::vector units(utf16.begin(), utf16.end()); + auto* other = JSStringCreateWithCharacters(units.data(), units.size()); + Require(JSStringIsEqual(string, other)); + Require(JSStringGetMaximumUTF8CStringSize(string) == utf16.size() * 3 + 1); + std::vector buffer(utf8.size() + 2, '?'); + Require(JSStringGetUTF8CString(string, buffer.data(), buffer.size() - 1) == utf8.size() + 1); + Require(std::memcmp(buffer.data(), utf8.c_str(), utf8.size() + 1) == 0); + Require(buffer.back() == '?'); + JSStringRelease(other); + JSStringRelease(string); +} + +static void Boundaries() { + const char text[] = "A\xC2\xA2\xE2\x82\xAC\xF0\x9F\x98\x80Z"; + auto* string = JSStringCreateWithUTF8CString(text); + Require(JSStringGetLength(string) == 6); + const std::array complete{0, 1, 3, 6, 10, 11}; + for (std::size_t capacity = 1; capacity <= sizeof(text) + 1; ++capacity) { + std::array buffer; + buffer.fill('?'); + std::size_t expected = 0; + for (const auto length : complete) { + if (length < capacity) expected = length; + } + Require(JSStringGetUTF8CString(string, buffer.data(), capacity) == expected + 1); + Require(std::memcmp(buffer.data(), text, expected) == 0 && buffer[expected] == '\0'); + for (auto index = expected + 1; index < buffer.size(); ++index) Require(buffer[index] == '?'); + } + char unchanged = '!'; + Require(JSStringGetUTF8CString(string, &unchanged, 0) == 0 && unchanged == '!'); + Require(JSStringGetUTF8CString(string, nullptr, 10) == 0); + Require(JSStringGetUTF8CString(nullptr, &unchanged, 1) == 0 && unchanged == '!'); + Require(JSStringGetLength(nullptr) == 0 && JSStringGetCharactersPtr(nullptr) == nullptr); + Require(JSStringIsEqual(nullptr, nullptr) && !JSStringIsEqual(nullptr, string)); + Require(JSStringIsEqualToUTF8CString(string, text)); + Require(!JSStringIsEqualToUTF8CString(string, "a")); + JSStringRelease(string); +} + +static void Invalid() { + const auto* unavailable = reinterpret_cast(std::uintptr_t{1}); + for (const auto count : {static_cast(std::numeric_limits::max()) + 1, + static_cast(std::numeric_limits::max()), std::numeric_limits::max()}) { + bool threw = false; + try { + JSStringCreateWithCharacters(unavailable, count); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } + auto* empty = JSStringCreateWithUTF8CString(""); + Require(JSStringGetLength(empty) == 0); + auto* nullString = JSStringCreateWithCharacters(nullptr, 0); + Require(JSStringGetLength(nullString) == 0 && JSStringGetCharactersPtr(nullString) == nullptr); + Require(!JSStringIsEqual(empty, nullString)); + Require(JSStringIsEqualToUTF8CString(nullString, "\xFF")); + JSStringRelease(nullString); + for (const auto* invalid : {"\x80", "\xC0\x80", "\xC1\xBF", "\xE0\x80\x80", "\xED\xA0\x80", "\xF0\x80\x80\x80", "\xF4\x90\x80\x80", "\xF5\x80\x80\x80", "\xFF", "x\xC2", "x\xE2\x82", "x\xF0\x9F\x98", "\xE2!"}) { + auto* string = JSStringCreateWithUTF8CString(invalid); + Require(JSStringGetLength(string) == 0 && JSStringGetCharactersPtr(string) == nullptr); + Require(!JSStringIsEqual(string, empty)); + Require(JSStringIsEqualToUTF8CString(string, "\xFF")); + char buffer = '!'; + Require(JSStringGetUTF8CString(string, &buffer, 1) == 1 && buffer == '\0'); + JSStringRelease(string); + } + for (const auto& units : std::vector>{{0xD800}, {0xDC00}, {'x', 0xD800}, {'x', 0xDC00, 'y'}, {'x', 0xD800, 'y'}}) { + auto* string = JSStringCreateWithCharacters(units.data(), units.size()); + Require(JSStringGetLength(string) == units.size()); + Require(std::equal(units.begin(), units.end(), JSStringGetCharactersPtr(string))); + std::array buffer; + buffer.fill('?'); + const std::size_t prefix = units[0] == 'x' ? 1 : 0; + Require(JSStringGetUTF8CString(string, buffer.data(), buffer.size()) == prefix + 1); + Require(buffer[prefix] == '\0' && buffer[prefix + 1] == '?'); + JSStringRelease(string); + } + JSStringRelease(empty); +} + +static void Ownership() { + std::array units{'A', 0, 0x03A9, 'B'}; + auto* string = JSStringCreateWithCharacters(units.data(), units.size()); + units.fill('?'); + const auto* pointer = JSStringGetCharactersPtr(string); + Require(pointer[0] == 'A' && pointer[1] == 0 && pointer[2] == 0x03A9 && pointer[3] == 'B'); + std::array buffer{}; + Require(JSStringGetUTF8CString(string, buffer.data(), buffer.size()) == 6); + const std::array expected{'A', 0, static_cast(0xCE), static_cast(0xA9), 'B', 0}; + Require(std::equal(expected.begin(), expected.end(), buffer.begin())); + auto* retained = JSStringRetain(string); + Require(retained == string); + JSStringRelease(string); + Require(JSStringGetCharactersPtr(retained) == pointer && JSStringGetLength(retained) == 4); + std::vector threads; + for (unsigned index = 0; index < 4; ++index) { + threads.emplace_back([retained] { + for (unsigned count = 0; count < 5000; ++count) { + Require(JSStringRetain(retained) == retained); + Require(JSStringGetLength(retained) == 4); + JSStringRelease(retained); + } + }); + } + for (auto& thread : threads) thread.join(); + JSStringRelease(retained); +} + +int main() { + Unicode(); + Boundaries(); + Invalid(); + Ownership(); +} diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 40e6b87d13..2a498b7e2c 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -510,10 +510,18 @@ void CheckSettimeofday() { Require(settimeofday_nid_postfix(nullptr, nullptr) == 0); Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); const KernelTimezone west{60, 1}; - for (const KernelTimeval invalid : {KernelTimeval{0, 1000000}, KernelTimeval{0, -1}, KernelTimeval{-1, 0}}) + const auto unreadableTime = reinterpret_cast(1); + const auto unreadableZone = reinterpret_cast(1); + Require(settimeofday_nid_postfix(unreadableTime, &west) == -1 && Error() == 14); + Require(settimeofday_nid_postfix(unreadableTime, unreadableZone) == -1 && Error() == 14); + Require(settimeofday_nid_postfix(nullptr, unreadableZone) == -1 && Error() == 14); + for (const KernelTimeval invalid : {KernelTimeval{0, 1000000}, KernelTimeval{0, -1}, KernelTimeval{-1, 0}}) { Require(settimeofday_nid_postfix(&invalid, &west) == -1 && Error() == 22); + Require(settimeofday_nid_postfix(&invalid, unreadableZone) == -1 && Error() == 14); + } Require(gettimeofday_nid_postfix(nullptr, &zone) == 0 && zone.tz_minuteswest == 0 && zone.tz_dsttime == 0); const KernelTimeval valid{1000000000, 0}; + Require(settimeofday_nid_postfix(&valid, unreadableZone) == -1 && Error() == 14); bool rejected = false; try { settimeofday_nid_postfix(&valid, &west); @@ -674,6 +682,10 @@ int main() { #endif CheckProcessQueries(); Require(setsid_nid_postfix() == -1 && Error() == 1); + Require(profil_nid_postfix(nullptr, 0, 0, (1u << 16) + 1) == -1 && Error() == 22); + Require(profil_nid_postfix(nullptr, 0, 0, 0xFFFFFFFFu) == -1 && Error() == 22); + Require(profil_nid_postfix(reinterpret_cast(0x10), 4096, 0, 0) == 0); + Require(profil_nid_postfix(nullptr, 0, 0, 0) == 0); Require(umask_nid_postfix(0) == 022); Require(umask_nid_postfix(07777) == 0); Require(umask_nid_postfix(022) == 07777); diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index c66eb62dc7..8c29df2cfa 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -100,6 +100,29 @@ static void MutexBasics() { RequireThrows([] { _Mtx_unlock_nid_postfix(nullptr); }); } +static void DefaultNameOverrideMutex() { + void* mutex = nullptr; + Require(_Mtx_init_with_default_name_override_nid_postfix(&mutex, tryable, "guest mutex") == success && mutex != nullptr); + Require(_Mtx_lock_nid_postfix(&mutex) == success && _Mtx_lock_nid_postfix(&mutex) == busy); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + _Mtx_destroy_nid_postfix(&mutex); + Require(mutex == nullptr); + + void* unnamedMutex = nullptr; + Require(_Mtx_init_with_default_name_override_nid_postfix(&unnamedMutex, 0, nullptr) == success && unnamedMutex != nullptr); + Require(_Mtx_lock_nid_postfix(&unnamedMutex) == success); + Require(_Mtx_unlock_nid_postfix(&unnamedMutex) == success); + _Mtx_destroy_nid_postfix(&unnamedMutex); + + void* recursiveMutex = nullptr; + Require(_Mtx_init_with_default_name_override_nid_postfix(&recursiveMutex, recursive, "recursive") == success && recursiveMutex != nullptr); + Require(_Mtx_lock_nid_postfix(&recursiveMutex) == success && _Mtx_lock_nid_postfix(&recursiveMutex) == success); + Require(_Mtx_unlock_nid_postfix(&recursiveMutex) == success && _Mtx_unlock_nid_postfix(&recursiveMutex) == success); + _Mtx_destroy_nid_postfix(&recursiveMutex); + + RequireThrows([] { _Mtx_init_with_default_name_override_nid_postfix(nullptr, tryable, "null"); }); +} + static void RecursiveMutex() { void* mutex = CreateMutex(tryable | recursive); for (int i = 0; i < 3; ++i) Require(_Mtx_lock_nid_postfix(&mutex) == success); @@ -589,6 +612,7 @@ int main() { VersionedSync(); ForwardedCalls(); MutexBasics(); + DefaultNameOverrideMutex(); RecursiveMutex(); TimedMutex(); MutualExclusion(); diff --git a/core/libs/tests/GuestLibreSsl3KeyReference.cpp b/core/libs/tests/GuestLibreSsl3KeyReference.cpp new file mode 100644 index 0000000000..ca69aceb4e --- /dev/null +++ b/core/libs/tests/GuestLibreSsl3KeyReference.cpp @@ -0,0 +1,68 @@ +#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +template +static void CheckKey(TRetain retain, std::size_t referenceOffset) { + struct GuardedKey { + std::array before; + TKey key; + std::array after; + } guarded; + for (std::int32_t initial : {-2, 0, 1, 37, std::numeric_limits::max() - 1}) { + std::memset(&guarded, 0xA5, sizeof(guarded)); + guarded.key.references = initial; + std::array expected; + std::memcpy(expected.data(), &guarded, sizeof(guarded)); + const std::int32_t next = initial + 1; + std::memcpy(expected.data() + 16 + referenceOffset, &next, sizeof(next)); + Require(retain(&guarded.key) == (next > 1), "retain return value"); + Require(std::memcmp(&guarded, expected.data(), sizeof(guarded)) == 0, "retain changed bytes outside reference count"); + } + std::memset(&guarded, 0x5A, sizeof(guarded)); + guarded.key.references = 1; + std::array expected; + std::memcpy(expected.data(), &guarded, sizeof(guarded)); + for (int index = 0; index < 32; ++index) + Require(retain(&guarded.key) == 1, "repeated retain failed"); + Require(guarded.key.references == 33, "repeated retain count"); + guarded.key.references = 1; + std::atomic start = false; + std::array threads; + for (auto& thread : threads) { + thread = std::thread([&] { + while (!start.load()) + std::this_thread::yield(); + for (int index = 0; index < 1000; ++index) + Require(retain(&guarded.key) == 1, "concurrent retain failed"); + }); + } + start = true; + for (auto& thread : threads) + thread.join(); + const std::int32_t finalCount = 4001; + std::memcpy(expected.data() + 16 + referenceOffset, &finalCount, sizeof(finalCount)); + Require(std::memcmp(&guarded, expected.data(), sizeof(guarded)) == 0, "concurrent count or adjacent bytes changed"); +} + +int main(int argc, char** argv) { + if (argc == 1 || std::strcmp(argv[1], "evp") == 0) + CheckKey(EVP_PKEY_up_ref_nid_postfix, 4); + if (argc == 1 || std::strcmp(argv[1], "ec") == 0) + CheckKey(EC_KEY_up_ref_nid_postfix, 48); + return 0; +} diff --git a/core/libs/tests/GuestLibreSslPkeyType.cpp b/core/libs/tests/GuestLibreSslPkeyType.cpp new file mode 100644 index 0000000000..a8ba0075e1 --- /dev/null +++ b/core/libs/tests/GuestLibreSslPkeyType.cpp @@ -0,0 +1,25 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL EVP_PKEY_type check failed at line %u\n", location.line()); + std::abort(); +} + +int main() { + Require(EVP_PKEY_type_nid_postfix(6) == 6); + Require(EVP_PKEY_type_nid_postfix(19) == 6); + Require(EVP_PKEY_type_nid_postfix(408) == 408); + Require(EVP_PKEY_type_nid_postfix(855) == 855); + Require(EVP_PKEY_type_nid_postfix(894) == 894); + Require(EVP_PKEY_type_nid_postfix(28) == 28); + for (const int type : {66, 67, 70, 113, 116}) Require(EVP_PKEY_type_nid_postfix(type) == 116); + for (const int type : {0, -1, 1, 5, 7, 64, 65, 117, 407, 409, 672, 854, 856, 811, 815, 893, 895, 946, 947, 948, INT_MIN, INT_MAX}) Require(EVP_PKEY_type_nid_postfix(type) == 0); + std::puts("libSceLibreSsl EVP_PKEY_type tests passed"); +} diff --git a/core/libs/tests/GuestLibreSslProtocol.cpp b/core/libs/tests/GuestLibreSslProtocol.cpp new file mode 100644 index 0000000000..e4ced5478f --- /dev/null +++ b/core/libs/tests/GuestLibreSslProtocol.cpp @@ -0,0 +1,120 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +using SelectProtocol = int (APS5_VABI*)(unsigned char**, unsigned char*, const unsigned char*, std::uint32_t, const unsigned char*, std::uint32_t); +using Protocol = std::vector; +using Protocols = std::vector; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "LibreSSL protocol check failed at line %u\n", location.line()); + std::abort(); + } +} + +Protocol Encode(const Protocols& names) { + Protocol result; + for (const auto& name : names) { + Require(!name.empty() && name.size() <= 255); + result.push_back(static_cast(name.size())); + result.insert(result.end(), name.begin(), name.end()); + } + return result; +} + +void CheckSelection(SelectProtocol select, const Protocols& peerNames, const Protocols& supportedNames) { + const auto peer = Encode(peerNames); + const auto supported = Encode(supportedNames); + const auto peerCopy = peer; + const auto supportedCopy = supported; + const unsigned char* expected = supported.data() + 1; + std::size_t expectedLength = supportedNames.front().size(); + int expectedStatus = 2; + std::size_t offset = 1; + for (const auto& name : peerNames) { + if (std::find(supportedNames.begin(), supportedNames.end(), name) != supportedNames.end()) { + expected = peer.data() + offset; + expectedLength = name.size(); + expectedStatus = 1; + break; + } + offset += name.size() + 1; + } + unsigned char* output = nullptr; + std::array length{0xC1, 0xA5, 0xC2}; + Require(select(&output, &length[1], peer.data(), static_cast(peer.size()), + supported.data(), static_cast(supported.size())) == expectedStatus); + Require(output == expected && length[1] == expectedLength); + Require(length[0] == 0xC1 && length[2] == 0xC2); + Require(peer == peerCopy && supported == supportedCopy); +} + +void Run(SelectProtocol select) { + const Protocol http{'h', 't', 't', 'p', '/', '1', '.', '1'}; + const Protocol h2{'h', '2'}; + const Protocol h3{'h', '3'}; + CheckSelection(select, {h2, http}, {http, h2}); + CheckSelection(select, {http, h2}, {h2, http}); + CheckSelection(select, {h3}, {http, h2}); + CheckSelection(select, {h2, h2, http}, {http, h2, h2}); + CheckSelection(select, {{0, 0xFF, 0x80}, {'a', 0, 'b'}}, {{'a', 0, 'b'}, {0, 0xFF, 0x80}}); + CheckSelection(select, {{'h', '2', 0}, {'H', '2'}}, {h2, http}); + Protocol longest(255); + for (std::size_t index = 0; index < longest.size(); ++index) longest[index] = static_cast(index); + CheckSelection(select, {longest}, {h2, longest}); + CheckSelection(select, {h2}, {longest}); + + const Protocols alphabet{{'a'}, {'a', 'b'}, {'b'}, {0, 0xFF}}; + std::vector lists; + for (std::size_t count = 1, combinations = alphabet.size(); count <= 3; ++count, combinations *= alphabet.size()) { + for (std::size_t code = 0; code < combinations; ++code) { + Protocols names; + for (std::size_t index = 0, remaining = code; index < count; ++index, remaining /= alphabet.size()) + names.push_back(alphabet[remaining % alphabet.size()]); + lists.push_back(std::move(names)); + } + } + for (const auto& peer : lists) + for (const auto& supported : lists) CheckSelection(select, peer, supported); + + Protocols large(33000, Protocol{'x'}); + large.push_back(h2); + CheckSelection(select, large, {h2}); + CheckSelection(select, {h2}, large); + + std::printf("LibreSSL protocol selection: 7056 generated pairs, and valid-list boundaries passed\n"); +} + +int main(int argc, char** argv) { + Require(argc == 3); +#ifdef _WIN32 + const auto library = LoadLibraryExW(std::filesystem::path(argv[1]).c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + if (!library) std::fprintf(stderr, "LoadLibraryExW failed: %lu\n", GetLastError()); + Require(library != nullptr); + const auto select = std::bit_cast(GetProcAddress(library, argv[2])); +#else + const auto library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + if (!library) std::fprintf(stderr, "%s\n", dlerror()); + Require(library != nullptr); + const auto select = reinterpret_cast(dlsym(library, argv[2])); +#endif + Require(select != nullptr); + Run(select); +} diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp index 9b567297b0..6491d4213b 100644 --- a/core/libs/tests/GuestLoginDialog.cpp +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -75,7 +75,12 @@ int main() { Require(sceLoginDialogClose() == 0); Require(sceLoginDialogTerminate() == 0); Require(sceLoginDialogTerminate() == kErrNotInitialized); + Require(sceLoginDialogOpen(nullptr) == kErrArgNull); + Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); + Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(&result) == kErrNotInitialized); Require(sceLoginDialogGetStatus() == kStatusNone); + Require(sceLoginDialogUpdateStatus() == kStatusNone); Require(sceLoginDialogInitialize() == 0); Require(sceLoginDialogGetStatus() == kStatusInitialized); @@ -85,4 +90,11 @@ int main() { Require(sceLoginDialogGetResult(&result) == 0); Require(result.result == kResultUserCanceled); Require(sceLoginDialogTerminate() == 0); + Require(sceLoginDialogTerminate() == kErrNotInitialized); + Require(sceLoginDialogOpen(nullptr) == kErrArgNull); + Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); + Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(&result) == kErrNotInitialized); + Require(sceLoginDialogGetStatus() == kStatusNone); + Require(sceLoginDialogUpdateStatus() == kStatusNone); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index a305ccc335..f8e0f9da06 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -173,6 +173,123 @@ static void CheckFegetenv() { Require(threw); } +template +struct ModfCase { Bits input, fraction, integral; unsigned flags; }; + +template +static void CheckModfCases(const ModfCase (&cases)[Count], Call call) { + std::fenv_t saved; + std::fegetenv(&saved); + const auto savedCsr = _mm_getcsr(); + int* error = __error_nid_postfix(); + const int savedError = *error; + const int rounding[] = {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; + for (unsigned mode = 0; mode < 4; ++mode) { + for (unsigned denorm = 0; denorm < 4; ++denorm) { + const unsigned control = 0x1f80u | (mode << 13u) | ((denorm & 1u) != 0 ? 0x40u : 0u) | ((denorm & 2u) != 0 ? 0x8000u : 0u); + for (const unsigned sticky : {0u, 5u, 32u}) { + for (const auto& item : cases) { + std::fesetenv(FE_DFL_ENV); + std::fesetround(rounding[mode]); + std::uint32_t environment[7]; + asm volatile("fnstenv %0" : "=m"(environment)); + environment[1] = (environment[1] & ~63u) | sticky; + asm volatile("fldenv %0" : : "m"(environment)); + _mm_setcsr(control | sticky); + const auto beforeControl = X87ControlWord(); + *error = 97; + Float integral = 77; + const auto fraction = std::bit_cast(call(std::bit_cast(item.input), &integral)); + const auto integralBits = std::bit_cast(integral); + const auto csr = _mm_getcsr(); + const auto x87Status = X87StatusWord(); + const auto x87Control = X87ControlWord(); + if (fraction != item.fraction || integralBits != item.integral || csr != (control | sticky | item.flags) || (x87Status & 63u) != sticky || x87Control != beforeControl || *error != 97) { + std::fprintf(stderr, "modf width=%zu input=%llx round=%u denorm=%u sticky=%u fraction=%llx integral=%llx csr=%x x87=%x\n", sizeof(Float), static_cast(item.input), mode, denorm, sticky, static_cast(fraction), static_cast(integralBits), csr, x87Status); + Require(false); + } + } + } + } + } + std::fesetenv(&saved); + _mm_setcsr(savedCsr); + *error = savedError; +} + +static void CheckModfDecomposition() { + constexpr ModfCase doubles[] = { + {0x00000000ull, 0x00000000ull, 0x0000000000000000ull, 0u}, + {0x8000000000000000ull, 0x8000000000000000ull, 0x8000000000000000ull, 0u}, + {0x00000001ull, 0x00000001ull, 0x0000000000000000ull, 0u}, + {0x8000000000000001ull, 0x8000000000000001ull, 0x8000000000000000ull, 0u}, + {0xfffffffffffffull, 0xfffffffffffffull, 0x0000000000000000ull, 0u}, + {0x800fffffffffffffull, 0x800fffffffffffffull, 0x8000000000000000ull, 0u}, + {0x10000000000000ull, 0x10000000000000ull, 0x0000000000000000ull, 0u}, + {0x8010000000000000ull, 0x8010000000000000ull, 0x8000000000000000ull, 0u}, + {0x3fe0000000000000ull, 0x3fe0000000000000ull, 0x0000000000000000ull, 0u}, + {0x3ff0000000000000ull, 0x00000000ull, 0x3ff0000000000000ull, 0u}, + {0xbff0000000000000ull, 0x8000000000000000ull, 0xbff0000000000000ull, 0u}, + {0x4028000000000000ull, 0x00000000ull, 0x4028000000000000ull, 0u}, + {0x7fefffffffffffffull, 0x00000000ull, 0x7fefffffffffffffull, 0u}, + {0xffefffffffffffffull, 0x8000000000000000ull, 0xffefffffffffffffull, 0u}, + {0x7ff0000000000000ull, 0x00000000ull, 0x7ff0000000000000ull, 0u}, + {0xfff0000000000000ull, 0x8000000000000000ull, 0xfff0000000000000ull, 0u}, + {0x7ff0000000000001ull, 0x7ff0000000000001ull, 0x7ff0000000000001ull, 0u}, + {0xfff0000000000001ull, 0xfff0000000000001ull, 0xfff0000000000001ull, 0u}, + {0x7ff8000000001234ull, 0x7ff8000000001234ull, 0x7ff8000000001234ull, 0u}, + {0xfff8000000001234ull, 0xfff8000000001234ull, 0xfff8000000001234ull, 0u}, + {0x3ff4000000000000ull, 0x3fd0000000000000ull, 0x3ff0000000000000ull, 0u}, + {0xbff4000000000000ull, 0xbfd0000000000000ull, 0xbff0000000000000ull, 0u}, + {0x3ff8000000000000ull, 0x3fe0000000000000ull, 0x3ff0000000000000ull, 0u}, + {0xbff8000000000000ull, 0xbfe0000000000000ull, 0xbff0000000000000ull, 0u}, + {0x4006000000000000ull, 0x3fe8000000000000ull, 0x4000000000000000ull, 0u}, + {0xc006000000000000ull, 0xbfe8000000000000ull, 0xc000000000000000ull, 0u}, + {0x3fe8000000000000ull, 0x3fe8000000000000ull, 0x0000000000000000ull, 0u}, + {0xbfe8000000000000ull, 0xbfe8000000000000ull, 0x8000000000000000ull, 0u}, + {0x3fffffffffffffffull, 0x3feffffffffffffeull, 0x3ff0000000000000ull, 0u}, + {0xbfffffffffffffffull, 0xbfeffffffffffffeull, 0xbff0000000000000ull, 0u}, + {0x4320000000000001ull, 0x3fe0000000000000ull, 0x4320000000000000ull, 0u}, + {0xc320000000000001ull, 0xbfe0000000000000ull, 0xc320000000000000ull, 0u}, + }; + constexpr ModfCase floats[] = { + {0x00000000u, 0x00000000u, 0x00000000u, 0u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0u}, + {0x00000001u, 0x00000001u, 0x00000000u, 0u}, + {0x80000001u, 0x80000001u, 0x80000000u, 0u}, + {0x007fffffu, 0x007fffffu, 0x00000000u, 0u}, + {0x807fffffu, 0x807fffffu, 0x80000000u, 0u}, + {0x00800000u, 0x00800000u, 0x00000000u, 0u}, + {0x80800000u, 0x80800000u, 0x80000000u, 0u}, + {0x3f000000u, 0x3f000000u, 0x00000000u, 0u}, + {0x3f800000u, 0x00000000u, 0x3f800000u, 0u}, + {0xbf800000u, 0x80000000u, 0xbf800000u, 0u}, + {0x41400000u, 0x00000000u, 0x41400000u, 0u}, + {0x7f7fffffu, 0x00000000u, 0x7f7fffffu, 0u}, + {0xff7fffffu, 0x80000000u, 0xff7fffffu, 0u}, + {0x7f800000u, 0x00000000u, 0x7f800000u, 0u}, + {0xff800000u, 0x80000000u, 0xff800000u, 0u}, + {0x7f800001u, 0x7f800001u, 0x7f800001u, 1u}, + {0xff800001u, 0xff800001u, 0xff800001u, 1u}, + {0x7fc01234u, 0x7fc01234u, 0x7fc01234u, 0u}, + {0xffc01234u, 0xffc01234u, 0xffc01234u, 0u}, + {0x3fa00000u, 0x3e800000u, 0x3f800000u, 0u}, + {0xbfa00000u, 0xbe800000u, 0xbf800000u, 0u}, + {0x3fc00000u, 0x3f000000u, 0x3f800000u, 0u}, + {0xbfc00000u, 0xbf000000u, 0xbf800000u, 0u}, + {0x40300000u, 0x3f400000u, 0x40000000u, 0u}, + {0xc0300000u, 0xbf400000u, 0xc0000000u, 0u}, + {0x3f400000u, 0x3f400000u, 0x00000000u, 0u}, + {0xbf400000u, 0xbf400000u, 0x80000000u, 0u}, + {0x3fffffffu, 0x3f7ffffeu, 0x3f800000u, 0u}, + {0xbfffffffu, 0xbf7ffffeu, 0xbf800000u, 0u}, + {0x4a800001u, 0x3f000000u, 0x4a800000u, 0u}, + {0xca800001u, 0xbf000000u, 0xca800000u, 0u}, + }; + CheckModfCases(doubles, modf_nid_postfix); + CheckModfCases(floats, modff_nid_postfix); +} + static bool ExtendedBits(long double value, std::uint64_t mantissa, std::uint16_t signExponent) { unsigned char bytes[sizeof(long double)]{}; std::memcpy(bytes, &value, sizeof(value)); @@ -378,6 +495,22 @@ static void CheckAtanfConsoleBits() { } } +static void CheckAtan2fConsoleBits() { + constexpr std::uint32_t cases[][3] = { + {0x3d6f53e1u, 0x3fb6a73eu, 0x3d279f68u}, {0x3e587cf9u, 0x3f8f2054u, 0x3e3f5992u}, {0xbe8d427du, 0x3e57a3bau, 0xbf6b3a09u}, + {0x40162421u, 0xbd5afe7bu, 0x3fcbfa85u}, {0xc0d9c130u, 0xbd9ebbffu, 0xbfca8510u}, {0xbe077b06u, 0x40786368u, 0xbd0b9407u}, + {0xbe51a574u, 0xbdf4773fu, 0xc006505au}, {0x3fe6cdabu, 0xbf7fff9bu, 0x4004f006u}, {0x3f800000u, 0x3f800000u, 0x3f490fdbu}, + {0x40000000u, 0x3f800000u, 0x3f8db70du}, {0x00000000u, 0xbf800000u, 0x40490fdbu}, {0x80000000u, 0xbf800000u, 0xc0490fdbu}, + {0x3f800000u, 0x00000000u, 0x3fc90fdbu}, {0xbf800000u, 0x80000000u, 0xbfc90fdbu}, {0x7f800000u, 0x7f800000u, 0x3f490fdbu}, + {0xff800000u, 0xff800000u, 0xc016cbe4u}, {0x3f800000u, 0xff800000u, 0x40490fdbu}, {0xbf800000u, 0x7f800000u, 0x80000000u}, + {0x7f800000u, 0x3f000000u, 0x3fc90fdbu}, {0x4e000000u, 0x3f800001u, 0x3fc90fdbu}, {0x30000000u, 0xc0000000u, 0x40490fdbu}, + {0x7fc00000u, 0xffc00001u, 0x7fc00000u}, {0x3f800000u, 0x7fc00002u, 0x7fc00002u}, + }; + for (const auto& [y, x, expected] : cases) { + Require(std::bit_cast(atan2f_nid_postfix(std::bit_cast(y), std::bit_cast(x))) == expected); + } +} + static void CheckHypotSpecialValue() { Require(std::bit_cast(hypot_nid_postfix(INFINITY, std::bit_cast(0x7ff0000000000001ull))) == 0x7ff0000000000000ull); Require(std::bit_cast(hypot_nid_postfix(std::bit_cast(0x7ff0000000000001ull), INFINITY)) == 0x7ff0000000000000ull); @@ -421,7 +554,33 @@ static void CheckExp2Errno() { } static void CheckPowfSpecialValue() { - Require(std::bit_cast(powf_nid_postfix(1.0f, std::bit_cast(0x7f800001u))) == 0x3f800000u); + constexpr struct { + std::uint32_t x; + std::uint32_t y; + std::uint32_t expected; + int error; + } cases[] = { + {0x3f9e2cf9u, 0xbf5a98dcu, 0x3f55ab48u, 0}, {0x3fad4946u, 0x40c1ad7au, 0x40c82840u, 0}, {0x3f1467ddu, 0x40b9b6f1u, 0x3d2d08eeu, 0}, + {0x3fda2303u, 0x3f002254u, 0x3fa724dau, 0}, {0x3ff3e78du, 0xc1db422eu, 0x32b5d534u, 0}, {0x3f8d3e01u, 0x3f9f7952u, 0x3f90b3f4u, 0}, + {0x3f800000u, 0x7f800001u, 0x7fc00001u, 0}, {0x3f800000u, 0x7fc00000u, 0x7fc00000u, 0}, {0x3f800000u, 0x7f800000u, 0xffc00000u, 0}, + {0xbf800000u, 0xff800000u, 0xffc00000u, 0}, {0x3f800000u, 0x40000000u, 0x3f800000u, 0}, {0x40000000u, 0x00000000u, 0x3f800000u, 0}, + {0x7fc00001u, 0x00000000u, 0x3f800000u, 0}, {0x40000000u, 0x3f000000u, 0x3fb504f3u, 0}, {0x40800000u, 0x3f000000u, 0x40000000u, 0}, + {0xc0000000u, 0x40400000u, 0xc1000000u, 0}, {0xc0000000u, 0x3fc00000u, 0xffc00000u, EDOM}, {0xbf800000u, 0x3fc00000u, 0xffc00000u, EDOM}, + {0x00000000u, 0xc0000000u, 0x7f800000u, 0}, {0x80000000u, 0xc0400000u, 0xff800000u, 0}, {0x42c80000u, 0x42c80000u, 0x7f800000u, ERANGE}, + {0x3c23d70au, 0x42c80000u, 0x00000000u, ERANGE}, {0x40000000u, 0x43000000u, 0x7f800000u, ERANGE}, {0x40000000u, 0xc3160000u, 0x00000000u, ERANGE}, + {0x40000000u, 0xc3150000u, 0x00000001u, 0}, {0x40000000u, 0x4e000000u, 0x7f800000u, 0}, {0x3f000000u, 0x4e000000u, 0x00000000u, 0}, + {0xc0000000u, 0x4f000000u, 0x7f800000u, 0}, {0x7f800000u, 0xbf800000u, 0x00000000u, 0}, {0xff800000u, 0x40400000u, 0xff800000u, 0}, + {0x7fc00001u, 0xffc00002u, 0x7fc00001u, 0}, {0xffc00002u, 0x7fc00001u, 0xffc00002u, 0}, {0x7f800001u, 0x7fc00003u, 0x7fc00001u, 0}, + {0x7fc00003u, 0x7f800001u, 0x7fc00003u, 0}, {0xff800002u, 0x7fc00003u, 0xffc00002u, 0}, {0x7fc00003u, 0xff800002u, 0x7fc00003u, 0}, + {0x7f800001u, 0xff800002u, 0x7fc00001u, 0}, {0xff800002u, 0x7f800001u, 0xffc00002u, 0}, {0x7fffffffu, 0xffffffffu, 0x7fffffffu, 0}, + {0x7f800001u, 0x00000000u, 0x3f800000u, 0}, {0xff800002u, 0x80000000u, 0x3f800000u, 0}, {0xff800002u, 0x7f800000u, 0xffc00002u, 0}, + {0xff800000u, 0xff800002u, 0xffc00002u, 0}, + }; + for (const auto& item : cases) { + errno = 1234; + Require(std::bit_cast(powf_nid_postfix(std::bit_cast(item.x), std::bit_cast(item.y))) == item.expected); + Require(errno == (item.error != 0 ? item.error : 1234)); + } } struct RestoreFloatingPointState { @@ -1427,8 +1586,7 @@ static void CheckFloatClassification() { std::fprintf(stderr, "Guest _FDtest failed for %08x\n", test.bits); std::abort(); } - const int fpclass = test.code == 0 ? 0x10 : test.code == -2 ? 0x08 : test.code == -1 ? 0x04 : test.code == 1 ? 0x01 : 0x02; - if (__fpclassifyf_nid_postfix(value) != fpclass) { + if (__fpclassifyf_nid_postfix(value) != test.code) { std::fprintf(stderr, "Guest __fpclassifyf failed for %08x\n", test.bits); std::abort(); } @@ -1465,6 +1623,58 @@ static void CheckLogfConsoleBits() { } } +static void CheckFrexpfEnvironment() { + constexpr struct { + std::uint32_t input; + std::uint32_t fraction; + int exponent; + } cases[] = { + {0x00000000u, 0x00000000u, 0}, {0x80000000u, 0x80000000u, 0}, + {0x00000001u, 0x3f000000u, -148}, {0x80000001u, 0xbf000000u, -148}, + {0x007fffffu, 0x3f7ffffeu, -126}, {0x807fffffu, 0xbf7ffffeu, -126}, + {0x00800000u, 0x3f000000u, -125}, {0x80800000u, 0xbf000000u, -125}, + {0x3f000000u, 0x3f000000u, 0}, {0x3f800000u, 0x3f000000u, 1}, + {0xbf800000u, 0xbf000000u, 1}, {0x41400000u, 0x3f400000u, 4}, + {0x7f7fffffu, 0x3f7fffffu, 128}, {0xff7fffffu, 0xbf7fffffu, 128}, + {0x7f800000u, 0x7f800000u, 0}, {0xff800000u, 0xff800000u, 0}, + {0x7f800001u, 0x7f800001u, 0}, {0xff800001u, 0xff800001u, 0}, + {0x7fc01234u, 0x7fc01234u, 0}, {0xffc01234u, 0xffc01234u, 0}, + }; + std::fenv_t saved; + Require(std::fegetenv(&saved) == 0); + const unsigned savedCsr = _mm_getcsr(); + constexpr int rounding[] = {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}; + const int savedError = *__error_nid_postfix(); + for (unsigned mode = 0; mode != 4; ++mode) { + for (unsigned denorm = 0; denorm != 4; ++denorm) { + const unsigned control = 0x1f80u | (mode << 13) | ((denorm & 1u) ? 0x40u : 0u) | ((denorm & 2u) ? 0x8000u : 0u); + for (const unsigned sticky : {0u, 5u, 32u}) { + for (const auto& item : cases) { + Require(std::fesetenv(FE_DFL_ENV) == 0); + Require(std::fesetround(rounding[mode]) == 0); + *__error_nid_postfix() = 97; + _mm_setcsr(control | sticky); + int exponent = 77; + const auto fraction = std::bit_cast(frexpf_nid_postfix(std::bit_cast(item.input), &exponent)); + const unsigned after = _mm_getcsr(); + const bool subnormal = (item.input & 0x7fffffffu) != 0 && (item.input & 0x7fffffffu) < 0x00800000u; + const bool flushed = subnormal && (denorm & 1u) != 0; + const unsigned flags = sticky | (subnormal && !flushed ? 2u : 0u); + const auto expectedFraction = flushed ? (item.input & 0x80000000u) | 0x3f000000u : item.fraction; + const int expectedExponent = flushed ? -151 : item.exponent; + if (fraction != expectedFraction || exponent != expectedExponent || after != (control | flags) || *__error_nid_postfix() != 97) { + std::fprintf(stderr, "frexpf input=%08x mode=%u denorm=%u sticky=%u fraction=%08x exponent=%d csr=%x\n", item.input, mode, denorm, sticky, fraction, exponent, after); + std::abort(); + } + } + } + } + } + Require(std::fesetenv(&saved) == 0); + _mm_setcsr(savedCsr); + *__error_nid_postfix() = savedError; +} + static void CheckDinkumwareConstants() { RequireBytes(&_Inf_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); RequireBytes(&_Denorm_nid_postfix, {1}); @@ -1618,6 +1828,10 @@ static void CheckLongDoubleFunctions() { Require(tgammaf_nid_postfix(5.0f) == 24.0f && std::abs(tgammaf_nid_postfix(0.5f) - 1.77245385f) < 1e-6f); Require(tgammaf_nid_postfix(1.0f) == 1.0f && tgammaf_nid_postfix(2.0f) == 1.0f && tgammaf_nid_postfix(3.0f) == 2.0f); Require(std::isinf(tgammaf_nid_postfix(0.0f)) && std::isnan(tgammaf_nid_postfix(-1.0f))); + const float negativeGammaUnderflow = tgammaf_nid_postfix(-4260.884765625f); + const float positiveGammaUnderflow = tgammaf_nid_postfix(-4261.2f); + Require(negativeGammaUnderflow == 0.0f && std::signbit(negativeGammaUnderflow)); + Require(positiveGammaUnderflow == 0.0f && !std::signbit(positiveGammaUnderflow)); Require(hypotl_nid_postfix(3.0L, -4.0L) == 5.0L && hypotl_nid_postfix(nan, -infinity) == infinity); Require(hypot3f_nid_postfix(3.0f, -4.0f, 12.0f) == 13.0f && hypot3f_nid_postfix(0.0f, 3.0f, -4.0f) == 5.0f); @@ -1770,6 +1984,57 @@ static void CheckDinkumwareFloatCharacteristics() { for (std::size_t index = 24; index < 32; ++index) Require(reinterpret_cast(table)[index] == 0); } +static void CheckLogbConsoleBits() { + constexpr struct { + std::uint64_t input; + std::uint64_t expected; + } cases[] = { + {0x0000000000000000ull, 0xfff0000000000000ull}, + {0x8000000000000000ull, 0xfff0000000000000ull}, + {0x0000000000000001ull, 0xc090c80000000000ull}, + {0x8000000000000001ull, 0xc090c80000000000ull}, + {0x000fffffffffffffull, 0xc08ff80000000000ull}, + {0x0010000000000000ull, 0xc08ff00000000000ull}, + {0x3fe8000000000000ull, 0xbff0000000000000ull}, + {0x4020000000000000ull, 0x4008000000000000ull}, + {0xc020000000000000ull, 0x4008000000000000ull}, + {0x7fefffffffffffffull, 0x408ff80000000000ull}, + {0x7ff0000000000000ull, 0x7ff0000000000000ull}, + {0xfff0000000000000ull, 0x7ff0000000000000ull}, + {0x7ff0000000000001ull, 0x7ff8000000000001ull}, + {0xfff4000000000123ull, 0xfffc000000000123ull}, + {0x7ff8000000000456ull, 0x7ff8000000000456ull}, + }; + for (const auto& item : cases) { + errno = 1234; + Require(std::bit_cast(logb_nid_postfix(std::bit_cast(item.input))) == item.expected && errno == 1234); + } + constexpr struct { + std::uint32_t input; + std::uint32_t expected; + } floatCases[] = { + {0x00000000u, 0xff800000u}, + {0x80000000u, 0xff800000u}, + {0x00000001u, 0xc3150000u}, + {0x80000001u, 0xc3150000u}, + {0x007fffffu, 0xc2fe0000u}, + {0x00800000u, 0xc2fc0000u}, + {0x3f400000u, 0xbf800000u}, + {0x41000000u, 0x40400000u}, + {0xc1000000u, 0x40400000u}, + {0x7f7fffffu, 0x42fe0000u}, + {0x7f800000u, 0x7f800000u}, + {0xff800000u, 0x7f800000u}, + {0x7f800001u, 0x7fc00001u}, + {0xffa00123u, 0xffe00123u}, + {0x7fc00456u, 0x7fc00456u}, + }; + for (const auto& item : floatCases) { + errno = 1234; + Require(std::bit_cast(logbf_nid_postfix(std::bit_cast(item.input))) == item.expected && errno == 1234); + } +} + int main() { CheckFloatClassification(); CheckDinkumwareConstants(); @@ -1791,6 +2056,7 @@ int main() { Require(*__error_nid_postfix() == 34); Require(strtold_nid_postfix("1.0000000000000000001!", &end) > 1.L && *end == '!'); CheckNanConversions(); + CheckModfDecomposition(); Require(fmodf_nid_postfix(5.5f, 2.f) == 1.5f); Require(fmodf_nid_postfix(-5.5f, 2.f) == -1.5f); Require(std::signbit(fmodf_nid_postfix(-4.f, 2.f))); @@ -1936,6 +2202,7 @@ int main() { CheckFegetenv(); CheckLongDouble(); CheckNearbyintl(); + CheckLogbConsoleBits(); CheckExpm1ConsoleBits(); CheckLog1pConsoleBits(); CheckNextafterConsoleBits(); @@ -2077,6 +2344,7 @@ int main() { CheckLogfConsoleBits(); CheckHypotSpecialValue(); CheckAtanfConsoleBits(); + CheckAtan2fConsoleBits(); CheckFloatUnderflowErrno(); CheckFmodErrno(); CheckFmodfErrno(); @@ -2086,4 +2354,5 @@ int main() { CheckLongDoubleFunctions(); CheckLongDoubleHyperbolic(); CheckDinkumwareFloatCharacteristics(); + CheckFrexpfEnvironment(); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index ffaa8e6ec4..a0d0df9f03 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -22,7 +22,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -231,6 +233,25 @@ static void CheckMprotectIgnoresUndefinedBits() { Require(sceKernelReleaseDirectMemory(physical, length) == 0); } +static void CheckDirectMapRefusesExecute() { + constexpr std::size_t length = 0x4000; + constexpr int invalid = static_cast(0x80020016u); + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, length, 0, 0, &physical) == 0); + for (const int protection : {5, 7, 0x35, 0x37}) { + void* refused = nullptr; + Require(sceKernelMapDirectMemory(&refused, length, protection, 0, physical, 0) == invalid); + Require(refused == nullptr); + } + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, length, 3, 0, physical, 0) == 0); + Require(sceKernelMprotect(mapping, length, 7) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapping, 0, &info, sizeof(info)) == 0 && info.protection == 7); + Require(sceKernelMunmap(reinterpret_cast(mapping), length) == 0); + Require(sceKernelReleaseDirectMemory(physical, length) == 0); +} + static void CheckStrictMemoryProtectionBits() { constexpr std::size_t length = 0x4000; std::int64_t physical = 0; @@ -3052,6 +3073,61 @@ static void CheckRegularFileMapping() { } #endif +static void CheckMappedLeasesPreserveReadableRuns() { + constexpr std::uintptr_t base = 0x7e0000000000ull; + constexpr std::size_t page = 0x4000; + const auto pointer = [&](std::size_t index) { return reinterpret_cast(base + index * page); }; + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < 8; ++index) mutation.Add(pointer(index), page, index != 3, index != 3 && index != 4, index != 5); + } + const auto weak = [&](std::size_t index) { return std::weak_ptr(GuestAllocations::GuestAllocationsLookup_nid_postfix(base + index * page, false)); }; + const auto verify = [&](const GuestAllocations::Mapped& mapped, std::initializer_list expected) { + const auto lease = GuestAllocations::GuestAllocationsAcquireMapped_nid_no_patch(mapped); + Require(lease.size() == expected.size()); + std::size_t position = 0; + for (const auto index : expected) Require(lease[position++]->address == base + index * page); + }; + verify({}, {}); + verify({weak(1)}, {0, 1, 2}); + verify({weak(2), weak(0), weak(1), weak(1)}, {0, 1, 2}); + verify({weak(3)}, {}); + verify({weak(4)}, {4}); + verify({weak(5)}, {5}); + verify({weak(7), weak(1)}, {0, 1, 2, 6, 7}); + auto stale = std::make_shared(*GuestAllocations::GuestAllocationsLookup_nid_postfix(base + page, false)); + { + GuestAllocations::Mutation mutation; + mutation.Remove(pointer(1)); + mutation.Add(pointer(1), page, true, true, true); + } + verify({stale}, {}); + stale.reset(); + verify({weak(1)}, {0, 1, 2}); + { + const auto lease = GuestAllocations::GuestAllocationsAcquireMapped_nid_no_patch({weak(1)}); + auto neighbor = weak(0); + Require(neighbor.use_count() >= 2); + { + GuestAllocations::Mutation mutation; + mutation.Add(pointer(10), page, true, true, true); + } + Require(lease.size() == 3 && neighbor.use_count() >= 2); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(pointer(1)); + } + verify({weak(0)}, {0}); + verify({weak(2)}, {2}); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < 8; ++index) if (index != 1) mutation.Remove(pointer(index)); + mutation.Remove(pointer(10)); + } + verify({weak(0)}, {}); +} + static void CheckVirtualQuerySplitFlexibleRanges() { constexpr std::size_t page = 0x4000; void* mapped = nullptr; @@ -3385,6 +3461,7 @@ int main(int argc, char** argv) { } Require(argc == 1); CheckAvailableDirectMemoryIsLargestFreeBlock(); + CheckMappedLeasesPreserveReadableRuns(); CheckVirtualQuerySplitFlexibleRanges(); CheckVirtualQueryForNonReadableGuestRange(); CheckReleaseFlexibleMemory(); @@ -3406,6 +3483,7 @@ int main(int argc, char** argv) { CheckAudioCoprocessorProtection(); CheckMprotectIgnoresUndefinedBits(); CheckStrictMemoryProtectionBits(); + CheckDirectMapRefusesExecute(); CheckPosixMprotectIgnoresUndefinedBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); @@ -3570,10 +3648,10 @@ int main(int argc, char** argv) { mincoreVec[0] = 0x55; Require(mincore_nid_postfix(mincoreReserved, page, mincoreVec) == 0 && mincoreVec[0] == 0); Require(sceKernelMunmap(reinterpret_cast(mincoreReserved), page) == 0); - Require(munmap_nid_postfix(memory + 1, page) == -1 && *__error_nid_postfix() == 22); + Require(munmap_nid_postfix(memory + 1, 0) == -1 && *__error_nid_postfix() == 22); Require(munmap_nid_postfix(memory, 0) == -1 && *__error_nid_postfix() == 22); Require(memory[0] == 42); - Require(munmap_nid_postfix(memory + page, 1) == 0); // round to one guest page + Require(munmap_nid_postfix(memory + page + 1, 1) == 0); Require(memory[0] == 42 && memory[page * 2] == 73); Require(mincore_nid_postfix(memory, page, mincoreVec) == 0 && mincoreVec[0] == 1); Require(mincore_nid_postfix(memory, page * 3, mincoreVec) == -1 && *__error_nid_postfix() == 12); diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index 21383b3c87..7e25847352 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -31,7 +31,9 @@ int main() { Require(sceMsgDialogTerminate() == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); + Require(sceMsgDialogOpen(nullptr) == kErrNotInitialized); Require(sceMsgDialogOpen(param) == kErrNotInitialized); + Require(sceMsgDialogGetResult(nullptr) == kErrNotInitialized); Require(sceMsgDialogGetResult(&result) == kErrNotInitialized); Require(sceMsgDialogUpdateStatus() == 0); Require(sceMsgDialogGetStatus() == 0); @@ -49,6 +51,8 @@ int main() { Require(sceMsgDialogOpen(param) == 0); Require(sceMsgDialogUpdateStatus() == kStatusFinished); Require(sceMsgDialogGetStatus() == kStatusFinished); + Require(sceMsgDialogOpen(nullptr) == kErrArgNull); + Require(sceMsgDialogOpen(param) == 0); Require(sceMsgDialogProgressBarInc(0, 10) == kErrNotRunning); Require(sceMsgDialogProgressBarSetMsg(0, "progress") == kErrNotRunning); Require(sceMsgDialogProgressBarSetValue(0, 100) == kErrNotRunning); @@ -64,7 +68,9 @@ int main() { Require(sceMsgDialogTerminate() == 0); Require(sceMsgDialogTerminate() == kErrNotInitialized); + Require(sceMsgDialogOpen(nullptr) == kErrNotInitialized); Require(sceMsgDialogOpen(param) == kErrNotInitialized); + Require(sceMsgDialogGetResult(nullptr) == kErrNotInitialized); Require(sceMsgDialogGetResult(&result) == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogGetStatus() == 0); diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index fa070adf54..561eeb0586 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -6,16 +6,17 @@ #else #include #include +#include #include #include #endif #ifdef __APPLE__ #include #include -#include #endif #include "prx/libc/include/general/VabiMacros.hpp" #include "SceTypes.hpp" +#include #include #include #include @@ -23,6 +24,8 @@ #include #include #include +#include +#include #ifdef __APPLE__ struct NetCtlDefaultRoute { @@ -38,6 +41,7 @@ extern "C" { #ifdef __APPLE__ std::size_t NetCtlDefaultRoutes_nid_no_patch(const char*, std::size_t, NetCtlDefaultRoute*, std::size_t); #endif +void NetCtlSetStunServers_nid_no_patch(const sockaddr_in*, std::size_t, int); } @@ -102,10 +106,15 @@ void CheckCallbacks() { int v6 = -1; Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0); + int stateV6 = -1; + Require(sceNetCtlGetStateV6(&stateV6) == 0); + const int eventV6 = stateV6 == kStateIpObtained ? kEventIpObtained : kEventDisconnected; + int first = -1; Require(sceNetCtlRegisterCallback(RecordFirst, &firstProbe, &first) == 0 && first == 0); Require(sceNetCtlCheckCallback() == 0); Require(g_first.calls == 1 && g_first.event == event && g_first.arg == &firstProbe); + Require(g_v6.calls == 1 && g_v6.event == eventV6 && g_v6.arg == &v6Probe); Require(sceNetCtlCheckCallback() == 0); Require(g_first.calls == 1); @@ -144,7 +153,8 @@ void CheckCallbacks() { Require(sceNetCtlRegisterCallback(RecordFirst, &secondProbe, &first) == 0 && first == 0); Require(sceNetCtlCheckCallback() == 0); Require(g_first.calls == 2 && g_first.event == event && g_first.arg == &secondProbe); - Require(g_second.calls == 2 && g_self.calls == 1 && g_v6.calls == 0); + Require(g_second.calls == 2 && g_self.calls == 1 && g_v6.calls == 2); + Require(g_v6.event == eventV6 && g_v6.arg == &v6Probe); Require(sceNetCtlUnregisterCallback(first) == 0); Require(sceNetCtlUnregisterCallbackV6(v6) == 0); } @@ -226,12 +236,17 @@ void CheckIpcIntCallbacks() { Require(sceNetCtlUnregisterCallback(internal) == 0); Require(sceNetCtlUnregisterCallbackIpcInt(internal) == idNotFound); + int stateV6 = -1; + Require(sceNetCtlGetStateV6(&stateV6) == 0); + const int eventV6 = stateV6 == kStateIpObtained ? kEventIpObtained : kEventDisconnected; int internalV6 = -1; Require(sceNetCtlRegisterCallbackV6IpcInt(RecordIpcInt, &probe, &internalV6) == 0 && internalV6 == 0); int sharedV6 = -1; Require(sceNetCtlRegisterCallbackV6(RecordIpcInt, &probe, &sharedV6) == 0 && sharedV6 == 1); Require(sceNetCtlCheckCallback() == 0); - Require(g_ipcInt.calls == 2); + Require(g_ipcInt.calls == 4 && g_ipcInt.event == eventV6 && g_ipcInt.arg == &probe); + Require(sceNetCtlCheckCallback() == 0); + Require(g_ipcInt.calls == 4 && g_ipcInt.event == eventV6 && g_ipcInt.arg == &probe); Require(sceNetCtlUnregisterCallbackV6IpcInt(sharedV6) == 0); Require(sceNetCtlUnregisterCallbackV6(internalV6) == 0); Require(sceNetCtlUnregisterCallbackV6IpcInt(internalV6) == idNotFound); @@ -285,6 +300,250 @@ bool Binds(in_addr address) { return bound; } +#ifdef _WIN32 +using TestSocket = SOCKET; +constexpr TestSocket kInvalidSocket = INVALID_SOCKET; +void CloseTestSocket(TestSocket descriptor) { closesocket(descriptor); } +#else +using TestSocket = int; +constexpr TestSocket kInvalidSocket = -1; +void CloseTestSocket(TestSocket descriptor) { close(descriptor); } +#endif + +TestSocket OpenTestUdpSocket(sockaddr_in& bound) { + const TestSocket descriptor = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + Require(descriptor != kInvalidSocket); + sockaddr_in local{}; + local.sin_family = AF_INET; + local.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + local.sin_port = 0; + Require(bind(descriptor, reinterpret_cast(&local), sizeof(local)) == 0); +#ifdef _WIN32 + int length = sizeof(bound); +#else + socklen_t length = sizeof(bound); +#endif + Require(getsockname(descriptor, reinterpret_cast(&bound), &length) == 0); + return descriptor; +} + +bool WaitReadable(TestSocket descriptor, long timeoutMs) { + fd_set readSet; + FD_ZERO(&readSet); + FD_SET(descriptor, &readSet); + timeval tv{}; + tv.tv_sec = timeoutMs / 1000; + tv.tv_usec = (timeoutMs % 1000) * 1000; +#ifdef _WIN32 + return select(0, &readSet, nullptr, nullptr, &tv) > 0; +#else + return select(descriptor + 1, &readSet, nullptr, nullptr, &tv) > 0; +#endif +} + +void AppendStunAddressAttribute(std::vector& packet, std::uint16_t attrType, std::uint32_t address, std::uint16_t hostPort, bool xorMask) { + constexpr std::uint32_t kCookie = 0x2112A442u; + const std::uint16_t wirePort = xorMask ? static_cast(hostPort ^ static_cast(kCookie >> 16)) : hostPort; + const std::uint32_t wireAddr = xorMask ? (address ^ htonl(kCookie)) : address; + packet.push_back(static_cast(attrType >> 8)); + packet.push_back(static_cast(attrType & 0xFF)); + packet.push_back(0x00); + packet.push_back(0x08); + packet.push_back(0x00); + packet.push_back(0x01); + packet.push_back(static_cast(wirePort >> 8)); + packet.push_back(static_cast(wirePort & 0xFF)); + const auto* addrBytes = reinterpret_cast(&wireAddr); + packet.insert(packet.end(), addrBytes, addrBytes + 4); +} + +using StunTxId = std::array; + +StunTxId ReceiveStunBindingRequest(TestSocket serverSocket, sockaddr_in& client) { + Require(WaitReadable(serverSocket, 2000)); + std::uint8_t request[64] = {}; +#ifdef _WIN32 + int clientLen = sizeof(client); +#else + socklen_t clientLen = sizeof(client); +#endif + const auto received = recvfrom(serverSocket, reinterpret_cast(request), sizeof(request), 0, + reinterpret_cast(&client), &clientLen); + Require(received == 20); + Require(request[0] == 0x00 && request[1] == 0x01 && request[2] == 0x00 && request[3] == 0x00); + const std::uint32_t expectedCookie = htonl(0x2112A442u); + Require(std::memcmp(request + 4, &expectedCookie, sizeof(expectedCookie)) == 0); + StunTxId txId{}; + std::memcpy(txId.data(), request + 8, txId.size()); + Require(txId != StunTxId{}); + return txId; +} + +void SendStunBindingResponse(TestSocket serverSocket, const sockaddr_in& client, const StunTxId& txId, std::uint32_t mappedAddr, std::uint16_t mappedHostPort, bool xorMapped, const sockaddr_in* otherServer = nullptr) { + const std::uint16_t effectivePort = mappedHostPort != 0 ? mappedHostPort : ntohs(client.sin_port); + const std::uint32_t effectiveAddr = mappedAddr != 0 ? mappedAddr : client.sin_addr.s_addr; + std::vector response(20, 0); + response[0] = 0x01; + response[1] = 0x01; + const std::uint32_t cookie = htonl(0x2112A442u); + std::memcpy(response.data() + 4, &cookie, sizeof(cookie)); + std::memcpy(response.data() + 8, txId.data(), txId.size()); + AppendStunAddressAttribute(response, xorMapped ? 0x0020 : 0x0001, effectiveAddr, effectivePort, xorMapped); + if (otherServer) { + AppendStunAddressAttribute(response, 0x0005, otherServer->sin_addr.s_addr, ntohs(otherServer->sin_port), false); + } + const auto attrLen = static_cast(response.size() - 20); + response[2] = static_cast(attrLen >> 8); + response[3] = static_cast(attrLen & 0xFF); +#ifdef _WIN32 + const int clientLen = sizeof(client); +#else + const socklen_t clientLen = sizeof(client); +#endif + const auto sent = sendto(serverSocket, reinterpret_cast(response.data()), static_cast(response.size()), 0, + reinterpret_cast(&client), clientLen); + Require(sent == static_cast(response.size())); +} + +void RecordUniqueTransactionId(std::vector& seen, const StunTxId& candidate) { + for (const auto& existing : seen) Require(existing != candidate); + seen.push_back(candidate); +} + +void CheckNatDiscovery(in_addr address) { + sockaddr_in server1Addr{}; + sockaddr_in server2Addr{}; + const TestSocket server1 = OpenTestUdpSocket(server1Addr); + const TestSocket server2 = OpenTestUdpSocket(server2Addr); + std::vector seenIds; + + NetCtlNatInfo natInfo{}; + natInfo.size = sizeof(natInfo); + natInfo.stun_status = 99; + natInfo.nat_type = 99; + natInfo.mapped_addr = 0xFFFFFFFFu; + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 40); + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 1); + Require(natInfo.nat_type == 0); + Require(natInfo.mapped_addr == 0); + { + sockaddr_in from{}; + RecordUniqueTransactionId(seenIds, ReceiveStunBindingRequest(server1, from)); + } + + StunTxId openTx{}; + std::thread openWorker([&] { + sockaddr_in client{}; + openTx = ReceiveStunBindingRequest(server1, client); + SendStunBindingResponse(server1, client, openTx, address.s_addr, 0, true); + }); + natInfo = {sizeof(natInfo), 99, 99, 0}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + openWorker.join(); + RecordUniqueTransactionId(seenIds, openTx); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 2); + Require(natInfo.nat_type == 1); + Require(natInfo.mapped_addr == address.s_addr); + + const std::uint32_t externalIp = (ntohl(address.s_addr) == 0xCB00712Au) ? htonl(0xCB00712Bu) : htonl(0xCB00712Au); + const std::uint32_t spoofedIp = htonl(0xC0000201u); + + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 30); + StunTxId mismatchedOnlyTx{}; + std::thread mismatchOnlyWorker([&] { + sockaddr_in client{}; + mismatchedOnlyTx = ReceiveStunBindingRequest(server1, client); + StunTxId badTx = mismatchedOnlyTx; + badTx[0] ^= 0xFFu; + SendStunBindingResponse(server1, client, badTx, spoofedIp, 54321, true); + }); + natInfo = {sizeof(natInfo), 99, 99, 0xFFFFFFFFu}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + mismatchOnlyWorker.join(); + RecordUniqueTransactionId(seenIds, mismatchedOnlyTx); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 1); + Require(natInfo.nat_type == 0); + Require(natInfo.mapped_addr == 0); + + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); + StunTxId incompleteTx{}; + std::thread incompleteWorker([&] { + sockaddr_in client{}; + incompleteTx = ReceiveStunBindingRequest(server1, client); + SendStunBindingResponse(server1, client, incompleteTx, externalIp, 54321, true); + }); + natInfo = {sizeof(natInfo), 99, 99, 0xFFFFFFFFu}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + incompleteWorker.join(); + RecordUniqueTransactionId(seenIds, incompleteTx); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 1); + Require(natInfo.nat_type == 0); + Require(natInfo.mapped_addr == 0); + + const sockaddr_in bothServers[2] = {server1Addr, server2Addr}; + NetCtlSetStunServers_nid_no_patch(bothServers, 2, 1000); + StunTxId type2Tx1{}; + StunTxId type2Tx2{}; + std::thread type2Worker([&] { + sockaddr_in firstClient{}; + type2Tx1 = ReceiveStunBindingRequest(server1, firstClient); + SendStunBindingResponse(server1, firstClient, type2Tx1, externalIp, 54321, true); + sockaddr_in secondClient{}; + type2Tx2 = ReceiveStunBindingRequest(server2, secondClient); + Require(firstClient.sin_addr.s_addr == secondClient.sin_addr.s_addr); + Require(firstClient.sin_port == secondClient.sin_port); + StunTxId wrongTx = type2Tx2; + wrongTx[11] ^= 0xA5u; + SendStunBindingResponse(server2, secondClient, wrongTx, spoofedIp, 59999, true); + SendStunBindingResponse(server2, secondClient, type2Tx2, externalIp, 54321, true); + }); + natInfo = {sizeof(natInfo), 99, 99, 0}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + type2Worker.join(); + RecordUniqueTransactionId(seenIds, type2Tx1); + RecordUniqueTransactionId(seenIds, type2Tx2); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 2); + Require(natInfo.nat_type == 2); + Require(natInfo.mapped_addr == externalIp); + + NetCtlNatInfo cachedInfo{sizeof(cachedInfo), 99, 99, 0}; + Require(sceNetCtlGetNatInfo(&cachedInfo) == 0); + Require(cachedInfo.stun_status == 2 && cachedInfo.nat_type == 2 && cachedInfo.mapped_addr == externalIp); + + NetCtlSetStunServers_nid_no_patch(&server1Addr, 1, 1000); + StunTxId type3Tx1{}; + StunTxId type3Tx2{}; + std::thread type3Worker([&] { + sockaddr_in firstClient{}; + type3Tx1 = ReceiveStunBindingRequest(server1, firstClient); + SendStunBindingResponse(server1, firstClient, type3Tx1, externalIp, 54321, false, &server2Addr); + sockaddr_in secondClient{}; + type3Tx2 = ReceiveStunBindingRequest(server2, secondClient); + Require(firstClient.sin_addr.s_addr == secondClient.sin_addr.s_addr); + Require(firstClient.sin_port == secondClient.sin_port); + SendStunBindingResponse(server2, secondClient, type3Tx2, externalIp, 54322, false); + }); + natInfo = {sizeof(natInfo), 99, 99, 0}; + Require(sceNetCtlGetNatInfo(&natInfo) == 0); + type3Worker.join(); + RecordUniqueTransactionId(seenIds, type3Tx1); + RecordUniqueTransactionId(seenIds, type3Tx2); + Require(natInfo.size == sizeof(natInfo)); + Require(natInfo.stun_status == 2); + Require(natInfo.nat_type == 3); + Require(natInfo.mapped_addr == externalIp); + + NetCtlSetStunServers_nid_no_patch(nullptr, 0, 0); + CloseTestSocket(server1); + CloseTestSocket(server2); +} + } #ifdef __APPLE__ @@ -438,14 +697,5 @@ int main() { natInfo.size = size; Require(sceNetCtlGetNatInfo(&natInfo) == static_cast(0x80412111)); } - natInfo.size = sizeof(natInfo); - std::memset(natInfo.opaque, kFill, sizeof(natInfo.opaque)); - bool natUnsupported = false; - try { - sceNetCtlGetNatInfo(&natInfo); - } catch (const std::logic_error&) { - natUnsupported = true; - } - Require(natUnsupported && natInfo.size == sizeof(natInfo)); - for (const auto value : natInfo.opaque) Require(value == kFill); + CheckNatDiscovery(address); } diff --git a/core/libs/tests/GuestNetCtlApDialog.cpp b/core/libs/tests/GuestNetCtlApDialog.cpp index 82b0318b87..f9dd7ae14d 100644 --- a/core/libs/tests/GuestNetCtlApDialog.cpp +++ b/core/libs/tests/GuestNetCtlApDialog.cpp @@ -37,8 +37,10 @@ int main() { std::memset(result, 0x5a, sizeof(result)); Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); + Require(sceNetCtlApDialogOpen(nullptr) == kErrArgNull); Require(sceNetCtlApDialogOpen(¶m) == kErrNotInitialized); Require(sceNetCtlApDialogClose() == kErrNotInitialized); + Require(sceNetCtlApDialogGetResult(nullptr) == kErrArgNull); Require(sceNetCtlApDialogGetResult(reinterpret_cast(result)) == kErrNotInitialized); Require(sceNetCtlApDialogGetStatus() == kStatusNone); Require(sceNetCtlApDialogUpdateStatus() == kStatusNone); @@ -78,11 +80,25 @@ int main() { Require(sceNetCtlApDialogClose() == static_cast(0x80B8000Bu)); Require(sceNetCtlApDialogTerminate() == 0); Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); + Require(sceNetCtlApDialogOpen(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogOpen(¶m) == kErrNotInitialized); + Require(sceNetCtlApDialogClose() == kErrNotInitialized); + Require(sceNetCtlApDialogGetResult(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogGetResult(reinterpret_cast(result)) == kErrNotInitialized); Require(sceNetCtlApDialogGetStatus() == kStatusNone); + Require(sceNetCtlApDialogUpdateStatus() == kStatusNone); Require(sceNetCtlApDialogInitialize() == 0); Require(sceNetCtlApDialogClose() == static_cast(0x80B8000Bu)); Require(sceNetCtlApDialogGetStatus() == kStatusInitialized); Require(sceNetCtlApDialogGetResult(reinterpret_cast(result)) == kErrNotFinished); Require(sceNetCtlApDialogTerminate() == 0); + Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); + Require(sceNetCtlApDialogOpen(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogOpen(¶m) == kErrNotInitialized); + Require(sceNetCtlApDialogClose() == kErrNotInitialized); + Require(sceNetCtlApDialogGetResult(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogGetResult(reinterpret_cast(result)) == kErrNotInitialized); + Require(sceNetCtlApDialogGetStatus() == kStatusNone); + Require(sceNetCtlApDialogUpdateStatus() == kStatusNone); } diff --git a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp index a8fecd529b..d75bc43596 100644 --- a/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp +++ b/core/libs/tests/GuestNpAuthAuthorizedAppDialog.cpp @@ -35,9 +35,12 @@ int main() { std::int32_t result[2] = {-1, -1}; Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusNone); Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogClose() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogInitialize() == 0); @@ -83,7 +86,13 @@ int main() { Require(sceNpAuthAuthorizedAppDialogClose() == static_cast(0x80B8000Bu)); Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogClose() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusNone); Require(sceNpAuthAuthorizedAppDialogInitialize() == 0); Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); @@ -91,8 +100,14 @@ int main() { Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusInitialized); Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotFinished); Require(sceNpAuthAuthorizedAppDialogTerminate() == 0); + Require(sceNpAuthAuthorizedAppDialogTerminate() == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogClose() == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogOpen(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogOpen(¶m) == kErrNotInitialized); Require(sceNpAuthAuthorizedAppDialogGetResult(nullptr) == kErrArgNull); + Require(sceNpAuthAuthorizedAppDialogGetResult(result) == kErrNotInitialized); + Require(sceNpAuthAuthorizedAppDialogGetStatus() == kStatusNone); + Require(sceNpAuthAuthorizedAppDialogUpdateStatus() == kStatusNone); Require(sceUserServiceTerminate() == 0); return 0; } diff --git a/core/libs/tests/GuestNpCppWebApiSignedOut.cpp b/core/libs/tests/GuestNpCppWebApiSignedOut.cpp index 826ceb8fa0..0cfea69f00 100644 --- a/core/libs/tests/GuestNpCppWebApiSignedOut.cpp +++ b/core/libs/tests/GuestNpCppWebApiSignedOut.cpp @@ -7,6 +7,7 @@ static void Require(bool value) { if (!value) std::abort(); } static constexpr int NotSignedIn = static_cast(0x80552907); +static constexpr int TransactionNotStarted = static_cast(0x80553508); using Object = int (APS5_VABI*)(void*); using Call = int (APS5_VABI*)(); @@ -99,6 +100,62 @@ static const RequestFlow flows[] = { }, }; +using Lifecycle = void (APS5_VABI*)(void*); +using Finish = int (APS5_VABI*)(void*); +using Identifier = std::int64_t (APS5_VABI*)(); + +struct TransactionLifecycle { + Lifecycle construct; + Lifecycle destroy; + Finish finish; +}; + +static const TransactionLifecycle lifecycles[] = { + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS2_6VectorINS4_INS1_13InGameCatalog2V59ContainerEEEEEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + }, + { + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev, + _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_17TitleCloudStorage2V132GetMultiDataStatusesResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv, + }, +}; + +static const Call aborts[] = { + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5abortEv, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V150PostPlayerSessionsSessionIdInvitationsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv, + _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5abortEv, +}; + +static const Identifier identifiers[] = { + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_15DefaultResponseENS2_12IntrusivePtrINS2_18ResponseHeaderBaseEEEE5getIdEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V129GetPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V130PostPlayerSessionsResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V152PostPlayerSessionsSessionIdMemberPlayersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv, + _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_14SessionManager2V161PostPlayerSessionsSessionIdJoinableSpecifiedUsersResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5getIdEv, +}; + +static void TestNeverStartedTransactions() { + for (const auto& lifecycle : lifecycles) { + alignas(16) unsigned char transaction[0x20]; + std::memset(transaction, 0x5a, sizeof(transaction)); + lifecycle.construct(transaction); + for (std::size_t index = 0; index < sizeof(transaction); ++index) + Require(transaction[index] == (index < 0x10 ? 0 : 0x5a)); + Require(lifecycle.finish(transaction) == TransactionNotStarted); + lifecycle.destroy(transaction); + for (std::size_t index = 0; index < sizeof(transaction); ++index) + Require(transaction[index] == (index < 0x10 ? 0 : 0x5a)); + } + for (const Call abort : aborts) Require(abort() == TransactionNotStarted); + for (const Identifier identifier : identifiers) Require(identifier() == 0); +} + static void TestRequestFlow(const RequestFlow& flow) { alignas(16) unsigned char transaction[0x100]; alignas(16) unsigned char parameter[0x100]; @@ -113,7 +170,7 @@ static void TestRequestFlow(const RequestFlow& flow) { Require(flow.call() == NotSignedIn); Require(flow.start() == NotSignedIn); Require(flow.getResponseHeaders() == NotSignedIn); - Require(flow.finish() == 0); + Require(flow.finish() == TransactionNotStarted); Require(flow.terminate() == 0); Require(flow.parameterDestroy(parameter) == 0); Require(flow.transactionDestroy(transaction) == 0); @@ -122,5 +179,6 @@ static void TestRequestFlow(const RequestFlow& flow) { int main() { for (const auto& flow : flows) TestRequestFlow(flow); Require(_ZN3sce2Np9CppWebApi15ProfanityFilter2V226WebApiFilterRequestFactory6createEPNS1_6Common10LibContextEPKcPNS5_12IntrusivePtrINS3_19WebApiFilterRequestEEE() == NotSignedIn); + TestNeverStartedTransactions(); return 0; } diff --git a/core/libs/tests/GuestNpFriendListDialog.cpp b/core/libs/tests/GuestNpFriendListDialog.cpp index 7e85ecf4cd..897d26ebe2 100644 --- a/core/libs/tests/GuestNpFriendListDialog.cpp +++ b/core/libs/tests/GuestNpFriendListDialog.cpp @@ -51,6 +51,11 @@ int main() { Require(sceNpFriendListDialogClose() == 0); Require(sceNpFriendListDialogTerminate() == 0); Require(sceNpFriendListDialogTerminate() == kErrNotInitialized); + Require(sceNpFriendListDialogOpenA(nullptr) == kErrNotInitialized); + Require(sceNpFriendListDialogOpenA(¶m) == kErrNotInitialized); + Require(sceNpFriendListDialogClose() == kErrNotInitialized); + Require(sceNpFriendListDialogGetResultA(nullptr) == kErrNotInitialized); + Require(sceNpFriendListDialogGetResultA(result) == kErrNotInitialized); Require(sceNpFriendListDialogGetStatus() == kStatusNone); Require(sceNpFriendListDialogUpdateStatus() == kStatusNone); @@ -62,4 +67,12 @@ int main() { Require(sceNpFriendListDialogGetResultA(result) == 0); Require(result[0] == kResultUserCanceled); Require(sceNpFriendListDialogTerminate() == 0); + Require(sceNpFriendListDialogTerminate() == kErrNotInitialized); + Require(sceNpFriendListDialogOpenA(nullptr) == kErrNotInitialized); + Require(sceNpFriendListDialogOpenA(¶m) == kErrNotInitialized); + Require(sceNpFriendListDialogClose() == kErrNotInitialized); + Require(sceNpFriendListDialogGetResultA(nullptr) == kErrNotInitialized); + Require(sceNpFriendListDialogGetResultA(result) == kErrNotInitialized); + Require(sceNpFriendListDialogGetStatus() == kStatusNone); + Require(sceNpFriendListDialogUpdateStatus() == kStatusNone); } diff --git a/core/libs/tests/GuestNpSnsFacebookDialog.cpp b/core/libs/tests/GuestNpSnsFacebookDialog.cpp index fd551e915f..507569039f 100644 --- a/core/libs/tests/GuestNpSnsFacebookDialog.cpp +++ b/core/libs/tests/GuestNpSnsFacebookDialog.cpp @@ -51,6 +51,11 @@ int main() { Require(sceNpSnsFacebookDialogClose() == 0); Require(sceNpSnsFacebookDialogTerminate() == 0); Require(sceNpSnsFacebookDialogTerminate() == kErrNotInitialized); + Require(sceNpSnsFacebookDialogOpen(nullptr) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogClose() == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetResult(result) == kErrNotInitialized); Require(sceNpSnsFacebookDialogGetStatus() == kStatusNone); Require(sceNpSnsFacebookDialogUpdateStatus() == kStatusNone); @@ -62,4 +67,12 @@ int main() { Require(sceNpSnsFacebookDialogGetResult(result) == 0); Require(result[0] == kResultUserCanceled); Require(sceNpSnsFacebookDialogTerminate() == 0); + Require(sceNpSnsFacebookDialogTerminate() == kErrNotInitialized); + Require(sceNpSnsFacebookDialogOpen(nullptr) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogOpen(¶m) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogClose() == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetResult(result) == kErrNotInitialized); + Require(sceNpSnsFacebookDialogGetStatus() == kStatusNone); + Require(sceNpSnsFacebookDialogUpdateStatus() == kStatusNone); } diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index 291dfdadd8..0c532feca4 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -69,6 +69,24 @@ int main() { Require(sceNpWebApi2IntInitialize2(&args2) == invalidArgument && sceNpWebApi2IntInitialize2(nullptr) == invalidArgument); const int presence = sceNpWebApi2InitializeForPresence(1, 0x2000); Require(presence > 0 && sceNpWebApi2GetMemoryPoolStats(presence, &stats) == 0 && stats.pool_size == 0x2000); + constexpr int unavailable = static_cast(0x80553406); + const int multipartUser = sceNpWebApi2CreateUserContext(presence, 1); + std::int64_t multipart = 0; + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, nullptr, "/v1/files", "POST", &multipart) == invalidArgument); + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, "fileManager", nullptr, "POST", &multipart) == invalidArgument); + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, "fileManager", "/v1/files", nullptr, &multipart) == invalidArgument); + Require(multipart == 0); + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, "fileManager", "/v1/files", "POST", &multipart) == 0 && multipart > 0); + std::int64_t nextMultipart = 0; + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, "fileManager", "/v1/files", "POST", &nextMultipart) == 0 && nextMultipart > multipart); + Require(sceNpWebApi2CreateMultipartRequest(multipartUser, "fileManager", "/v1/files", "POST", nullptr) == 0); + unsigned char part[4]{1, 2, 3, 4}; + Require(sceNpWebApi2SendMultipartRequest(multipart, 0, part, sizeof(part), nullptr) == invalidArgument); + Require(sceNpWebApi2SendMultipartRequest(multipart, -1, part, sizeof(part), nullptr) == invalidArgument); + Require(sceNpWebApi2SendMultipartRequest(multipart, 1, nullptr, sizeof(part), nullptr) == invalidArgument); + Require(sceNpWebApi2SendMultipartRequest(multipart, 1, part, 0, nullptr) == invalidArgument); + Require(sceNpWebApi2SendMultipartRequest(multipart, 1, part, sizeof(part), nullptr) == unavailable); + Require(sceNpWebApi2DeleteRequest(multipart) == 0 && sceNpWebApi2DeleteRequest(nextMultipart) == 0); for (const int created : {internal, internal2, presence}) { Require(sceNpWebApi2Terminate(created) == 0); Require(sceNpWebApi2GetMemoryPoolStats(created, &stats) == invalidContext); diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index 3dd2c7a0b7..9e441d4b0c 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -38,24 +38,6 @@ int main() { } } - for (std::int64_t requestId : {std::int64_t{0}, std::int64_t{1}, INT64_MAX}) { - bool abortFailed = false; - bool deleteFailed = false; - try { - sceNpEntitlementAccessAbortRequest(requestId); - - } catch (const std::logic_error&) { - abortFailed = true; - } - try { - sceNpEntitlementAccessDeleteRequest(requestId); - - } catch (const std::logic_error&) { - deleteFailed = true; - } - Require(abortFailed && deleteFailed); - } - constexpr int network = static_cast(0x80431063); Require(sceHttpSendRequest(1, nullptr, 0) == network); diff --git a/core/libs/tests/GuestOpusCeltEnc.cpp b/core/libs/tests/GuestOpusCeltEnc.cpp new file mode 100644 index 0000000000..99fae83ff6 --- /dev/null +++ b/core/libs/tests/GuestOpusCeltEnc.cpp @@ -0,0 +1,272 @@ +#include "prx/libSceOpusCeltEnc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value, const std::source_location& location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "Require failed at %s:%d\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +template +void RequireThrows(TFunction function) { + try { function(); } catch (const std::runtime_error&) { return; } + std::abort(); +} + +template +OpusCeltEncoder* ToState(std::array& block) { + return reinterpret_cast(block.data()); +} + +std::uint32_t Word(const std::uint8_t* data, std::size_t offset) { + std::uint32_t word = 0; + std::memcpy(&word, data + offset, sizeof word); + return word; +} + +struct SetupWord { + std::size_t offset; + std::uint32_t value; +}; + +void RequireState(const std::uint8_t* state, std::size_t stateSize, const SetupWord* words, std::size_t wordCount, std::uint8_t canary) { + std::array covered{}; + for (std::size_t index = 0; index < wordCount; ++index) { + Require(words[index].offset + 4 <= stateSize); + Require(Word(state, words[index].offset) == words[index].value); + for (std::size_t byte = 0; byte < 4; ++byte) covered[words[index].offset + byte] = true; + } + for (std::size_t index = 0; index < stateSize; ++index) + if (!covered[index]) Require(state[index] == 0); + Require(state[stateSize] == canary); +} + +std::uint32_t Bitrate(int rate, int channels) { + if (rate == 8000) return channels == 1 ? 11000u : 19000u; + if (rate == 12000) return channels == 1 ? 15000u : 27000u; + if (rate == 16000) return channels == 1 ? 19000u : 35000u; + if (rate == 24000) return channels == 1 ? 27000u : 51000u; + return channels == 1 ? 51000u : 99000u; +} + +std::array SetupWords(std::uint32_t instanceId, int rate, int channels) { + const auto bandwidth = Bitrate(rate, channels); + const auto rateWord = static_cast(rate); + const auto frameWord = static_cast(rate / 50); + const auto channelWord = static_cast(channels); + return {{ + {0x00, instanceId}, + {0x08, 8u * channelWord}, + {0x0C, 2u}, + {0x10, channelWord}, + {0x14, rateWord}, + {0x1C, bandwidth}, + {0x20, 2u}, + {0x24, 0x451u}, + {0x28, 0x5E0u}, + {0x2C, 10u}, + {0x40, frameWord}, + {0xA8, 0x803u}, + {0xAC, 0x451u}, + {0xB0, 0x451u}, + {0xB4, 1u}, + {0xB8, 1u}, + {0xBC, 0xFFFFFC18u}, + {0xC0, bandwidth}, + {0xC4, 0x10u}, + {0xC8, frameWord}, + {0xCC, 0x17B2u}, + }}; +} + +void FillSine(std::int16_t* samples, int frameSize, int channels, int rate, int position) { + for (int index = 0; index < frameSize; ++index) { + const int value = static_cast(3000 * std::sin(2 * 3.14159265358979323846 * 2000 * (position + index) / rate)); + for (int channel = 0; channel < channels; ++channel) samples[index * channels + channel] = static_cast(value); + } +} + +void RequireTone(const std::int16_t* pcm, int samples, int channels, int rate) { + for (int channel = 0; channel < channels; ++channel) { + int peak = 0, crossings = 0; + for (int sample = 0; sample < samples; ++sample) { + const int value = pcm[sample * channels + channel]; + peak = std::max(peak, std::abs(value)); + if (sample > 0) { + const int previous = pcm[(sample - 1) * channels + channel]; + if ((previous <= 0 && value > 0) || (previous >= 0 && value < 0)) ++crossings; + } + } + Require(peak > 30 && peak < 32000); + const int expected = 2 * samples * 2000 / rate; + Require(crossings >= expected - expected / 3 && crossings <= expected + expected / 3); + } +} + +} // namespace + +int main() { + constexpr std::uint32_t errorBase = 0x813F0000u; + constexpr std::size_t stateSize = 728; + constexpr int callerApplication = OPUS_APPLICATION_RESTRICTED_LOWDELAY; + constexpr int codecApplication = OPUS_APPLICATION_RESTRICTED_CELT; + std::uint32_t instanceId = 1; + Require(sceOpusCeltEncInitialize(nullptr) == static_cast(errorBase + 0x19)); + Require(sceOpusCeltEncDestroy(nullptr) == static_cast(errorBase + 0x01)); + std::uint32_t context = 0; + Require(sceOpusCeltEncInitialize(&context) == 0); + RequireThrows([&] { sceOpusCeltEncInitialize(&context); }); + RequireThrows([&] { sceOpusCeltEncTerminate(nullptr); }); + + std::array invalidState{}; + std::array samples{}; + samples.fill(12345); + std::array floatSamples{}; + std::uint8_t packet[1024]; + for (auto& byte : packet) byte = 0x5A; + Require(sceOpusCeltEncCreate(nullptr, 48000, 2, callerApplication) == static_cast(errorBase + 0x01)); + Require(sceOpusCeltEncCreate(ToState(invalidState), 48000, 0, callerApplication) == static_cast(errorBase + 0x0D)); + Require(sceOpusCeltEncCreate(ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x10)); + Require(sceOpusCeltEncCreate(ToState(invalidState), 22050, 2, callerApplication) == static_cast(errorBase + 0x10)); + Require(sceOpusCeltEncCreate(ToState(invalidState), 48000, 2, OPUS_APPLICATION_VOIP) == static_cast(errorBase + 0x11)); + Require(sceOpusCeltEncCreateEx(nullptr, ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x01)); + Require(sceOpusCeltEncCreateEx(&context, ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x10)); + Require(sceOpusCeltEncCreateEx(&context, ToState(invalidState), 48000, 0, callerApplication) == static_cast(errorBase + 0x0D)); + Require(sceOpusCeltEncCreateEx(&context, ToState(invalidState), 48000, 2, OPUS_APPLICATION_VOIP) == static_cast(errorBase + 0x11)); + Require(sceOpusCeltEncGetSize(1) == 728); + Require(sceOpusCeltEncGetSize(2) == 728); + Require(sceOpusCeltEncGetSize(3) == static_cast(errorBase + 0x0D)); + + std::array state{}; + state.fill(0x5A); + Require(sceOpusCeltEncCreateEx(&context, ToState(state), 48000, 2, callerApplication) == 0); + const auto words = SetupWords(instanceId, 48000, 2); + RequireState(state.data(), stateSize, words.data(), words.size(), 0x5A); + ++instanceId; + Require(sceOpusCeltEncEncode(nullptr, samples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x01)); + Require(sceOpusCeltEncEncode(ToState(state), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusCeltEncEncode(ToState(state), nullptr, 0, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 0, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), -1, nullptr, sizeof(packet)) == static_cast(errorBase + 0x04)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), -1, packet, 0) == static_cast(errorBase + 0x05)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), -1, nullptr, 0) == static_cast(errorBase + 0x04)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 1, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 119, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 959, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 5760, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(packet[0] == 0x5A && packet[1] == 0x5A); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 120, packet, sizeof(packet)) > 2); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 240, packet, sizeof(packet)) > 2); + RequireThrows([&] { sceOpusCeltEncEncode(ToState(state), samples.data(), 960, packet, -1); }); + Require(sceOpusCeltEncEncodeFloat(nullptr, floatSamples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x01)); + Require(sceOpusCeltEncEncodeFloat(ToState(state), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusCeltEncEncodeFloat(ToState(state), floatSamples.data(), 0, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusCeltEncEncodeFloat(ToState(invalidState), floatSamples.data(), -1, packet, sizeof(packet)) == static_cast(errorBase + 0x16)); + Require(sceOpusCeltEncEncodeFloat(ToState(invalidState), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + RequireThrows([&] { sceOpusCeltEncEncodeFloat(ToState(state), floatSamples.data(), -1, packet, sizeof(packet)); }); + RequireThrows([&] { sceOpusCeltEncEncodeFloat(ToState(state), floatSamples.data(), 1100000, packet, sizeof(packet)); }); + + auto snapshot = state; + Require(sceOpusCeltEncDestroy(ToState(state)) == 0); + for (std::size_t index = 0; index < stateSize; ++index) + if (index < 0xCC || index >= 0xD0) Require(state[index] == snapshot[index]); + Require(Word(state.data(), 0xCC) == 0); + Require(sceOpusCeltEncDestroy(ToState(state)) == static_cast(errorBase + 0x16)); + Require(sceOpusCeltEncEncode(ToState(state), samples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x16)); + Require(sceOpusCeltEncEncodeFloat(ToState(state), floatSamples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x16)); + + for (int rate : {8000, 12000, 16000, 24000, 48000}) { + for (int channels : {1, 2}) { + std::array matrixState{}; + matrixState.fill(0x5A); + Require(sceOpusCeltEncCreateEx(&context, ToState(matrixState), rate, channels, callerApplication) == 0); + const auto matrixWords = SetupWords(instanceId, rate, channels); + RequireState(matrixState.data(), stateSize, matrixWords.data(), matrixWords.size(), 0x5A); + ++instanceId; + int opusError = OPUS_OK; + auto* reference = opus_encoder_create(rate, channels, codecApplication, &opusError); + Require(reference && opusError == OPUS_OK); + const int frameSize = rate / 50; + for (int frame = 0; frame < 4; ++frame) { + FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); + const int bytes = sceOpusCeltEncEncode(ToState(matrixState), samples.data(), frameSize, packet, sizeof(packet)); + Require(bytes > 2 && static_cast(packet[0]) >> 3 >= 16); + std::array expected{}; + const int expectedBytes = opus_encode(reference, samples.data(), frameSize, expected.data(), sizeof(expected)); + Require(expectedBytes == bytes && std::equal(packet, packet + bytes, expected.begin())); + } + int decodeError = OPUS_OK; + auto* decoder = opus_decoder_create(rate, channels, &decodeError); + Require(decoder && decodeError == OPUS_OK); + std::array pcm{}; + for (int frame = 0; frame < 4; ++frame) { + FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); + const int bytes = sceOpusCeltEncEncode(ToState(matrixState), samples.data(), frameSize, packet, sizeof(packet)); + pcm.fill(12345); + Require(opus_decode(decoder, packet, bytes, pcm.data() + 1, frameSize, 0) == frameSize); + Require(pcm.front() == 12345 && pcm.back() == 12345); + RequireTone(pcm.data() + 1, frameSize, channels, rate); + } + opus_decoder_destroy(decoder); + opus_encoder_destroy(reference); + Require(sceOpusCeltEncDestroy(ToState(matrixState)) == 0); + } + } + Require(sceOpusCeltEncDestroy(ToState(invalidState)) == static_cast(errorBase + 0x16)); + + std::array retireState{}; + retireState.fill(0x5A); + Require(sceOpusCeltEncCreateEx(&context, ToState(retireState), 48000, 1, callerApplication) == 0); + ++instanceId; + Require(sceOpusCeltEncCreateEx(&context, ToState(retireState), 48000, 2, callerApplication) == 0); + const auto retireWords = SetupWords(instanceId, 48000, 2); + RequireState(retireState.data(), stateSize, retireWords.data(), retireWords.size(), 0x5A); + ++instanceId; + Require(sceOpusCeltEncEncode(ToState(retireState), samples.data(), 960, packet, sizeof(packet)) > 2); + Require(sceOpusCeltEncDestroy(ToState(retireState)) == 0); + + std::array floatState{}; + floatState.fill(0x5A); + Require(sceOpusCeltEncCreateEx(&context, ToState(floatState), 48000, 1, callerApplication) == 0); + const auto floatWords = SetupWords(instanceId, 48000, 1); + RequireState(floatState.data(), stateSize, floatWords.data(), floatWords.size(), 0x5A); + ++instanceId; + for (int index = 0; index < 960; ++index) + floatSamples[static_cast(index)] = static_cast(0.2 * std::sin(2 * 3.14159265358979323846 * 2000 * index / 48000.0)); + const int floatResult = sceOpusCeltEncEncodeFloat(ToState(floatState), floatSamples.data(), 960, packet, static_cast(sizeof(packet))); + Require(floatResult > 2 && static_cast(packet[0]) >> 3 >= 16); + int floatOpusError = OPUS_OK; + auto* floatReference = opus_encoder_create(48000, 1, codecApplication, &floatOpusError); + Require(floatReference && floatOpusError == OPUS_OK); + std::array floatExpected{}; + const int floatExpectedBytes = opus_encode_float(floatReference, floatSamples.data(), 960, floatExpected.data(), sizeof(floatExpected)); + Require(floatExpectedBytes == floatResult && std::equal(packet, packet + floatResult, floatExpected.begin())); + opus_encoder_destroy(floatReference); + Require(sceOpusCeltEncDestroy(ToState(floatState)) == 0); + + std::array liveState{}; + Require(sceOpusCeltEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication) == 0); + ++instanceId; + Require(sceOpusCeltEncDestroy(ToState(liveState)) == 0); + Require(sceOpusCeltEncTerminate(&context) == 0); + RequireThrows([&] { sceOpusCeltEncTerminate(&context); }); + RequireThrows([&] { sceOpusCeltEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication); }); + Require(sceOpusCeltEncCreate(ToState(liveState), 48000, 1, callerApplication) == 0); + ++instanceId; + Require(sceOpusCeltEncCreate(ToState(liveState), 48000, 2, callerApplication) == 0); + ++instanceId; + Require(sceOpusCeltEncDestroy(ToState(liveState)) == 0); +} diff --git a/core/libs/tests/GuestOpusSilkEnc.cpp b/core/libs/tests/GuestOpusSilkEnc.cpp new file mode 100644 index 0000000000..b1cd42762d --- /dev/null +++ b/core/libs/tests/GuestOpusSilkEnc.cpp @@ -0,0 +1,268 @@ +#include "prx/libSceOpusSilkEnc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value, const std::source_location& location = std::source_location::current()) { + if (!value) { + std::fprintf(stderr, "Require failed at %s:%d\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +template +void RequireThrows(TFunction function) { + try { function(); } catch (const std::runtime_error&) { return; } + std::abort(); +} + +template +OpusSilkEncoder* ToState(std::array& block) { + return reinterpret_cast(block.data()); +} + +std::uint32_t Word(const std::uint8_t* data, std::size_t offset) { + std::uint32_t word = 0; + std::memcpy(&word, data + offset, sizeof word); + return word; +} + +struct SetupWord { + std::size_t offset; + std::uint32_t value; +}; + +void RequireState(const std::uint8_t* state, std::size_t stateSize, const SetupWord* words, std::size_t wordCount, std::uint8_t canary) { + std::array covered{}; + for (std::size_t index = 0; index < wordCount; ++index) { + Require(words[index].offset + 4 <= stateSize); + Require(Word(state, words[index].offset) == words[index].value); + for (std::size_t byte = 0; byte < 4; ++byte) covered[words[index].offset + byte] = true; + } + for (std::size_t index = 0; index < stateSize; ++index) + if (!covered[index]) Require(state[index] == 0); + Require(state[stateSize] == canary); +} + +std::uint32_t Bitrate(int rate, int channels) { + if (rate == 8000) return channels == 1 ? 12000u : 19000u; + if (rate == 12000) return channels == 1 ? 15000u : 27000u; + if (rate == 16000) return channels == 1 ? 19000u : 35000u; + if (rate == 24000) return channels == 1 ? 27000u : 51000u; + return channels == 1 ? 51000u : 99000u; +} + +std::array SetupWords(std::uint32_t instanceId, int rate, int channels) { + const auto bandwidth = Bitrate(rate, channels); + const auto rateWord = static_cast(rate); + const auto frameWord = static_cast(rate / 50); + const auto channelWord = static_cast(channels); + return {{ + {0x00, instanceId}, + {0x08, 8u * channelWord}, + {0x10, 0x800u}, + {0x14, rateWord}, + {0x18, channelWord}, + {0x1C, bandwidth}, + {0x28, 0x44Fu}, + {0x2C, 0x44Fu}, + {0x30, 0x5E0u}, + {0x34, 10u}, + {0x48, 0x3E8u}, + {0x58, frameWord}, + {0xE4, frameWord}, + {0xD8, 0xFFFFFC18u}, + {0xDC, bandwidth}, + {0xE0, 0x10u}, + {0xE8, 0x17B2u}, + }}; +} + +void FillSine(std::int16_t* samples, int frameSize, int channels, int rate, int position) { + for (int index = 0; index < frameSize; ++index) { + const int value = static_cast(3000 * std::sin(2 * 3.14159265358979323846 * 2000 * (position + index) / rate)); + for (int channel = 0; channel < channels; ++channel) samples[index * channels + channel] = static_cast(value); + } +} + +void RequireTone(const std::int16_t* pcm, int samples, int channels, int rate) { + for (int channel = 0; channel < channels; ++channel) { + int peak = 0, crossings = 0; + for (int sample = 0; sample < samples; ++sample) { + const int value = pcm[sample * channels + channel]; + peak = std::max(peak, std::abs(value)); + if (sample > 0) { + const int previous = pcm[(sample - 1) * channels + channel]; + if ((previous <= 0 && value > 0) || (previous >= 0 && value < 0)) ++crossings; + } + } + Require(peak > 30 && peak < 32000); + const int expected = 2 * samples * 2000 / rate; + Require(crossings >= expected - expected / 3 && crossings <= expected + expected / 3); + } +} + +} // namespace + +int main() { + constexpr std::uint32_t errorBase = 0x81770000u; + constexpr std::size_t stateSize = 752; + constexpr int callerApplication = OPUS_APPLICATION_VOIP; + constexpr int codecApplication = OPUS_APPLICATION_RESTRICTED_SILK; + std::uint32_t instanceId = 1; + Require(sceOpusSilkEncInitialize(nullptr) == static_cast(errorBase + 0x18)); + Require(sceOpusSilkEncDestroy(nullptr) == static_cast(errorBase + 0x01)); + std::uint32_t context = 0; + Require(sceOpusSilkEncInitialize(&context) == 0); + RequireThrows([&] { sceOpusSilkEncInitialize(&context); }); + RequireThrows([&] { sceOpusSilkEncTerminate(nullptr); }); + + std::array invalidState{}; + std::array samples{}; + samples.fill(12345); + std::array floatSamples{}; + std::uint8_t packet[1024]; + for (auto& byte : packet) byte = 0x5A; + Require(sceOpusSilkEncCreate(nullptr, 48000, 2, callerApplication) == static_cast(errorBase + 0x01)); + Require(sceOpusSilkEncCreate(ToState(invalidState), 48000, 0, callerApplication) == static_cast(errorBase + 0x0C)); + Require(sceOpusSilkEncCreate(ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x0F)); + Require(sceOpusSilkEncCreate(ToState(invalidState), 22050, 2, callerApplication) == static_cast(errorBase + 0x0F)); + Require(sceOpusSilkEncCreate(ToState(invalidState), 48000, 2, OPUS_APPLICATION_RESTRICTED_LOWDELAY) == static_cast(errorBase + 0x10)); + Require(sceOpusSilkEncCreateEx(nullptr, ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x01)); + Require(sceOpusSilkEncCreateEx(&context, ToState(invalidState), 0, 0, callerApplication) == static_cast(errorBase + 0x0F)); + Require(sceOpusSilkEncCreateEx(&context, ToState(invalidState), 48000, 0, callerApplication) == static_cast(errorBase + 0x0C)); + Require(sceOpusSilkEncCreateEx(&context, ToState(invalidState), 48000, 2, OPUS_APPLICATION_RESTRICTED_LOWDELAY) == static_cast(errorBase + 0x10)); + Require(sceOpusSilkEncGetSize(1) == 752); + Require(sceOpusSilkEncGetSize(2) == 752); + Require(sceOpusSilkEncGetSize(3) == static_cast(errorBase + 0x0C)); + + std::array state{}; + state.fill(0x5A); + Require(sceOpusSilkEncCreateEx(&context, ToState(state), 48000, 2, callerApplication) == 0); + const auto words = SetupWords(instanceId, 48000, 2); + RequireState(state.data(), stateSize, words.data(), words.size(), 0x5A); + ++instanceId; + Require(sceOpusSilkEncEncode(nullptr, samples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x01)); + Require(sceOpusSilkEncEncode(ToState(state), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusSilkEncEncode(ToState(state), nullptr, 0, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 0, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), -1, nullptr, sizeof(packet)) == static_cast(errorBase + 0x04)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), -1, packet, 0) == static_cast(errorBase + 0x05)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), -1, nullptr, 0) == static_cast(errorBase + 0x04)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 1, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 119, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 959, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 5760, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 120, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 240, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(packet[0] == 0x5A && packet[1] == 0x5A); + RequireThrows([&] { sceOpusSilkEncEncode(ToState(state), samples.data(), 960, packet, -1); }); + Require(sceOpusSilkEncEncodeFloat(nullptr, floatSamples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x01)); + Require(sceOpusSilkEncEncodeFloat(ToState(state), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + Require(sceOpusSilkEncEncodeFloat(ToState(state), floatSamples.data(), 0, packet, sizeof(packet)) == static_cast(errorBase + 0x03)); + Require(sceOpusSilkEncEncodeFloat(ToState(invalidState), floatSamples.data(), -1, packet, sizeof(packet)) == static_cast(errorBase + 0x15)); + Require(sceOpusSilkEncEncodeFloat(ToState(invalidState), nullptr, 960, packet, sizeof(packet)) == static_cast(errorBase + 0x02)); + RequireThrows([&] { sceOpusSilkEncEncodeFloat(ToState(state), floatSamples.data(), -1, packet, sizeof(packet)); }); + RequireThrows([&] { sceOpusSilkEncEncodeFloat(ToState(state), floatSamples.data(), 1100000, packet, sizeof(packet)); }); + + auto snapshot = state; + Require(sceOpusSilkEncDestroy(ToState(state)) == 0); + for (std::size_t index = 0; index < stateSize; ++index) + if (index < 0xE8 || index >= 0xEC) Require(state[index] == snapshot[index]); + Require(Word(state.data(), 0xE8) == 0); + Require(sceOpusSilkEncDestroy(ToState(state)) == static_cast(errorBase + 0x15)); + Require(sceOpusSilkEncEncode(ToState(state), samples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x15)); + Require(sceOpusSilkEncEncodeFloat(ToState(state), floatSamples.data(), 960, packet, sizeof(packet)) == static_cast(errorBase + 0x15)); + + for (int rate : {8000, 12000, 16000, 24000, 48000}) { + for (int channels : {1, 2}) { + std::array matrixState{}; + matrixState.fill(0x5A); + Require(sceOpusSilkEncCreateEx(&context, ToState(matrixState), rate, channels, callerApplication) == 0); + const auto matrixWords = SetupWords(instanceId, rate, channels); + RequireState(matrixState.data(), stateSize, matrixWords.data(), matrixWords.size(), 0x5A); + ++instanceId; + int opusError = OPUS_OK; + auto* reference = opus_encoder_create(rate, channels, codecApplication, &opusError); + Require(reference && opusError == OPUS_OK); + const int frameSize = rate / 50; + for (int frame = 0; frame < 4; ++frame) { + FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); + const int bytes = sceOpusSilkEncEncode(ToState(matrixState), samples.data(), frameSize, packet, sizeof(packet)); + Require(bytes > 2 && static_cast(packet[0]) >> 3 < 12); + std::array expected{}; + const int expectedBytes = opus_encode(reference, samples.data(), frameSize, expected.data(), sizeof(expected)); + Require(expectedBytes == bytes && std::equal(packet, packet + bytes, expected.begin())); + } + int decodeError = OPUS_OK; + auto* decoder = opus_decoder_create(rate, channels, &decodeError); + Require(decoder && decodeError == OPUS_OK); + std::array pcm{}; + for (int frame = 0; frame < 4; ++frame) { + FillSine(samples.data(), frameSize, channels, rate, frame * frameSize); + const int bytes = sceOpusSilkEncEncode(ToState(matrixState), samples.data(), frameSize, packet, sizeof(packet)); + pcm.fill(12345); + Require(opus_decode(decoder, packet, bytes, pcm.data() + 1, frameSize, 0) == frameSize); + Require(pcm.front() == 12345 && pcm.back() == 12345); + RequireTone(pcm.data() + 1, frameSize, channels, rate); + } + opus_decoder_destroy(decoder); + opus_encoder_destroy(reference); + Require(sceOpusSilkEncDestroy(ToState(matrixState)) == 0); + } + } + Require(sceOpusSilkEncDestroy(ToState(invalidState)) == static_cast(errorBase + 0x15)); + + std::array retireState{}; + retireState.fill(0x5A); + Require(sceOpusSilkEncCreateEx(&context, ToState(retireState), 48000, 1, callerApplication) == 0); + ++instanceId; + Require(sceOpusSilkEncCreateEx(&context, ToState(retireState), 48000, 2, callerApplication) == 0); + const auto retireWords = SetupWords(instanceId, 48000, 2); + RequireState(retireState.data(), stateSize, retireWords.data(), retireWords.size(), 0x5A); + ++instanceId; + Require(sceOpusSilkEncEncode(ToState(retireState), samples.data(), 960, packet, sizeof(packet)) > 2); + Require(sceOpusSilkEncDestroy(ToState(retireState)) == 0); + + std::array floatState{}; + floatState.fill(0x5A); + Require(sceOpusSilkEncCreateEx(&context, ToState(floatState), 48000, 1, callerApplication) == 0); + const auto floatWords = SetupWords(instanceId, 48000, 1); + RequireState(floatState.data(), stateSize, floatWords.data(), floatWords.size(), 0x5A); + ++instanceId; + for (int index = 0; index < 960; ++index) + floatSamples[static_cast(index)] = static_cast(0.2 * std::sin(2 * 3.14159265358979323846 * 2000 * index / 48000.0)); + const int floatResult = sceOpusSilkEncEncodeFloat(ToState(floatState), floatSamples.data(), 960, packet, static_cast(sizeof(packet))); + Require(floatResult > 2 && static_cast(packet[0]) >> 3 < 12); + int floatOpusError = OPUS_OK; + auto* floatReference = opus_encoder_create(48000, 1, codecApplication, &floatOpusError); + Require(floatReference && floatOpusError == OPUS_OK); + std::array floatExpected{}; + const int floatExpectedBytes = opus_encode_float(floatReference, floatSamples.data(), 960, floatExpected.data(), sizeof(floatExpected)); + Require(floatExpectedBytes == floatResult && std::equal(packet, packet + floatResult, floatExpected.begin())); + opus_encoder_destroy(floatReference); + Require(sceOpusSilkEncDestroy(ToState(floatState)) == 0); + + std::array liveState{}; + Require(sceOpusSilkEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication) == 0); + ++instanceId; + Require(sceOpusSilkEncDestroy(ToState(liveState)) == 0); + Require(sceOpusSilkEncTerminate(&context) == 0); + RequireThrows([&] { sceOpusSilkEncTerminate(&context); }); + RequireThrows([&] { sceOpusSilkEncCreateEx(&context, ToState(liveState), 48000, 2, callerApplication); }); + Require(sceOpusSilkEncCreate(ToState(liveState), 48000, 1, callerApplication) == 0); + ++instanceId; + Require(sceOpusSilkEncCreate(ToState(liveState), 48000, 2, callerApplication) == 0); + ++instanceId; + Require(sceOpusSilkEncDestroy(ToState(liveState)) == 0); +} diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 7fbee7be4d..ab20077bcd 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -130,11 +130,6 @@ static void CheckStickDeadZones(int handle) { PadControllerInformation info{}; Require(scePadGetControllerInformation(handle, &info) == PAD_OK); Require(info.stickInfo.deadZoneLeft == 13 && info.stickInfo.deadZoneRight == 13); - std::array extended{}; - Require(scePadGetExtControllerInformation(handle, extended.data()) == PAD_OK); - PadControllerInformation base{}; - std::memcpy(&base, extended.data(), sizeof(base)); - Require(base.stickInfo.deadZoneLeft == 13 && base.stickInfo.deadZoneRight == 13); } static void CheckTouchContact() { @@ -159,15 +154,13 @@ static void CheckTouchPadInformation(int handle) { Require(info.touchPadInfo.resolution.y == kTouchPadHeight); Require(info.touchPadInfo.pixelDensity == 44.86f); Require(scePadGetControllerInformation(handle + 1, &info) == PAD_ERROR_INVALID_HANDLE); +} - std::array extended{}; - extended.fill(0x5a); - Require(scePadGetExtControllerInformation(handle, extended.data()) == PAD_OK); - PadControllerInformation base{}; - std::memcpy(&base, extended.data(), sizeof(base)); - Require(base.touchPadInfo.resolution.x == kTouchPadWidth); - Require(base.touchPadInfo.resolution.y == kTouchPadHeight); - Require(base.touchPadInfo.pixelDensity == 44.86f); +static void CheckExtControllerInformation(int handle) { + constexpr int notPermitted = static_cast(0x80920101); + std::array extended{}; + Require(scePadGetExtControllerInformation(handle, extended.data()) == notPermitted); + Require(scePadGetExtControllerInformation(handle + 1, extended.data()) == PAD_ERROR_INVALID_HANDLE); } static void CheckVirtualTouchContact() { @@ -464,6 +457,80 @@ static void CheckSystemRemote(int handle) { Require(scePadReadState(handle, &data) == PAD_OK); } +static void CheckIsValidHandle(int user, int handle) { + Require(scePadIsValidHandle(handle, PAD_PORT_TYPE_STANDARD)); + for (const int type : {PAD_PORT_TYPE_SPECIAL, PAD_PORT_TYPE_REMOTE, 1, 3, 4, 5, -1}) Require(!scePadIsValidHandle(handle, type)); + for (const int other : {handle + 1, PAD_HANDLE_BASE + PAD_MAX_CONTROLLERS, 0, -1, PAD_SPECIAL_HANDLE}) Require(!scePadIsValidHandle(other, PAD_PORT_TYPE_STANDARD)); + const int special = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(special >= PAD_SPECIAL_HANDLE); + Require(scePadIsValidHandle(special, PAD_PORT_TYPE_SPECIAL) && !scePadIsValidHandle(special, PAD_PORT_TYPE_STANDARD)); + Require(!scePadIsValidHandle(special + 1, PAD_PORT_TYPE_SPECIAL)); + Require(scePadClose_nid_postfix(special) == PAD_OK); + Require(!scePadIsValidHandle(special, PAD_PORT_TYPE_SPECIAL)); + Require(!scePadIsValidHandle(PAD_REMOTE_HANDLE, PAD_PORT_TYPE_REMOTE)); + const int remote = scePadOpen_nid_postfix(PAD_USER_ID_SYSTEM, PAD_PORT_TYPE_REMOTE, 0, nullptr); + Require(remote == PAD_REMOTE_HANDLE); + Require(scePadIsValidHandle(remote, PAD_PORT_TYPE_REMOTE) && !scePadIsValidHandle(remote, PAD_PORT_TYPE_STANDARD)); + Require(scePadClose_nid_postfix(remote) == PAD_OK); + Require(!scePadIsValidHandle(remote, PAD_PORT_TYPE_REMOTE)); +} + +static PadOutputState OutputOf(int port) { + std::uint32_t seen = 0xffffffffu; + PadOutputState output; + Require(PadFetchOutputForPort_nid_postfix(port, &seen, &output)); + return output; +} + +static bool LightBarIs(int port, bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b) { + const PadOutputState output = OutputOf(port); + return output.lightBarValid == valid && output.lightBar[0] == r && output.lightBar[1] == g && output.lightBar[2] == b; +} + +static void CheckLightBarResets(int user, int handle) { + const PadLightBarParam color{10, 20, 30}; + const auto setBoth = [&] { + Require(scePadSetLightBar(handle, &color) == PAD_OK); + Pad::SetLightBar(2, true, 40, 50, 60); + Require(LightBarIs(0, true, 10, 20, 30) && LightBarIs(2, true, 40, 50, 60)); + }; + const auto openPortReset = [] { return LightBarIs(0, false, 0, 0, 0) && LightBarIs(2, true, 40, 50, 60); }; + setBoth(); + Require(scePadResetLightBarAll() == PAD_OK && openPortReset()); + Require(scePadResetLightBarAll() == PAD_OK && openPortReset()); + setBoth(); + const int special = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(special >= PAD_SPECIAL_HANDLE); + for (const std::uint32_t type : {1u, 2u}) Require(scePadResetLightBarAllByPortType(type) == PAD_OK); + Require(scePadClose_nid_postfix(special) == PAD_OK); + for (const std::uint32_t type : {3u, 4u, 5u, 6u, 16u, 0xffu, 0xffffffffu}) Require(scePadResetLightBarAllByPortType(type) == PAD_ERROR_INVALID_ARG); + Require(LightBarIs(0, true, 10, 20, 30) && LightBarIs(2, true, 40, 50, 60)); + Require(scePadResetLightBarAllByPortType(PAD_PORT_TYPE_STANDARD) == PAD_OK && openPortReset()); + Pad::SetLightBar(2, false, 0, 0, 0); +} + +static bool VibrationIs(int port, std::uint8_t large, std::uint8_t small) { + const PadOutputState output = OutputOf(port); + return output.vibrationLarge == large && output.vibrationSmall == small; +} + +static void CheckVibrationForce(int user, int handle) { + const PadVibrationParam strong{70, 80}; + const PadVibrationParam weak{5, 6}; + Require(scePadSetVibrationForce(handle, &strong) == PAD_OK && VibrationIs(0, 70, 80)); + Require(scePadSetVibration(handle, &weak) == PAD_OK && VibrationIs(0, 5, 6)); + Require(scePadSetVibrationForce(handle, &weak) == scePadSetVibration(handle, &weak) && VibrationIs(0, 5, 6)); + Require(scePadSetVibrationForce(handle, nullptr) == PAD_ERROR_INVALID_ARG && VibrationIs(0, 5, 6)); + for (const int other : {handle + 1, 0x999, -1}) Require(scePadSetVibrationForce(other, nullptr) == PAD_ERROR_INVALID_ARG); + const int special = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); + Require(special >= PAD_SPECIAL_HANDLE); + Require(scePadSetVibrationForce(special, &strong) == PAD_OK && VibrationIs(0, 5, 6)); + Require(scePadSetVibrationForce(special, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadClose_nid_postfix(special) == PAD_OK); + const PadVibrationParam off{0, 0}; + Require(scePadSetVibrationForce(handle, &off) == PAD_OK && VibrationIs(0, 0, 0)); +} + static void CheckAccessibilityControllerState(int handle) { std::uint32_t state = 7; Require(scePadGetAccessibilityControllerState(handle + 1, nullptr) == PAD_ERROR_INVALID_ARG); @@ -588,6 +655,13 @@ int main() { Require(scePadGetControllerType(PAD_HANDLE, &controllerType) == PAD_ERROR_NOT_INITIALIZED); Require(controllerType == -1); Require(scePadUnknown_n3kSX62fgNo() == static_cast(0x80920005)); + Require(!scePadIsValidHandle(PAD_HANDLE, PAD_PORT_TYPE_STANDARD)); + const PadVibrationParam earlyVibration{1, 2}; + for (const PadVibrationParam* param : {static_cast(nullptr), &earlyVibration}) Require(scePadSetVibrationForce(PAD_HANDLE, param) == PAD_ERROR_NOT_INITIALIZED); + Pad::SetLightBar(0, true, 1, 2, 3); + Require(scePadResetLightBarAll() == PAD_OK && LightBarIs(0, true, 1, 2, 3)); + Require(scePadResetLightBarAllByPortType(3) == PAD_ERROR_INVALID_ARG && scePadResetLightBarAllByPortType(PAD_PORT_TYPE_STANDARD) == PAD_OK && LightBarIs(0, true, 1, 2, 3)); + Pad::SetLightBar(0, false, 0, 0, 0); const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-state-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { std::filesystem::path path; @@ -641,6 +715,7 @@ int main() { Require(scePadGetHandle(user, 0, 0) == noHandle); const int earlySpecial = scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr); Require(earlySpecial > 0); + Require(!scePadIsValidHandle(earlySpecial, PAD_PORT_TYPE_SPECIAL)); Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == earlySpecial); Require(scePadClose_nid_postfix(earlySpecial) == PAD_OK); @@ -665,6 +740,7 @@ int main() { CheckVirtualTouchContact(); CheckTouchPadInformation(handle); CheckStickDeadZones(handle); + CheckExtControllerInformation(handle); CheckPartialTrigger(); CheckReadQueue(handle); CheckTapBetweenReads(); @@ -675,6 +751,9 @@ int main() { CheckRemoteController(handle); CheckTrackerDevices(handle); CheckAccessibilityControllerState(handle); + CheckIsValidHandle(user, handle); + CheckLightBarResets(user, handle); + CheckVibrationForce(user, handle); CheckControllerType(handle); CheckInjectedState(commandPath); CheckInjectedQueueTimes(commandPath); @@ -699,6 +778,10 @@ int main() { Require(scePadGetHandle(user, 0, 1) == noHandle); Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); + Require(!scePadIsValidHandle(handle, PAD_PORT_TYPE_STANDARD)); + Require(scePadSetLightBar(handle, &lightBar) == PAD_OK && LightBarIs(0, true, 255, 128, 64)); + Require(scePadResetLightBarAll() == PAD_OK && LightBarIs(0, true, 255, 128, 64)); + Require(scePadResetLightBar(handle) == PAD_OK && LightBarIs(0, false, 0, 0, 0)); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0); Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0); @@ -717,6 +800,7 @@ int main() { const int handle2 = scePadOpen_nid_postfix(user2, 0, 0, nullptr); Require(handle2 == PAD_HANDLE_BASE + 1); Require(scePadGetHandle(user2, 0, 0) == handle2); + Require(scePadIsValidHandle(handle2, PAD_PORT_TYPE_STANDARD) && scePadIsValidHandle(handle, PAD_PORT_TYPE_STANDARD) == false); PadInputState p2Input{}; p2Input.buttons = static_cast(Pad::PadButton::Triangle); @@ -745,6 +829,7 @@ int main() { Require(scePadClose_nid_postfix(handle2) == 0); Require(scePadGetHandle(user2, 0, 0) == noHandle); + Require(!scePadIsValidHandle(handle2, PAD_PORT_TYPE_STANDARD)); Require(scePadSetAngularVelocityDeadbandState(handle2, false) == PAD_OK); Require(scePadSetAngularVelocityDeadbandState(handle2 + 1, false) == PAD_ERROR_INVALID_HANDLE); } diff --git a/core/libs/tests/GuestPlayGoDialog.cpp b/core/libs/tests/GuestPlayGoDialog.cpp index e8a89d8e49..82e8c63de2 100644 --- a/core/libs/tests/GuestPlayGoDialog.cpp +++ b/core/libs/tests/GuestPlayGoDialog.cpp @@ -38,6 +38,8 @@ int main() { Require(scePlayGoDialogOpen(&openParam) == 0); Require(scePlayGoDialogGetStatus() == 3); Require(scePlayGoDialogUpdateStatus() == 3); + Require(scePlayGoDialogOpen(nullptr) == paramInvalid); + Require(scePlayGoDialogOpen(&openParam) == 0); PlayGoDialogResult result{}; Require(scePlayGoDialogGetResult(&result) == 0); @@ -51,5 +53,13 @@ int main() { Require(scePlayGoDialogTerminate() == 0); Require(scePlayGoDialogGetStatus() == 0); Require(scePlayGoDialogTerminate() == notInitialized); + Require(scePlayGoDialogClose() == notInitialized); + Require(scePlayGoDialogOpen(nullptr) == paramInvalid); + Require(scePlayGoDialogOpen(&openParam) == notInitialized); + Require(scePlayGoDialogGetResult(nullptr) == paramInvalid); + Require(scePlayGoDialogGetResult(&result) == static_cast(0x80B8000Au)); + result.reserved[0] = 0; + Require(scePlayGoDialogGetResult(&result) == notInitialized); + Require(scePlayGoDialogUpdateStatus() == 0); return 0; } diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index ccf187396e..1f608a2e58 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -83,7 +83,14 @@ int main() { Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(scePlayerReviewDialogTerminate() == 0); + Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogOpen(nullptr) == static_cast(0x80B8000Du)); + Require(scePlayerReviewDialogOpen(¶m) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogGetResult(nullptr) == static_cast(0x80B8000Du)); + Require(scePlayerReviewDialogGetResult(reinterpret_cast(result)) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogInitialize() == 0); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogClose() == static_cast(0x80B8000Bu)); @@ -102,6 +109,13 @@ int main() { Require(scePlayerReviewDialogGetResult(reinterpret_cast(result)) == COMMON_DIALOG_RESULT_USER_CANCELED); Require(result[2] == COMMON_DIALOG_RESULT_USER_CANCELED); Require(scePlayerReviewDialogTerminate() == 0); + Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogOpen(nullptr) == static_cast(0x80B8000Du)); + Require(scePlayerReviewDialogOpen(¶m) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogGetResult(nullptr) == static_cast(0x80B8000Du)); + Require(scePlayerReviewDialogGetResult(reinterpret_cast(result)) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceUserServiceTerminate() == 0); } diff --git a/core/libs/tests/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp index 1930f44533..bbdf154c9d 100644 --- a/core/libs/tests/GuestPlayerSelectionDialog.cpp +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -97,7 +97,12 @@ int main() { Require(scePlayerSelectionDialogClose() == static_cast(0x80B8000Bu)); Require(scePlayerSelectionDialogTerminate() == 0); Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); - Require(scePlayerSelectionDialogGetStatus() == 0); + Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogOpen(¶m) == kErrNotInitialized); Require(scePlayerSelectionDialogClose() == kErrNotInitialized); + Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogGetResult(&result) == kErrNotInitialized); + Require(scePlayerSelectionDialogGetStatus() == 0); + Require(scePlayerSelectionDialogUpdateStatus() == 0); Require(sceUserServiceTerminate() == 0); } diff --git a/core/libs/tests/GuestQsort.cpp b/core/libs/tests/GuestQsort.cpp index 00e7f72ef2..17a380e5c7 100644 --- a/core/libs/tests/GuestQsort.cpp +++ b/core/libs/tests/GuestQsort.cpp @@ -210,6 +210,29 @@ bool CheckMergesort() { if (!correct) std::fprintf(stderr, "mergesort failed\n"); return correct; } + +bool CheckHeapsort() { + bool correct = true; + for (const std::size_t count : {0, 1, 2, 3, 7, 16, 33, 1000}) { + std::vector elements; + for (std::size_t index = 0; index < count; ++index) elements.push_back({static_cast((index * 7919) % 5), static_cast(index)}); + correct &= heapsort_nid_postfix(elements.data(), count, sizeof(Element), CompareKeys) == 0; + for (std::size_t index = 1; index < count; ++index) { + const auto& before = elements[index - 1]; + const auto& after = elements[index]; + correct &= before.key <= after.key; + } + } + unsigned char bytes[3] = {3, 1, 2}; + *__error_nid_postfix() = 0; + correct &= heapsort_nid_postfix(nullptr, 0, 0, nullptr) == 0; + unsigned char single = 42; + correct &= heapsort_nid_postfix(&single, 1, 0, nullptr) == 0; + correct &= heapsort_nid_postfix(bytes, 3, 0, CompareFirstByte) == -1 && *__error_nid_postfix() == 22 && bytes[0] == 3; + correct &= heapsort_nid_postfix(nullptr, 0, 4, CompareKeys) == 0; + if (!correct) std::fprintf(stderr, "heapsort failed\n"); + return correct; +} } int main() { @@ -229,5 +252,6 @@ int main() { for (const std::size_t count : {0, 1, 2, 6, 7, 8, 40, 41, 1000}) correct &= ExpectSorted(size, count); correct &= CheckMergesort(); + correct &= CheckHeapsort(); return correct ? 0 : 1; } diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index 205e6e4c81..439e808613 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -402,7 +402,7 @@ static void* APS5_VABI AddressWaking(void* arg) { auto& worker = *static_cast(arg); worker.id = std::this_thread::get_id(); worker.started.store(true); - while (!worker.stop.load()) Require(sceKernelSyncOnAddressWake(&addressWord, 1) == 0); + while (!worker.stop.load()) Require(sceKernelSyncOnAddressWake(&addressWord, 1) >= 0); return nullptr; } diff --git a/core/libs/tests/GuestRealpath.cpp b/core/libs/tests/GuestRealpath.cpp index 52240b0a7f..ec6494ff2b 100644 --- a/core/libs/tests/GuestRealpath.cpp +++ b/core/libs/tests/GuestRealpath.cpp @@ -11,6 +11,10 @@ #include #include #include +#include +#ifdef _WIN32 +#include +#endif static void Require(bool value) { if (!value) std::abort(); } @@ -95,6 +99,57 @@ int main() { *__error_nid_postfix() = 0; Require(realpath_nid_postfix((guest + "/missing").c_str(), nullptr) == nullptr && *__error_nid_postfix() == 2); + const auto createLink = [&](const std::filesystem::path& link, const std::string& target, bool toDirectory) { +#ifdef _WIN32 + return CreateSymbolicLinkW(link.c_str(), std::filesystem::path(target).make_preferred().c_str(), + (toDirectory ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0) | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0; +#else + std::error_code linkError; + if (toDirectory) std::filesystem::create_directory_symlink(target, link, linkError); + else std::filesystem::create_symlink(target, link, linkError); + return !linkError; +#endif + }; + std::vector files, directories; + const auto fileLink = [&](const std::filesystem::path& link, const std::string& target) { + files.push_back(link); + return createLink(link, target, false); + }; + if (fileLink(directory / "file-link", "file.txt")) { + directories.push_back(directory / "directory-link"); + Require(createLink(directory / "directory-link", "sub", true)); + Require(fileLink(directory / "dangling-link", "missing")); + Require(fileLink(directory / "loop-a", "loop-b") && fileLink(directory / "loop-b", "loop-a")); + Require(fileLink(directory / "chain-link", "file-link")); + Require(fileLink(directory / "sub" / "up-link", "../file.txt")); + Require(Resolves(guest + "/file-link", guest + "/file.txt")); + Require(Resolves(guest + "/directory-link", guest + "/sub")); + Require(Resolves(guest + "/directory-link/inner.txt", guest + "/sub/inner.txt")); + Require(Resolves(guest + "/directory-link/../file.txt", guest + "/file.txt")); + Require(Resolves(guest + "/chain-link", guest + "/file.txt")); + Require(Resolves(guest + "/sub/up-link", guest + "/file.txt")); + Require(Resolves(guest + "/directory-link/up-link", guest + "/file.txt")); + Require(Resolves(guest + "/directory-link/", guest + "/sub")); + directories.push_back(directory / "absolute-link"); + Require(createLink(directory / "absolute-link", (directory / "sub").string(), true)); + Require(Resolves(guest + "/absolute-link/inner.txt", guest + "/absolute-link/inner.txt")); + Require(Resolves(guest + "/absolute-link/up-link", guest + "/absolute-link/up-link")); + Require(Fails((guest + "/dangling-link").c_str(), 2, guest + "/missing")); + Require(Fails((guest + "/dangling-link/x").c_str(), 2, guest + "/missing")); + Require(Fails((guest + "/loop-a").c_str(), 62)); + Require(Fails((guest + "/file-link/x").c_str(), 20)); + Require(Fails((guest + "/file-link/").c_str(), 20)); + for (int index = 0; index < 34; ++index) { + Require(fileLink(directory / ("long-" + std::to_string(index)), index == 33 ? "file.txt" : "long-" + std::to_string(index + 1))); + } + Require(Resolves(guest + "/long-1", guest + "/file.txt")); + Require(Fails((guest + "/long-0").c_str(), 62)); + } +#ifdef _WIN32 + for (const auto& link : files) DeleteFileW(link.c_str()); + for (const auto& link : directories) RemoveDirectoryW(link.c_str()); +#endif + std::filesystem::remove_all(directory); return 0; } diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index b728c6dad1..57a213635e 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -139,6 +139,10 @@ int main(int argc, char** argv) { Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, RtcDateTime{9999, 12, 31, 23, 59, 59, 999999})); tick.tick = maxTick + 1; Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, RtcDateTime{10000, 1, 1, 0, 0, 0, 0})); + RtcTick convertedTick{}; + Require(sceRtcConvertDateTimeToTick(&leapDay, &convertedTick) == 0 && convertedTick.tick == leapDayTick); + RtcDateTime fromConvertedTick{}; + Require(sceRtcConvertTickToDateTime(&tick, &fromConvertedTick) == 0 && Equal(fromConvertedTick, RtcDateTime{10000, 1, 1, 0, 0, 0, 0})); Require(sceRtcGetCurrentClock(nullptr, 0) == invalidPointer); Require(sceRtcGetCurrentClock(&converted, 0) == 0 && sceRtcCheckValid(&converted) == 0); @@ -146,6 +150,8 @@ int main(int argc, char** argv) { Require(sceRtcGetCurrentClock(&converted, -300) == 0 && sceRtcCheckValid(&converted) == 0); Require(sceRtcGetCurrentClockLocalTime(nullptr) == invalidPointer); Require(sceRtcGetCurrentClockLocalTime(&converted) == 0 && sceRtcCheckValid(&converted) == 0); + Require(sceRtcGetCurrentClockUtc(nullptr) == invalidPointer); + Require(sceRtcGetCurrentClockUtc(&converted) == 0 && sceRtcCheckValid(&converted) == 0); RtcDateTime epoch{1970, 1, 1, 0, 0, 0, 0}; std::int64_t seconds = -1; @@ -153,6 +159,8 @@ int main(int argc, char** argv) { RtcDateTime millennium{2000, 1, 1, 0, 0, 0, 0}; Require(sceRtcGetTime_t(&millennium, &seconds) == 0 && seconds == 946684800); Require(sceRtcSetTime_t(&converted, 946684800) == 0 && Equal(converted, millennium)); + Require(sceRtcConvertDateTimeToTime_t(&millennium, &seconds) == 0 && seconds == 946684800); + Require(sceRtcConvertTime_tToDateTime(946684800, &converted) == 0 && Equal(converted, millennium)); Require(sceRtcSetTime_t(&converted, -1) == 0 && Equal(converted, RtcDateTime{2106, 2, 7, 6, 28, 15, 0})); Require(sceRtcSetTime_t(&converted, 0x100000001ll) == 0 && Equal(converted, RtcDateTime{1970, 1, 1, 0, 0, 1, 0})); Require(sceRtcSetTime_t(&converted, -0x80000000ll) == 0 && Equal(converted, RtcDateTime{2038, 1, 19, 3, 14, 8, 0})); @@ -944,6 +952,8 @@ int main(int argc, char** argv) { RtcDateTime sentinel{0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaaaaaau}; RtcDateTime ranged = sentinel; Require(sceRtcSetTime_t(&ranged, -1) == invalidValue && Equal(ranged, sentinel)); + Require(sceRtcSetTime_t(nullptr, -1) == invalidValue && sceRtcSetTime_t(nullptr, std::numeric_limits::min()) == invalidValue); + Require(sceRtcSetTime_t(nullptr, 0) == static_cast(0x80B50002) && sceRtcSetTime_t(nullptr, 1) == static_cast(0x80B50002)); Require(sceRtcSetTime_t(&ranged, std::numeric_limits::min()) == invalidValue && Equal(ranged, sentinel)); Require(sceRtcSetTime_t(&ranged, 4294967296ll) == 0 && Equal(ranged, RtcDateTime{2106, 2, 7, 6, 28, 16, 0})); Require(sceRtcSetTime_t(&ranged, 253402300800ll) == 0 && Equal(ranged, RtcDateTime{10000, 1, 1, 0, 0, 0, 0})); diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp index 26482bfeea..7cc96959f0 100644 --- a/core/libs/tests/GuestRudpPump.cpp +++ b/core/libs/tests/GuestRudpPump.cpp @@ -18,6 +18,7 @@ static void Require(bool value, int line) { int main() { constexpr int notInitialized = static_cast(0x80770001); constexpr int invalidArgument = static_cast(0x80770004); + constexpr int notAcceptable = static_cast(0x80770011); std::uint16_t segmentSize = 0x5a5a; CHECK(sceRudpProcessEvents(0) == notInitialized); CHECK(sceRudpNetFlush() == notInitialized); @@ -26,6 +27,7 @@ int main() { CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); CHECK(segmentSize == 0x5a5a); CHECK(sceRudpGetMaxSegmentSize(nullptr) == notInitialized); + CHECK(sceRudpSetMaxSegmentSize(1200) == notInitialized); alignas(8) static unsigned char pool[0xC58]; CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); @@ -39,6 +41,12 @@ int main() { std::uint16_t segment[3] = {0, 0, 0x5a5a}; CHECK(sceRudpGetMaxSegmentSize(&segment[1]) == 0); CHECK(segment[0] == 0 && segment[1] == 1410 && segment[2] == 0x5a5a); + CHECK(sceRudpSetMaxSegmentSize(1200) == 0); + int context = -1; + CHECK(sceRudpCreateContext(nullptr, nullptr, &context) == 0); + CHECK(sceRudpSetMaxSegmentSize(1000) == notAcceptable); + CHECK(sceRudpTerminate(context) == 0); + CHECK(sceRudpSetMaxSegmentSize(1000) == 0); CHECK(sceRudpEnd() == 0); segmentSize = 0x5a5a; diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 0c4a45540f..d35c27146d 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -26,16 +26,6 @@ void Check(bool condition, const std::string& what) { ++failures; } -template -bool ThrowsRuntimeError(TFunction function) { - try { - function(); - } catch (const std::runtime_error&) { - return true; - } - return false; -} - void TestTransferringMountPs4() { SceSaveDataTitleId title{}; std::strcpy(title.data, "CUSA00001"); @@ -77,8 +67,8 @@ void TestDirNameSearchPs4() { Check(result.set_num == 0, "DirNameSearchPs4 writes zero set entries"); Check(static_cast(names[0].data[0]) == 0x5A, "DirNameSearchPs4 leaves the name buffer untouched"); - Check(ThrowsRuntimeError([&] { sceSaveDataDirNameSearchPs4(nullptr, &result); }), "null cond throws"); - Check(ThrowsRuntimeError([&] { sceSaveDataDirNameSearchPs4(&cond, nullptr); }), "null result throws"); + Check(sceSaveDataDirNameSearchPs4(nullptr, &result) == SaveDataErrorParameter, "null cond returns PARAMETER in sceSaveDataDirNameSearchPs4"); + Check(sceSaveDataDirNameSearchPs4(&cond, nullptr) == SaveDataErrorParameter, "null result returns PARAMETER in sceSaveDataDirNameSearchPs4"); } void TestSaveDataMemoryV1() { diff --git a/core/libs/tests/GuestSigninDialog.cpp b/core/libs/tests/GuestSigninDialog.cpp index 7baf28dae6..735cb77a9a 100644 --- a/core/libs/tests/GuestSigninDialog.cpp +++ b/core/libs/tests/GuestSigninDialog.cpp @@ -55,7 +55,13 @@ int main() { Require(sceSigninDialogClose() == 0); Require(sceSigninDialogTerminate() == 0); Require(sceSigninDialogTerminate() == kErrNotInitialized); + Require(sceSigninDialogOpen(nullptr) == kErrNotInitialized); + Require(sceSigninDialogOpen(¶m) == kErrNotInitialized); + Require(sceSigninDialogClose() == kErrNotInitialized); + Require(sceSigninDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceSigninDialogGetResult(result) == kErrNotInitialized); Require(sceSigninDialogGetStatus() == kStatusNone); + Require(sceSigninDialogUpdateStatus() == kStatusNone); Require(sceSigninDialogInitialize() == 0); Require(sceSigninDialogGetStatus() == kStatusInitialized); @@ -68,4 +74,12 @@ int main() { Require(result[2] == 0); Require(result[3] == 0); Require(sceSigninDialogTerminate() == 0); + Require(sceSigninDialogTerminate() == kErrNotInitialized); + Require(sceSigninDialogOpen(nullptr) == kErrNotInitialized); + Require(sceSigninDialogOpen(¶m) == kErrNotInitialized); + Require(sceSigninDialogClose() == kErrNotInitialized); + Require(sceSigninDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceSigninDialogGetResult(result) == kErrNotInitialized); + Require(sceSigninDialogGetStatus() == kStatusNone); + Require(sceSigninDialogUpdateStatus() == kStatusNone); } diff --git a/core/libs/tests/GuestSocialScreenDialog.cpp b/core/libs/tests/GuestSocialScreenDialog.cpp index fc75ebafcf..eb261f831e 100644 --- a/core/libs/tests/GuestSocialScreenDialog.cpp +++ b/core/libs/tests/GuestSocialScreenDialog.cpp @@ -51,6 +51,8 @@ int main() { Require(sceSocialScreenDialogClose() == 0); Require(sceSocialScreenDialogTerminate() == 0); Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); + Require(sceSocialScreenDialogOpen(nullptr) == kErrNotInitialized); + Require(sceSocialScreenDialogGetResult(nullptr) == kErrNotInitialized); Require(sceSocialScreenDialogGetStatus() == kStatusNone); Require(sceSocialScreenDialogUpdateStatus() == kStatusNone); @@ -62,4 +64,9 @@ int main() { Require(sceSocialScreenDialogGetResult(result) == 0); Require(result[0] == kResultUserCanceled); Require(sceSocialScreenDialogTerminate() == 0); + Require(sceSocialScreenDialogTerminate() == kErrNotInitialized); + Require(sceSocialScreenDialogOpen(nullptr) == kErrNotInitialized); + Require(sceSocialScreenDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceSocialScreenDialogGetStatus() == kStatusNone); + Require(sceSocialScreenDialogUpdateStatus() == kStatusNone); } diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 9925382594..ffdf41f6ff 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -604,6 +604,40 @@ static int CheckExclusiveStandardOutput() { return 0; } +static int CheckUpdateStandardOutput() { + const auto filename = "anyps5-update-stdout-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + auto* const stream = __stdoutp_nid_postfix; + Require(freopen_nid_postfix(filename.c_str(), "a+b", stream) == stream); + Require(fputs_nid_postfix("0123456789", stream) >= 0); + const auto readFirst = [stream] { Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0 && fgetc_nid_postfix(stream) == '0'); }; + readFirst(); + Require(puts_nid_postfix("A") != EOF); + readFirst(); + Require(printf_nid_postfix("%s", "B") == 1); + readFirst(); + Require(PrintStandard("%s", "C") == 1); + readFirst(); + Require(printf_s_nid_postfix("%s", "D") == 1); + readFirst(); + Require(wprintf_nid_postfix(u"E") == 1); + Require(ferror_nid_postfix(stream) == 0); + Require(freopen_nid_postfix(filename.c_str(), "rb", stream) == stream && fgetc_nid_postfix(stream) == '0'); + *__error_nid_postfix() = 0; + Require(puts_nid_postfix("A") == EOF && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(printf_nid_postfix("%s", "B") == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(PrintStandard("%s", "C") == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(printf_s_nid_postfix("%s", "D") == -1 && *__error_nid_postfix() == 9); + *__error_nid_postfix() = 0; + Require(wprintf_nid_postfix(u"E") == -1 && *__error_nid_postfix() == 9); + Require(ferror_nid_postfix(stream) != 0 && fclose_nid_postfix(stream) == 0); + Require(CheckFileBytes(filename, "0123456789A\nBCDE", "a+b", true)); + Require(std::filesystem::remove(filename)); + return 0; +} + #ifdef _WIN32 static void CheckStandardStreamDeleteSharing() { int index = 0; @@ -625,6 +659,7 @@ static void CheckStandardStreamDeleteSharing() { #endif int main(int argc, char** argv) { + if (argc > 1 && std::string_view(argv[1]) == "--update-stdout") return CheckUpdateStandardOutput(); if (argc > 1 && std::string_view(argv[1]) == "--closed-stdout") return CheckClosedStandardOutput(); if (argc > 1 && std::string_view(argv[1]) == "--exclusive-stdout") return CheckExclusiveStandardOutput(); CheckClosedStreamWrites(); diff --git a/core/libs/tests/GuestSyncOnAddress.cpp b/core/libs/tests/GuestSyncOnAddress.cpp index e320373917..9977aee09c 100644 --- a/core/libs/tests/GuestSyncOnAddress.cpp +++ b/core/libs/tests/GuestSyncOnAddress.cpp @@ -6,23 +6,17 @@ #include #include #include -#include +#include #include #include static constexpr int SCE_OK = 0; static constexpr KernelUseconds FAILSAFE_TIMEOUT = 10000000; +static constexpr KernelUseconds SHORT_TIMEOUT = 1000; static constexpr auto SETTLE = std::chrono::milliseconds(100); static void Require(bool value) { if (!value) std::abort(); } -template -static bool Rejects(TCall call) { - try { call(); } - catch (const std::invalid_argument&) { return true; } - return false; -} - static void AwaitCount(const std::atomic& counter, int value) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (counter.load() != value) { @@ -31,81 +25,93 @@ static void AwaitCount(const std::atomic& counter, int value) { } } -static void ReturnsWhenTheValueDiffers() { +static void FailsAgainWhenTheValueDiffers() { std::uint32_t word = 2; - Require(sceKernelSyncOnAddressWait(&word, 1, nullptr, "differs") == SCE_OK); - const KernelUseconds timeout = 0; - Require(sceKernelSyncOnAddressWait(&word, 1, &timeout, nullptr) == SCE_OK); + Require(sceKernelSyncOnAddressWait(&word, 1, nullptr, "differs") == SCE_KERNEL_ERROR_EAGAIN); + KernelUseconds timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait(&word, 1, &timeout, nullptr) == SCE_KERNEL_ERROR_EAGAIN && timeout == SHORT_TIMEOUT); + timeout = 0; + Require(sceKernelSyncOnAddressWait(&word, 1, &timeout, nullptr) == SCE_KERNEL_ERROR_ETIMEDOUT); } static void TimesOutWhileTheValueMatches() { std::uint32_t word = 1; - const KernelUseconds none = 0; - Require(sceKernelSyncOnAddressWait(&word, 1, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + KernelUseconds none = 0; + Require(sceKernelSyncOnAddressWait(&word, 1, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT && none == 0); - const KernelUseconds timeout = 20000; + KernelUseconds timeout = 20000; const auto start = std::chrono::steady_clock::now(); Require(sceKernelSyncOnAddressWait(&word, 1, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); - Require(sceKernelSyncOnAddressWake(&word, INT_MAX) == SCE_OK); + Require(timeout == 0); + Require(sceKernelSyncOnAddressWake(&word, INT_MAX) == 0); } -static void WakeReleasesAWaiterWithoutAValueChange() { +static void WakeReleasesAWaiterAndReportsTheTimeLeft() { std::uint32_t word = 1; + std::atomic started{0}; std::atomic finished{0}; int result = -1; + KernelUseconds timeout = FAILSAFE_TIMEOUT; std::thread waiter([&] { - result = sceKernelSyncOnAddressWait(&word, 1, &FAILSAFE_TIMEOUT, "wake"); + ++started; + result = sceKernelSyncOnAddressWait(&word, 1, &timeout, "wake"); ++finished; }); + AwaitCount(started, 1); + std::this_thread::sleep_for(SETTLE); + int woken = 0; while (finished.load() == 0) { - Require(sceKernelSyncOnAddressWake(&word, 1) == SCE_OK); + woken += sceKernelSyncOnAddressWake(&word, 1); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } waiter.join(); - Require(result == SCE_OK); + Require(result == SCE_OK && woken == 1); + Require(timeout > 0 && timeout < FAILSAFE_TIMEOUT); } -static void WakeHonoursTheCountAndTheAddress() { +static void WakeCountsOfOneOrLessWakeOneWaiter() { std::uint32_t word = 1; std::uint32_t other = 1; std::atomic started{0}; std::atomic finished{0}; std::atomic otherFinished{0}; - std::vector results(3, -1); + std::vector results(5, -1); int otherResult = -1; std::vector waiters; - for (int i = 0; i < 3; ++i) { + for (int i = 0; i < 5; ++i) { waiters.emplace_back([&, i] { ++started; - results[i] = sceKernelSyncOnAddressWait(&word, 1, &FAILSAFE_TIMEOUT, "count"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + results[i] = sceKernelSyncOnAddressWait(&word, 1, &timeout, "count"); ++finished; }); } std::thread otherWaiter([&] { ++started; - otherResult = sceKernelSyncOnAddressWait(&other, 1, &FAILSAFE_TIMEOUT, "other"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + otherResult = sceKernelSyncOnAddressWait(&other, 1, &timeout, "other"); ++otherFinished; }); - AwaitCount(started, 4); + AwaitCount(started, 6); std::this_thread::sleep_for(SETTLE); - Require(sceKernelSyncOnAddressWake(&word, 0) == SCE_OK); - std::this_thread::sleep_for(SETTLE); - Require(finished.load() == 0); + int expected = 0; + for (const std::int32_t count : {0, -1, 1}) { + Require(sceKernelSyncOnAddressWake(&word, count) == 1); + AwaitCount(finished, ++expected); + std::this_thread::sleep_for(SETTLE); + Require(finished.load() == expected); + } - Require(sceKernelSyncOnAddressWake(&word, 1) == SCE_OK); - AwaitCount(finished, 1); - std::this_thread::sleep_for(SETTLE); - Require(finished.load() == 1); - - Require(sceKernelSyncOnAddressWake(&word, INT_MAX) == SCE_OK); - AwaitCount(finished, 3); + Require(sceKernelSyncOnAddressWake(&word, INT_MAX) == 2); + AwaitCount(finished, 5); std::this_thread::sleep_for(SETTLE); Require(otherFinished.load() == 0); + Require(sceKernelSyncOnAddressWake(&word, INT_MAX) == 0); - Require(sceKernelSyncOnAddressWake(&other, 1) == SCE_OK); + Require(sceKernelSyncOnAddressWake(&other, 1) == 1); for (auto& waiter : waiters) waiter.join(); otherWaiter.join(); for (const int result : results) Require(result == SCE_OK); @@ -117,38 +123,42 @@ static void WaitsOnTheLowHalfOfAnOnceState() { auto* word = reinterpret_cast(&state); std::thread waiter([&] { while (state.load() == 1) { - Require(sceKernelSyncOnAddressWait(word, 1, nullptr, "once") == SCE_OK); + const int result = sceKernelSyncOnAddressWait(word, 1, nullptr, "once"); + Require(result == SCE_OK || result == SCE_KERNEL_ERROR_EAGAIN); } }); std::this_thread::sleep_for(std::chrono::milliseconds(5)); state.store(2); - Require(sceKernelSyncOnAddressWake(word, INT_MAX) == SCE_OK); + Require(sceKernelSyncOnAddressWake(word, INT_MAX) >= 0); waiter.join(); } static void SizedWaitsTimeOutWhileTheValueMatches() { std::uint32_t word = 5; std::uint64_t value = 0x500000005ULL; - const KernelUseconds none = 0; - Require(sceKernelSyncOnAddressWait32(&word, 4, &none, "differs") == SCE_OK); + KernelUseconds timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait32(&word, 4, &timeout, "differs") == SCE_KERNEL_ERROR_EAGAIN); + KernelUseconds none = 0; + Require(sceKernelSyncOnAddressWait32(&word, 4, &none, "differs") == SCE_KERNEL_ERROR_ETIMEDOUT); Require(sceKernelSyncOnAddressWait32(&word, 5, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); Require(sceKernelSyncOnAddressWait64(&value, 0x500000005ULL, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); - const KernelUseconds timeout = 20000; + timeout = 20000; auto start = std::chrono::steady_clock::now(); - Require(sceKernelSyncOnAddressWait32(&word, 5, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelSyncOnAddressWait32(&word, 5, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT && timeout == 0); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + timeout = 20000; start = std::chrono::steady_clock::now(); - Require(sceKernelSyncOnAddressWait64(&value, 0x500000005ULL, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelSyncOnAddressWait64(&value, 0x500000005ULL, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT && timeout == 0); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); } static void Wait64ComparesTheHighHalf() { std::uint64_t value = 0x100000001ULL; - const KernelUseconds none = 0; - Require(sceKernelSyncOnAddressWait64(&value, 0x1ULL, &none, "high") == SCE_OK); - Require(sceKernelSyncOnAddressWait64(&value, 0x200000001ULL, &none, "high") == SCE_OK); - Require(sceKernelSyncOnAddressWait64(&value, 0x100000001ULL, &none, "high") == SCE_KERNEL_ERROR_ETIMEDOUT); + KernelUseconds timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait64(&value, 0x1ULL, &timeout, "high") == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelSyncOnAddressWait64(&value, 0x200000001ULL, &timeout, "high") == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelSyncOnAddressWait64(&value, 0x100000001ULL, &timeout, "high") == SCE_KERNEL_ERROR_ETIMEDOUT); } static void WakeReleasesWaitersOfEverySize() { @@ -161,23 +171,26 @@ static void WakeReleasesWaitersOfEverySize() { std::vector waiters; waiters.emplace_back([&] { ++started; - results[0] = sceKernelSyncOnAddressWait(word, 1, &FAILSAFE_TIMEOUT, "sized"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + results[0] = sceKernelSyncOnAddressWait(word, 1, &timeout, "sized"); ++finished; }); waiters.emplace_back([&] { ++started; - results[1] = sceKernelSyncOnAddressWait32(word, 1, &FAILSAFE_TIMEOUT, "sized"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + results[1] = sceKernelSyncOnAddressWait32(word, 1, &timeout, "sized"); ++finished; }); waiters.emplace_back([&] { ++started; - results[2] = sceKernelSyncOnAddressWait64(&value, 1, &FAILSAFE_TIMEOUT, "sized"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + results[2] = sceKernelSyncOnAddressWait64(&value, 1, &timeout, "sized"); ++finished; }); AwaitCount(started, 3); std::this_thread::sleep_for(SETTLE); - Require(sceKernelSyncOnAddressWake(&value, 1) == SCE_OK); + Require(sceKernelSyncOnAddressWake(&value, 1) == 1); AwaitCount(finished, 1); std::this_thread::sleep_for(SETTLE); Require(finished.load() == 1); @@ -185,7 +198,7 @@ static void WakeReleasesWaitersOfEverySize() { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); while (finished.load() != 3) { Require(std::chrono::steady_clock::now() < deadline); - Require(sceKernelSyncOnAddressWake(&value, INT_MAX) == SCE_OK); + Require(sceKernelSyncOnAddressWake(&value, INT_MAX) >= 0); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } for (auto& waiter : waiters) waiter.join(); @@ -193,14 +206,15 @@ static void WakeReleasesWaitersOfEverySize() { } static void NarrowWaitsCompareOnlyTheirWidth() { - const KernelUseconds none = 0; + KernelUseconds timeout = SHORT_TIMEOUT; alignas(4) std::uint8_t bytes[4] = {1, 5, 7, 7}; - Require(sceKernelSyncOnAddressWait8(&bytes[1], 4, &none, "differs") == SCE_OK); - Require(sceKernelSyncOnAddressWait8(&bytes[1], 5, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelSyncOnAddressWait8(&bytes[1], 4, &timeout, "differs") == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelSyncOnAddressWait8(&bytes[1], 5, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + timeout = SHORT_TIMEOUT; alignas(4) std::uint16_t halves[2] = {1, 0x0705}; - Require(sceKernelSyncOnAddressWait16(&halves[1], 0x0005, &none, "high") == SCE_OK); - Require(sceKernelSyncOnAddressWait16(&halves[1], 0x0705, &none, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelSyncOnAddressWait16(&halves[1], 0x0005, &timeout, "high") == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelSyncOnAddressWait16(&halves[1], 0x0705, &timeout, "timeout") == SCE_KERNEL_ERROR_ETIMEDOUT); } static void WakeReleasesNarrowWaitersAtTheirOwnAddress() { @@ -216,26 +230,28 @@ static void WakeReleasesNarrowWaitersAtTheirOwnAddress() { std::thread byteWaiter([&] { ++started; - byteResult = sceKernelSyncOnAddressWait8(&bytes[1], 3, &FAILSAFE_TIMEOUT, "byte"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + byteResult = sceKernelSyncOnAddressWait8(&bytes[1], 3, &timeout, "byte"); ++byteFinished; }); std::thread halfWaiter([&] { ++started; - halfResult = sceKernelSyncOnAddressWait16(half, 9, &FAILSAFE_TIMEOUT, "half"); + KernelUseconds timeout = FAILSAFE_TIMEOUT; + halfResult = sceKernelSyncOnAddressWait16(half, 9, &timeout, "half"); ++halfFinished; }); AwaitCount(started, 2); std::this_thread::sleep_for(SETTLE); - Require(sceKernelSyncOnAddressWake(&bytes[0], INT_MAX) == SCE_OK); - Require(sceKernelSyncOnAddressWake(&bytes[3], INT_MAX) == SCE_OK); + Require(sceKernelSyncOnAddressWake(&bytes[0], INT_MAX) == 0); + Require(sceKernelSyncOnAddressWake(&bytes[3], INT_MAX) == 0); std::this_thread::sleep_for(SETTLE); Require(byteFinished.load() == 0 && halfFinished.load() == 0); - Require(sceKernelSyncOnAddressWake(&bytes[1], 1) == SCE_OK); + Require(sceKernelSyncOnAddressWake(&bytes[1], 1) == 1); AwaitCount(byteFinished, 1); Require(halfFinished.load() == 0); - Require(sceKernelSyncOnAddressWake(half, 1) == SCE_OK); + Require(sceKernelSyncOnAddressWake(half, 1) == 1); AwaitCount(halfFinished, 1); byteWaiter.join(); @@ -243,36 +259,87 @@ static void WakeReleasesNarrowWaitersAtTheirOwnAddress() { Require(byteResult == SCE_OK && halfResult == SCE_OK); } -static void RejectsInvalidArguments() { - std::uint32_t words[2] = {1, 1}; - auto* misaligned = reinterpret_cast(reinterpret_cast(words) + 1); - Require(Rejects([&] { sceKernelSyncOnAddressWait(nullptr, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait(misaligned, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait32(nullptr, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait32(misaligned, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWake(nullptr, 1); })); - Require(sceKernelSyncOnAddressWake(misaligned, 1) == SCE_OK); - Require(Rejects([&] { sceKernelSyncOnAddressWait8(nullptr, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait16(nullptr, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait16(reinterpret_cast(reinterpret_cast(words) + 1), 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWake(words, -1); })); +static void MisalignedAddressesWaitAndWake() { + alignas(16) unsigned char storage[16] = {}; + auto* word = reinterpret_cast(storage + 1); + auto* quad = reinterpret_cast(storage + 4); + const std::uint32_t wordValue = 0x11223344; + std::memcpy(word, &wordValue, sizeof(wordValue)); + KernelUseconds timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait32(word, 1, &timeout, "misaligned") == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelSyncOnAddressWait32(word, wordValue, &timeout, "misaligned") == SCE_KERNEL_ERROR_ETIMEDOUT && timeout == 0); + timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait64(quad, 1, &timeout, "misaligned") == SCE_KERNEL_ERROR_EAGAIN); + timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait16(reinterpret_cast(storage + 9), 1, &timeout, "misaligned") == SCE_KERNEL_ERROR_EAGAIN); - std::uint64_t values[2] = {1, 1}; - auto* wordAligned = reinterpret_cast(reinterpret_cast(values) + sizeof(std::uint32_t)); - Require(Rejects([&] { sceKernelSyncOnAddressWait64(nullptr, 1, nullptr, nullptr); })); - Require(Rejects([&] { sceKernelSyncOnAddressWait64(wordAligned, 1, nullptr, nullptr); })); + std::atomic started{0}; + int result = -1; + std::thread waiter([&] { + ++started; + KernelUseconds waitTimeout = FAILSAFE_TIMEOUT; + result = sceKernelSyncOnAddressWait32(word, wordValue, &waitTimeout, "misaligned"); + }); + AwaitCount(started, 1); + std::this_thread::sleep_for(SETTLE); + Require(sceKernelSyncOnAddressWake(storage, INT_MAX) == 0); + Require(sceKernelSyncOnAddressWake(word, 1) == 1); + waiter.join(); + Require(result == SCE_OK); + + auto* shared = reinterpret_cast(storage + 3); + std::atomic stop{false}; + std::atomic rounds{0}; + std::thread changer([&] { + std::uint64_t next = 1; + while (!stop.load()) { + __atomic_store_n(reinterpret_cast(shared), static_cast(next), __ATOMIC_RELAXED); + ++next; + sceKernelSyncOnAddressWake(shared, INT_MAX); + } + }); + for (int round = 0; round < 2000; ++round) { + unsigned char bytes[sizeof(std::uint64_t)]; + bytes[0] = __atomic_load_n(reinterpret_cast(shared), __ATOMIC_RELAXED); + std::memcpy(bytes + 1, reinterpret_cast(shared) + 1, sizeof(bytes) - 1); + std::uint64_t seen = 0; + std::memcpy(&seen, bytes, sizeof(seen)); + KernelUseconds waitTimeout = SHORT_TIMEOUT; + const int waited = sceKernelSyncOnAddressWait64(shared, seen, &waitTimeout, "changing"); + Require(waited == SCE_OK || waited == SCE_KERNEL_ERROR_EAGAIN || waited == SCE_KERNEL_ERROR_ETIMEDOUT); + ++rounds; + } + stop.store(true); + changer.join(); + Require(rounds.load() == 2000); +} + +static void NullAddresses() { + KernelUseconds timeout = SHORT_TIMEOUT; + Require(sceKernelSyncOnAddressWait(nullptr, 1, &timeout, nullptr) == SCE_KERNEL_ERROR_EFAULT && timeout == SHORT_TIMEOUT); + Require(sceKernelSyncOnAddressWait8(nullptr, 1, nullptr, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelSyncOnAddressWait16(nullptr, 1, nullptr, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelSyncOnAddressWait32(nullptr, 1, nullptr, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelSyncOnAddressWait64(nullptr, 1, nullptr, nullptr) == SCE_KERNEL_ERROR_EFAULT); + KernelUseconds none = 0; + Require(sceKernelSyncOnAddressWait32(nullptr, 1, &none, nullptr) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelSyncOnAddressWake(nullptr, 1) == 0); + Require(sceKernelSyncOnAddressWake(nullptr, 0) == 0); + std::uint32_t word = 0; + Require(sceKernelSyncOnAddressWake(&word, -1) == 0); } int main() { - ReturnsWhenTheValueDiffers(); + FailsAgainWhenTheValueDiffers(); TimesOutWhileTheValueMatches(); - WakeReleasesAWaiterWithoutAValueChange(); - WakeHonoursTheCountAndTheAddress(); + WakeReleasesAWaiterAndReportsTheTimeLeft(); + WakeCountsOfOneOrLessWakeOneWaiter(); WaitsOnTheLowHalfOfAnOnceState(); SizedWaitsTimeOutWhileTheValueMatches(); Wait64ComparesTheHighHalf(); WakeReleasesWaitersOfEverySize(); NarrowWaitsCompareOnlyTheirWidth(); WakeReleasesNarrowWaitersAtTheirOwnAddress(); - RejectsInvalidArguments(); + MisalignedAddressesWaitAndWake(); + NullAddresses(); } diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index 643b4761e0..f4b5946c3c 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -175,6 +176,120 @@ static int RenderedPixels(FontHandle font, std::uint32_t code, float& advance) { return lit; } +struct GlyphImage { + int rc = SCE_FONT_OK; + std::vector pixels = std::vector(128 * 128); + FontGlyphMetrics metrics{}; + FontRenderOutput result{}; +}; + +template static GlyphImage RenderInto(TRender render) { + GlyphImage image; + FontRenderSurface surface{}; + sceFontRenderSurfaceInit(&surface, image.pixels.data(), 128, 1, 128, 128); + image.rc = render(&surface, &image.metrics, &image.result); + return image; +} + +static bool SurfaceOffset(const GlyphImage& image, std::uintptr_t& offset) { + const auto& surface = image.result.SurfaceImage; + const auto& rect = image.result.UpdateRect; + const auto base = reinterpret_cast(image.pixels.data()); + const auto address = reinterpret_cast(surface.address); + const std::uintptr_t span = rect.w == 0 || rect.h == 0 ? 0 : static_cast(rect.h - 1) * surface.widthByte + static_cast(rect.w) * surface.pixelSizeByte; + if (address < base || address - base > image.pixels.size() || span > image.pixels.size() - (address - base)) return false; + offset = address - base; + return true; +} + +static bool SameImage(const GlyphImage& left, const GlyphImage& right) { + int lit = 0; + for (const std::uint8_t pixel : left.pixels) lit += pixel != 0 ? 1 : 0; + const auto& a = left.result; + const auto& b = right.result; + std::uintptr_t leftOffset = 0; + std::uintptr_t rightOffset = 0; + const bool sameSurface = SurfaceOffset(left, leftOffset) && SurfaceOffset(right, rightOffset) && leftOffset == rightOffset && a.SurfaceImage.widthByte == b.SurfaceImage.widthByte && a.SurfaceImage.pixelSizeByte == b.SurfaceImage.pixelSizeByte && a.SurfaceImage.pixelFormat == b.SurfaceImage.pixelFormat; + const bool sameOutput = a.stage == b.stage && sameSurface && std::memcmp(&a.UpdateRect, &b.UpdateRect, sizeof(a.UpdateRect)) == 0 && std::memcmp(&a.ImageMetrics, &b.ImageMetrics, sizeof(a.ImageMetrics)) == 0; + return left.rc == SCE_FONT_OK && right.rc == SCE_FONT_OK && lit > 0 && left.pixels == right.pixels && std::memcmp(&left.metrics, &right.metrics, sizeof(left.metrics)) == 0 && sameOutput; +} + +static bool Cleared(const GlyphImage& image, int rc) { + const FontGlyphMetrics noMetrics{}; + const FontRenderOutput noResult{}; + return image.rc == rc && std::memcmp(&image.metrics, &noMetrics, sizeof(noMetrics)) == 0 && std::memcmp(&image.result, &noResult, sizeof(noResult)) == 0; +} + +static void CheckGlyphImages(FontHandle font, FontRenderer renderer) { + Require(sceFontBindRenderer(font, renderer) == SCE_FONT_OK); + Require(sceFontSetScalePixel(font, 40.0f, 40.0f) == SCE_FONT_OK); + Require(sceFontSetupRenderScalePixel(font, 40.0f, 40.0f) == SCE_FONT_OK); + FontGlyph glyph = nullptr; + Require(sceFontGenerateCharGlyph(font, 'A', nullptr, &glyph) == SCE_FONT_OK && glyph != nullptr); + + const auto horizontal = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImageHorizontal(font, 'A', surface, 10.0f, 100.0f, metrics, result); }); + Require(SameImage(horizontal, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); }))); + const auto written = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImage(font, 'A', surface, 10.0f, 20.0f, metrics, result); }); + Require(SameImage(written, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImage(glyph, nullptr, renderer, surface, 10.0f, 20.0f, metrics, result); }))); + + Require(sceFontSetupRenderScalePixel(font, 20.0f, 20.0f) == SCE_FONT_OK); + Require(sceFontSetScalePixel(font, 20.0f, 20.0f) == SCE_FONT_OK); + const auto smaller = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImageHorizontal(font, 'A', surface, 10.0f, 100.0f, metrics, result); }); + Require(smaller.rc == SCE_FONT_OK && smaller.pixels != horizontal.pixels); + Require(SameImage(horizontal, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); }))); + FontStyleFrame frame{}; + Require(sceFontStyleFrameInit(&frame) == SCE_FONT_OK && sceFontStyleFrameSetScalePixel(&frame, 20.0f, 20.0f) == SCE_FONT_OK); + Require(SameImage(smaller, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageHorizontal(glyph, &frame, renderer, surface, 10.0f, 100.0f, metrics, result); }))); + Require(SameImage(smaller, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { + Require(sceFontRenderSurfaceSetStyleFrame(surface, &frame) == SCE_FONT_OK); + return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); + }))); + FontStyleFrame larger{}; + Require(sceFontStyleFrameInit(&larger) == SCE_FONT_OK && sceFontStyleFrameSetScalePixel(&larger, 30.0f, 30.0f) == SCE_FONT_OK); + const auto surfaceLarger = RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { + Require(sceFontRenderSurfaceSetStyleFrame(surface, &larger) == SCE_FONT_OK); + return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); + }); + Require(surfaceLarger.rc == SCE_FONT_OK && surfaceLarger.pixels != smaller.pixels && surfaceLarger.pixels != horizontal.pixels); + Require(SameImage(surfaceLarger, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { + Require(sceFontRenderSurfaceSetStyleFrame(surface, &larger) == SCE_FONT_OK); + return sceFontGlyphRenderImageHorizontal(glyph, &frame, renderer, surface, 10.0f, 100.0f, metrics, result); + }))); + Require(SameImage(smaller, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { + Require(sceFontRenderSurfaceSetStyleFrame(surface, &frame) == SCE_FONT_OK); + return sceFontGlyphRenderImageHorizontal(glyph, &larger, renderer, surface, 10.0f, 100.0f, metrics, result); + }))); + FontStyleFrame notAFrame{}; + FontStyleFrame initialized{}; + Require(sceFontStyleFrameInit(&initialized) == SCE_FONT_OK); + for (const FontStyleFrame* unscaled : {¬AFrame, &initialized}) { + Require(SameImage(horizontal, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageHorizontal(glyph, unscaled, renderer, surface, 10.0f, 100.0f, metrics, result); }))); + Require(SameImage(written, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImage(glyph, unscaled, renderer, surface, 10.0f, 20.0f, metrics, result); }))); + } + + Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); + Require(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontRenderCharGlyphImageHorizontal(font, 'A', surface, 10.0f, 100.0f, metrics, result); }).rc == SCE_FONT_ERROR_NOT_BOUND_RENDERER); + Require(SameImage(horizontal, RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { return sceFontGlyphRenderImageHorizontal(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); }))); + + const auto filled = [](FontGlyphMetrics* metrics, FontRenderOutput* result) { + std::memset(metrics, 0xA5, sizeof(*metrics)); + std::memset(result, 0xA5, sizeof(*result)); + }; + FontGlyphOpaque notAGlyph{}; + for (const FontGlyph wrong : {static_cast(nullptr), ¬AGlyph}) { + for (const auto render : {sceFontGlyphRenderImage, sceFontGlyphRenderImageHorizontal, sceFontGlyphRenderImageVertical}) { + Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(wrong, nullptr, renderer, surface, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_GLYPH)); + } + } + for (const auto render : {sceFontGlyphRenderImage, sceFontGlyphRenderImageHorizontal, sceFontGlyphRenderImageVertical}) { + Require(Cleared(RenderInto([&](FontRenderSurface*, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(glyph, nullptr, renderer, nullptr, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_PARAMETER)); + Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics*, FontRenderOutput* result) { std::memset(result, 0xA5, sizeof(*result)); return render(glyph, nullptr, renderer, surface, 10.0f, 100.0f, nullptr, result); }), SCE_FONT_ERROR_INVALID_PARAMETER)); + Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput*) { std::memset(metrics, 0xA5, sizeof(*metrics)); return render(glyph, nullptr, renderer, surface, 10.0f, 100.0f, metrics, nullptr); }), SCE_FONT_ERROR_INVALID_PARAMETER)); + Require(Cleared(RenderInto([&](FontRenderSurface* surface, FontGlyphMetrics* metrics, FontRenderOutput* result) { filled(metrics, result); return render(glyph, nullptr, nullptr, surface, 10.0f, 100.0f, metrics, result); }), SCE_FONT_ERROR_INVALID_RENDERER)); + } + Require(sceFontDeleteGlyph(nullptr, &glyph) == SCE_FONT_OK && glyph == nullptr); +} + int main() { constexpr std::uint32_t EuropeanLight = 0x180700C3u; constexpr std::uint32_t EuropeanBold = 0x180700C7u; @@ -310,6 +425,7 @@ int main() { Require(sceFontGetKerning(mixed, 'A', 0x4E00, &kerning) == SCE_FONT_OK && kerning.offsetX == 0.0f); FontRenderer renderer = nullptr; Require(sceFontCreateRenderer(&memory, sceFontSelectRendererFt(0), &renderer) == SCE_FONT_OK); + CheckGlyphImages(memoryFont, renderer); Require(sceFontBindRenderer(mixed, renderer) == SCE_FONT_OK); Require(sceFontSetupRenderScalePixel(mixed, 100.0f, 100.0f) == SCE_FONT_OK); float advance = 0.0f; diff --git a/core/libs/tests/GuestUnwindModuleInfoMacos.cpp b/core/libs/tests/GuestUnwindModuleInfoMacos.cpp index ea43177833..e600903695 100644 --- a/core/libs/tests/GuestUnwindModuleInfoMacos.cpp +++ b/core/libs/tests/GuestUnwindModuleInfoMacos.cpp @@ -1,3 +1,4 @@ +#include "prx/libkernel/include/Api.hpp" #include "SceTypes.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include "tests/GuestUnwindModuleInfoFixture.hpp" @@ -6,8 +7,6 @@ #include #include -extern "C" int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleInfoForUnwind* info); - struct GuestImageMeta { std::uint64_t version = 2; std::uint64_t loadBias = 0; diff --git a/core/libs/tests/GuestUsbStorage.cpp b/core/libs/tests/GuestUsbStorage.cpp index 43a173d247..4013c5af5f 100644 --- a/core/libs/tests/GuestUsbStorage.cpp +++ b/core/libs/tests/GuestUsbStorage.cpp @@ -1,15 +1,33 @@ #include "prx/libSceUsbStorage/UsbStorage.hpp" +#include "prx/libSceUsbStorage/include/HostVolumes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include #include #include #include #include +#include +#include +#include +#include +#include #include +#include #include +#include +#include +#include +#include +#include namespace { int g_attr = 0; +int g_mountTag = 0; +int g_unmountTag = 0; void APS5_VABI CallbackStub(const UsbStorageEvent*, void*) {} @@ -24,91 +42,266 @@ void Require(bool condition, const char* message) { } } +template +bool ThrowsWith(TCall call, std::string_view message) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string_view(error.what()) == message; + } + return false; +} + +struct RecordedEvent { + UsbStorageEvent event; + void* userData; +}; + +struct Recorder { + std::mutex mutex; + std::condition_variable changed; + std::vector events; +}; + +Recorder& Events() { + static Recorder recorder; + return recorder; +} + +void APS5_VABI Record(const UsbStorageEvent* event, void* userData) { + auto& recorder = Events(); + std::lock_guard lock(recorder.mutex); + recorder.events.push_back({*event, userData}); + recorder.changed.notify_all(); +} + +RecordedEvent NextEvent(std::size_t index) { + auto& recorder = Events(); + std::unique_lock lock(recorder.mutex); + const bool arrived = recorder.changed.wait_for(lock, std::chrono::seconds(10), [&] { return recorder.events.size() > index; }); + Require(arrived, "no USB storage event arrived within 10 seconds"); + return recorder.events[index]; +} + +std::size_t RecordedCount() { + auto& recorder = Events(); + std::lock_guard lock(recorder.mutex); + return recorder.events.size(); +} + +std::atomic g_selfRemovalCalls{0}; +std::atomic g_selfRemovalResult{-1}; + +void APS5_VABI RemoveSelf(const UsbStorageEvent*, void*) { + ++g_selfRemovalCalls; + g_selfRemovalResult = sceUsbStorageUnregisterCallback(UsbStorage::MountEvent, &RemoveSelf); +} + +struct Blocker { + std::mutex mutex; + std::condition_variable changed; + int calls = 0; + bool entered = false; + bool released = false; +}; + +Blocker& Blocked() { + static Blocker blocker; + return blocker; +} + +void APS5_VABI BlockInside(const UsbStorageEvent*, void*) { + auto& blocker = Blocked(); + std::unique_lock lock(blocker.mutex); + ++blocker.calls; + blocker.entered = true; + blocker.changed.notify_all(); + blocker.changed.wait_for(lock, std::chrono::seconds(10), [&] { return blocker.released; }); +} + +struct FakeHost { + std::filesystem::path root; + std::filesystem::path mountInfo; + std::filesystem::path sys; + std::filesystem::path firstMount; + std::filesystem::path secondMount; + std::filesystem::path movedMount; +}; + +void WriteFile(const std::filesystem::path& path, std::string_view text) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream file(path, std::ios::binary); + file << text; + file.close(); + Require(static_cast(file), "write a fake host file"); +} + +void WriteMounts(const FakeHost& host, const std::string& lines) { + auto staged = host.mountInfo; + staged += ".new"; + WriteFile(staged, lines); + std::error_code error; + for (int attempt = 0; attempt < 500; ++attempt) { + std::filesystem::rename(staged, host.mountInfo, error); + if (!error) return; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + Require(false, "replace the fake mountinfo"); +} + +std::string Escaped(const std::filesystem::path& path) { + std::string escaped; + for (const char character : path.string()) { + if (character == ' ') escaped += "\\040"; + else if (character == '\\') escaped += "\\134"; + else escaped += character; + } + return escaped; +} + +std::string MountLine(std::string_view device, std::string_view root, const std::filesystem::path& mount, std::string_view options, std::string_view type) { + return "36 25 " + std::string(device) + " " + std::string(root) + " " + Escaped(mount) + " " + std::string(options) + + " shared:1 - " + std::string(type) + " /dev/fake rw\n"; +} + +std::string FirstLine(const FakeHost& host) { + return MountLine("8_17", "/", host.firstMount, "rw,nosuid,relatime", "vfat"); +} + +std::string SecondLine(const FakeHost& host) { + return MountLine("8_33", "/", host.secondMount, "ro,relatime", "exfat"); +} + +constexpr std::uint64_t FirstVolumeSize = 61530439680ull; +constexpr std::uint64_t SecondVolumeSize = 31914983424ull; + +std::string Utf8(const std::filesystem::path& path) { + const auto text = path.u8string(); + return std::string(text.begin(), text.end()); +} + +void Inject(const FakeHost& host, bool first, bool second, bool firstMoved = false) { + static const UsbStorageTestVolume none{}; + const auto firstRoot = Utf8(firstMoved ? host.movedMount : host.firstMount); + const auto secondRoot = Utf8(host.secondMount); + std::vector volumes; + if (first) volumes.push_back({"first", firstRoot.c_str(), FirstVolumeSize, 0x0781, 0x5581, 0x0100, "SanDisk", "Ultra Fit", "4C530001", false, false}); + if (second) volumes.push_back({"second", secondRoot.c_str(), SecondVolumeSize, 0x090c, 0x1000, 0x1100, "Samsung", "Flash Drive", "", true, true}); + SetUsbStorageHostVolumesForTest_nid_no_patch(volumes.empty() ? &none : volumes.data(), volumes.size()); +} + +void SetPassthrough(bool enabled) { +#ifdef _WIN32 + Require(_putenv_s("APS5_USB_STORAGE_PASSTHROUGH", enabled ? "1" : "") == 0, "set the passthrough switch"); +#else + Require((enabled ? ::setenv("APS5_USB_STORAGE_PASSTHROUGH", "1", 1) : ::unsetenv("APS5_USB_STORAGE_PASSTHROUGH")) == 0, "set the passthrough switch"); +#endif +} + +FakeHost CreateHost() { + FakeHost host; + host.root = std::filesystem::temp_directory_path() / ("anyps5_usb_storage_" + std::to_string(std::random_device{}())); + host.mountInfo = host.root / "mountinfo"; + host.sys = host.root / "sys"; + host.firstMount = host.root / "media" / "USB DRIVE"; + host.secondMount = host.root / "media" / "second"; + host.movedMount = host.root / "media" / "moved"; + WriteFile(host.firstMount / "song.txt", "song"); + WriteFile(host.movedMount / "moved.txt", "moved"); + WriteFile(host.firstMount / ".hidden", "hidden"); + std::filesystem::create_directories(host.firstMount / "Music"); + WriteFile(host.secondMount / "Readme.txt", "readme"); + WriteFile(host.root / "media" / "efi" / "boot.efi", "efi"); + Inject(host, false, false); + return host; +} + +bool Link(const FakeHost& host, std::string_view device, const std::filesystem::path& target) { + std::filesystem::create_directories(target); + std::error_code error; + std::filesystem::create_directory_symlink(target, host.sys / "dev" / "block" / std::string(device), error); + return !error; +} + +bool CreateSysfs(const FakeHost& host) { + std::filesystem::create_directories(host.sys / "dev" / "block"); + if (!Link(host, "0_0", host.sys / "devices" / "probe")) return false; + const auto controller = host.sys / "devices" / "pci0000_00" / "0000_00_14.0" / "usb1"; + const auto first = controller / "1-1"; + WriteFile(first / "idVendor", "0781\n"); + WriteFile(first / "idProduct", "5581\n"); + WriteFile(first / "bcdDevice", "0100\n"); + WriteFile(first / "serial", "4C530001\n"); + WriteFile(first / "1-1_1.0" / "bInterfaceClass", "08\n"); + const auto firstScsi = first / "1-1_1.0" / "host6" / "target6_0_0" / "6_0_0_0"; + WriteFile(firstScsi / "vendor", "SanDisk \n"); + WriteFile(firstScsi / "model", "Ultra Fit \n"); + const auto second = controller / "1-2"; + WriteFile(second / "idVendor", "090c\n"); + WriteFile(second / "idProduct", "1000\n"); + WriteFile(second / "bcdDevice", "1100\n"); + WriteFile(second / "1-2_1.0" / "bInterfaceClass", "08\n"); + const auto secondScsi = second / "1-2_1.0" / "host7" / "target7_0_0" / "7_0_0_0"; + WriteFile(secondScsi / "vendor", "Samsung \n"); + WriteFile(secondScsi / "model", "Flash Drive \n"); + const auto sata = host.sys / "devices" / "pci0000_00" / "0000_00_17.0" / "ata1" / "host0" / "target0_0_0" / "0_0_0_0"; + WriteFile(sata / "vendor", "ATA \n"); + WriteFile(sata / "model", "Disk \n"); + return Link(host, "8_17", firstScsi / "block" / "sdb" / "sdb1") && Link(host, "8_33", secondScsi / "block" / "sdc" / "sdc1") && + Link(host, "8_1", sata / "block" / "sda" / "sda1") && Link(host, "8_49", firstScsi / "block" / "sdd" / "sdd1"); +} + void TestInitializeAndTerminate() { + Require(sceUsbStorageTerm() == 0, "terminate before initialize"); Require(sceUsbStorageInit(nullptr) == 0, "initialize with a null attribute"); - Require(sceUsbStorageInit(&g_attr) == 0, "initialize with an attribute"); + Require(sceUsbStorageInit(nullptr) == 0, "initialize again"); Require(sceUsbStorageTerm() == 0, "terminate"); - Require(sceUsbStorageInit(nullptr) == 0, "initialize after terminate"); Require(sceUsbStorageTerm() == 0, "terminate again"); + Require(sceUsbStorageInit(nullptr) == 0, "initialize after terminate"); + Require(sceUsbStorageTerm() == 0, "terminate after initializing again"); } void TestRegisterCallbacks() { - bool threw = false; - try { - sceUsbStorageRegisterCallback(1, Callback(), nullptr); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRegisterCallback not implemented"; - } - Require(threw, "register callback of event type 1 did not throw its marker"); - threw = false; - try { - sceUsbStorageRegisterCallback(2, Callback(), nullptr); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRegisterCallback not implemented"; - } - Require(threw, "register callback of event type 2 did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageRegisterCallback(1, Callback(), nullptr); }, + "sceUsbStorageRegisterCallback: registering before sceUsbStorageInit is not implemented"), + "register a callback before initialize did not throw"); + Require(ThrowsWith([] { sceUsbStorageUnregisterCallback(1, Callback()); }, "sceUsbStorageUnregisterCallback not implemented"), + "unregister a callback before initialize did not throw its marker"); Require(sceUsbStorageRegisterCallback(1, nullptr, nullptr) == UsbStorage::InvalidArgument, "register a null callback"); Require(sceUsbStorageRegisterCallback(0, Callback(), nullptr) == UsbStorage::InvalidArgument, "register callback of event type 0"); Require(sceUsbStorageRegisterCallback(3, Callback(), nullptr) == UsbStorage::InvalidArgument, "register callback of event type 3"); Require(sceUsbStorageRegisterCallback(-1, Callback(), nullptr) == UsbStorage::InvalidArgument, "register callback of a negative event type"); - threw = false; - try { - sceUsbStorageUnregisterCallback(1, Callback()); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageUnregisterCallback not implemented"; - } - Require(threw, "unregister callback of event type 1 did not throw its marker"); - threw = false; - try { - sceUsbStorageUnregisterCallback(2, Callback()); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageUnregisterCallback not implemented"; - } - Require(threw, "unregister callback of event type 2 did not throw its marker"); + Require(sceUsbStorageInit(nullptr) == 0, "initialize for callbacks"); + Require(sceUsbStorageRegisterCallback(1, Callback(), nullptr) == 0, "register a mount callback"); + Require(sceUsbStorageRegisterCallback(2, Callback(), &g_attr) == 0, "register an unmount callback"); + Require(ThrowsWith([] { sceUsbStorageRegisterCallback(1, Callback(), &g_attr); }, + "sceUsbStorageRegisterCallback: registering a registered callback again is not implemented"), + "register a registered callback again did not throw"); Require(sceUsbStorageUnregisterCallback(1, nullptr) == UsbStorage::InvalidArgument, "unregister a null callback"); Require(sceUsbStorageUnregisterCallback(0, Callback()) == UsbStorage::InvalidArgument, "unregister callback of event type 0"); + Require(sceUsbStorageUnregisterCallback(1, Callback()) == 0, "unregister the mount callback"); + Require(sceUsbStorageUnregisterCallback(2, Callback()) == 0, "unregister the unmount callback"); + Require(ThrowsWith([] { sceUsbStorageUnregisterCallback(1, Callback()); }, "sceUsbStorageUnregisterCallback not implemented"), + "unregister an unregistered callback did not throw its marker"); + Require(sceUsbStorageRegisterCallback(2, Callback(), nullptr) == 0, "register a callback before terminate"); + Require(sceUsbStorageTerm() == 0, "terminate with a registered callback"); + Require(ThrowsWith([] { sceUsbStorageUnregisterCallback(2, Callback()); }, "sceUsbStorageUnregisterCallback not implemented"), + "terminate kept the registered callback"); } void TestMapCallbacks() { - bool threw = false; - try { - sceUsbStorageRegisterCallbackForMapAvailable(0, "/data", Callback(), nullptr); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRegisterCallbackForMapAvailable not implemented"; - } - Require(threw, "register map-available callback did not throw its marker"); Require(sceUsbStorageRegisterCallbackForMapAvailable(0, nullptr, Callback(), nullptr) == UsbStorage::InvalidArgument, "register map-available callback with a null directory"); Require(sceUsbStorageRegisterCallbackForMapAvailable(0, "/data", nullptr, nullptr) == UsbStorage::InvalidArgument, "register a null map-available callback"); - threw = false; - try { - sceUsbStorageUnregisterCallbackForMapAvailable(0, "/data", Callback()); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageUnregisterCallbackForMapAvailable not implemented"; - } - Require(threw, "unregister map-available callback did not throw its marker"); Require(sceUsbStorageUnregisterCallbackForMapAvailable(0, nullptr, Callback()) == UsbStorage::InvalidArgument, "unregister map-available callback with a null directory"); Require(sceUsbStorageUnregisterCallbackForMapAvailable(0, "/data", nullptr) == UsbStorage::InvalidArgument, "unregister a null map-available callback"); - threw = false; - try { - sceUsbStorageRegisterCallbackForMapUnavailable(0, "/data", Callback(), nullptr); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRegisterCallbackForMapUnavailable not implemented"; - } - Require(threw, "register map-unavailable callback did not throw its marker"); Require(sceUsbStorageRegisterCallbackForMapUnavailable(0, nullptr, Callback(), nullptr) == UsbStorage::InvalidArgument, "register map-unavailable callback with a null directory"); Require(sceUsbStorageRegisterCallbackForMapUnavailable(0, "/data", nullptr, nullptr) == UsbStorage::InvalidArgument, "register a null map-unavailable callback"); - threw = false; - try { - sceUsbStorageUnregisterCallbackForMapUnavailable(0, "/data", Callback()); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageUnregisterCallbackForMapUnavailable not implemented"; - } - Require(threw, "unregister map-unavailable callback did not throw its marker"); Require(sceUsbStorageUnregisterCallbackForMapUnavailable(0, nullptr, Callback()) == UsbStorage::InvalidArgument, "unregister map-unavailable callback with a null directory"); Require(sceUsbStorageUnregisterCallbackForMapUnavailable(0, "/data", nullptr) == UsbStorage::InvalidArgument, "unregister a null map-unavailable callback"); } void TestNoDevice() { + Require(sceUsbStorageInit(nullptr) == 0, "initialize without a device"); std::uint32_t devices[4]{0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; std::int32_t count = -1; Require(sceUsbStorageGetDeviceList(devices, &count) == 0, "get the device list"); @@ -117,12 +310,11 @@ void TestNoDevice() { Require(sceUsbStorageGetDeviceList(nullptr, &count) == UsbStorage::InvalidArgument, "get the device list without a device array"); Require(sceUsbStorageGetDeviceList(devices, nullptr) == UsbStorage::InvalidArgument, "get the device list without a count"); - std::uint8_t info[16]; - std::memset(info, 0x5a, sizeof(info)); - auto* const infoRecord = reinterpret_cast(info); - Require(sceUsbStorageGetDeviceInfo(0, infoRecord) == UsbStorage::DeviceNotFound, "get device info of device 0"); - Require(sceUsbStorageGetDeviceInfo(1, infoRecord) == UsbStorage::DeviceNotFound, "get device info of device 1"); - Require(info[0] == 0x5a && info[15] == 0x5a, "device info was written"); + UsbStorageDeviceInfo info; + std::memset(&info, 0x5a, sizeof(info)); + Require(sceUsbStorageGetDeviceInfo(0, &info) == UsbStorage::DeviceNotFound, "get device info of device 0"); + Require(sceUsbStorageGetDeviceInfo(1, &info) == UsbStorage::DeviceNotFound, "get device info of device 1"); + Require(info.volumeSize == 0x5a5a5a5a5a5a5a5aull && info.flags == 0x5a5a5a5a5a5a5a5aull, "device info was written"); Require(sceUsbStorageGetDeviceInfo(0, nullptr) == UsbStorage::InvalidArgument, "get device info without an info buffer"); std::uint8_t exists = 1; @@ -131,6 +323,9 @@ void TestNoDevice() { Require(sceUsbStorageIsExist(1, nullptr, &exists) == true && exists == 0, "device 1 does not exist"); exists = 1; Require(sceUsbStorageIsExist(0xffffffffu, "/mnt/usb0", &exists) == true && exists == 0, "no device id exists"); + Require(ThrowsWith([] { sceUsbStorageIsExist(0, "/mnt/usb0", nullptr); }, "sceUsbStorageIsExist: a null output is not implemented"), + "an existence query without an output did not throw"); + Require(sceUsbStorageTerm() == 0, "terminate without a device"); } void TestMapRequests() { @@ -146,20 +341,10 @@ void TestMapRequests() { Require(sceUsbStorageRequestMapWSB(0, nullptr, 0, 0, nullptr, nullptr) == UsbStorage::DeviceNotFound, "request a WSB map without a subpath"); Require(mountPoint[0] == static_cast(0x5a) && status == 0x5a5a5a5a, "the WSB map request wrote its outputs"); Require(sceUsbStorageRequestUnmap(0, nullptr) == UsbStorage::InvalidArgument, "unmap a null directory"); - bool threw = false; - try { - sceUsbStorageRequestUnmap(0, "/mnt/usb0"); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRequestUnmap not implemented"; - } - Require(threw, "unmap a directory did not throw its marker"); - threw = false; - try { - sceUsbStorageRequestUnmap(7, "/mnt/usb7"); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageRequestUnmap not implemented"; - } - Require(threw, "unmap a directory of another device did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageRequestUnmap(0, "/mnt/usb0"); }, "sceUsbStorageRequestUnmap not implemented"), + "unmap a directory did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageRequestUnmap(7, "/mnt/usb7"); }, "sceUsbStorageRequestUnmap not implemented"), + "unmap a directory of another device did not throw its marker"); } void TestGetdents() { @@ -180,57 +365,306 @@ void TestGetdents() { Require(sceUsbStorageGetdentsRead(1, nullptr, sizeof(buffer), &bytesRead) == UsbStorage::InvalidArgument, "read a getdents scan without a buffer"); Require(sceUsbStorageGetdentsRead(1, buffer, sizeof(buffer), nullptr) == UsbStorage::InvalidArgument, "read a getdents scan without a byte count"); - bool threw = false; - try { - sceUsbStorageGetdentsClose(1); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageGetdentsClose not implemented"; - } - Require(threw, "close a getdents scan that was never opened did not throw its marker"); - threw = false; - try { - sceUsbStorageGetdentsClose(0); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageGetdentsClose not implemented"; - } - Require(threw, "close descriptor 0 did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageGetdentsClose(1); }, "sceUsbStorageGetdentsClose not implemented"), + "close a getdents scan that was never opened did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageGetdentsClose(0); }, "sceUsbStorageGetdentsClose not implemented"), + "close descriptor 0 did not throw its marker"); } void TestThrowingExports() { char mountPoint[8]{}; - bool threw = false; - try { - sceUsbStorageGetMountPointOfShellCore(0, mountPoint); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageGetMountPointOfShellCore not implemented"; + Require(ThrowsWith([&] { sceUsbStorageGetMountPointOfShellCore(0, mountPoint); }, "sceUsbStorageGetMountPointOfShellCore not implemented"), + "get the mount point of shell core without a device did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageClearFakeWriteProtection(); }, "sceUsbStorageClearFakeWriteProtection not implemented"), + "clear fake write protection did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageSetFakeMapLockForDebug(); }, "sceUsbStorageSetFakeMapLockForDebug not implemented"), + "set fake map lock for debug did not throw its marker"); + Require(ThrowsWith([] { sceUsbStorageSetFakeWriteProtection(); }, "sceUsbStorageSetFakeWriteProtection not implemented"), + "set fake write protection did not throw its marker"); +} + +void TestDeviceInfo(const FakeHost& host) { + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0, "get the device list with a device"); + Require(count == 1 && devices[0] == 0, "the attached volume is listed as device 0"); + + UsbStorageDeviceInfo info; + std::memset(&info, 0x5a, sizeof(info)); + Require(sceUsbStorageGetDeviceInfo(0, &info) == 0, "get device info of the attached device"); + Require(info.vendorId == 0x0781 && info.productId == 0x5581 && info.bcdDevice == 0x0100, "the device info holds the USB ids and release"); + Require(std::string_view(info.vendor) == "SanDisk" && info.vendorLength == 7, "the device info holds the vendor"); + Require(std::string_view(info.product) == "Ultra Fit" && info.productLength == 9, "the device info holds the product"); + Require(std::string_view(info.serialNumber) == "4C530001" && info.serialNumberLength == 8, "the device info holds the serial number"); + Require(info.flags == UsbStorage::FatFlag, "the device info flags a writable FAT volume"); + Require(info.reserved == 0 && info.padding[0] == 0 && info.padding[1] == 0, "the device info cleared its unknown fields"); + Require(info.volumeSize == FirstVolumeSize, "the device info holds the volume size"); + Require(sceUsbStorageGetDeviceInfo(1, &info) == UsbStorage::DeviceNotFound, "get device info of an absent device"); + + char mountPoint[UsbStorage::MountPointSize]; + std::memset(mountPoint, 0x5a, sizeof(mountPoint)); + Require(sceUsbStorageGetMountPointOfShellCore(0, mountPoint) == 0 && std::string_view(mountPoint) == "/mnt/usb0", "the device is mounted at /mnt/usb0"); + std::uint8_t exists = 0; + Require(sceUsbStorageIsExist(0, "/", &exists) == true && exists == 1, "the root of the attached device exists"); + exists = 0; + Require(sceUsbStorageIsExist(0, nullptr, &exists) == true && exists == 1, "a null directory is the root of the attached device"); + exists = 0; + Require(sceUsbStorageIsExist(0, "Music/", &exists) == true && exists == 1, "a directory of the attached device exists"); + exists = 1; + Require(sceUsbStorageIsExist(0, "/Missing", &exists) == true && exists == 0, "a missing directory does not exist"); + exists = 1; + Require(sceUsbStorageIsExist(0, "/song.txt", &exists) == true && exists == 0, "a file is not a directory"); + Require(ThrowsWith([&] { sceUsbStorageIsExist(0, "/Music/../Music", &exists); }, "sceUsbStorageIsExist: a directory with . or .. components is not implemented"), + "an existence query with a .. component did not throw"); + exists = 1; + Require(sceUsbStorageIsExist(1, "/", &exists) == true && exists == 0, "device 1 does not exist"); + const auto resolved = ResolvePath_nid_no_patch("/mnt/usb0/song.txt"); + std::error_code error; + Require(std::filesystem::equivalent(resolved, host.firstMount / "song.txt", error), "the mount point resolves to the host volume"); +} + +void TestDeviceMaps() { + char mountPoint[UsbStorage::MountPointSize]; + std::uint64_t mountId = 0x5a5a5a5a5a5a5a5aull; + Require(sceUsbStorageRequestMap(0, "/", 0, 0, mountPoint, &mountId) == 0, "map the root of the device"); + Require(std::string_view(mountPoint) == "/mnt/usb0" && mountId == 0, "the root is mapped at the mount point"); + Require(sceUsbStorageRequestMap(0, "Music/", 0, 0, mountPoint, nullptr) == 0, "map a directory of the device"); + Require(std::string_view(mountPoint) == "/mnt/usb0/Music", "the directory is mapped below the mount point"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/Missing", 0, 0, mountPoint, nullptr); }, + "sceUsbStorageRequestMap: mapping a directory the device does not hold is not implemented"), + "map a missing directory did not throw"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/song.txt", 0, 0, mountPoint, nullptr); }, + "sceUsbStorageRequestMap: mapping a directory the device does not hold is not implemented"), + "map a file did not throw"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/Music/../Music", 0, 0, mountPoint, nullptr); }, + "sceUsbStorageRequestMap: a directory with . or .. components is not implemented"), + "map a directory with a .. component did not throw"); +#ifdef _WIN32 + constexpr std::string_view otherDrive = "sceUsbStorageRequestMap: a directory with a root name is not implemented"; +#else + constexpr std::string_view otherDrive = "sceUsbStorageRequestMap: mapping a directory the device does not hold is not implemented"; +#endif + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "C:/Windows", 0, 0, mountPoint, nullptr); }, otherDrive), + "map a directory of another drive did not throw"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "/C:Windows", 0, 0, mountPoint, nullptr); }, otherDrive), + "map a drive-relative directory did not throw"); + Require(ThrowsWith([&] { sceUsbStorageRequestMap(0, "Music\\..", 0, 0, mountPoint, nullptr); }, + "sceUsbStorageRequestMap: a directory with a backslash is not implemented"), + "map a directory with a backslash did not throw"); + Require(sceUsbStorageRequestUnmap(0, "/mnt/usb0/Music") == 0, "unmap the directory"); + Require(ThrowsWith([] { sceUsbStorageRequestUnmap(0, "/mnt/usb0/Music"); }, "sceUsbStorageRequestUnmap not implemented"), + "unmap an unmapped directory did not throw its marker"); + Require(sceUsbStorageRequestUnmap(0, "/mnt/usb0") == 0, "unmap the root"); + std::int32_t status = 0x5a5a5a5a; + Require(sceUsbStorageRequestMapWSB(0, nullptr, 0, 0, mountPoint, &status) == 0, "map the root through the WSB request"); + Require(std::string_view(mountPoint) == "/mnt/usb0" && status == 0, "the WSB request mapped the root at the mount point"); + Require(sceUsbStorageRequestUnmap(0, "/mnt/usb0") == 0, "unmap the WSB root"); +} + +void TestDeviceGetdents() { + std::int32_t fd = -1; + std::uint64_t bufferSize = 0; + Require(sceUsbStorageGetdentsOpen(0, &fd, &bufferSize) == 0 && fd > 0 && bufferSize == 0x8000, "open a getdents scan of the device"); + std::uint64_t bytesRead = 0; + std::vector buffer(0x8000, 0x5a); + Require(sceUsbStorageGetdentsRead(fd, buffer.data(), buffer.size(), &bytesRead) == 0, "read the getdents scan"); + std::set> entries; + std::set fileNumbers; + for (std::uint64_t offset = 0; offset < bytesRead;) { + std::uint32_t fileNumber = 0; + std::uint16_t length = 0; + std::memcpy(&fileNumber, buffer.data() + offset, sizeof(fileNumber)); + std::memcpy(&length, buffer.data() + offset + 4, sizeof(length)); + const std::uint8_t type = buffer[offset + 6]; + const std::uint8_t nameLength = buffer[offset + 7]; + const std::string name(reinterpret_cast(buffer.data() + offset + 8), nameLength); + Require(length == ((8 + nameLength + 1 + 7) & ~7) && buffer[offset + 8 + nameLength] == 0, "a dirent record has the packed layout"); + Require(offset + length <= bytesRead, "a dirent record ends inside the bytes read"); + fileNumbers.insert(fileNumber); + entries.emplace(name, type); + offset += length; } - Require(threw, "get the mount point of shell core did not throw its marker"); - threw = false; - try { - sceUsbStorageClearFakeWriteProtection(); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageClearFakeWriteProtection not implemented"; + Require(entries == std::set>{{"Music", 4}, {"song.txt", 8}}, "the scan lists the root without dot names"); + Require(fileNumbers == std::set{1, 2}, "the scan numbers its entries from 1"); + Require(sceUsbStorageGetdentsRead(fd, buffer.data(), buffer.size(), &bytesRead) == 0 && bytesRead == 0, "the scan ends with zero bytes"); + Require(sceUsbStorageGetdentsClose(fd) == 0, "close the getdents scan"); + Require(ThrowsWith([&] { sceUsbStorageGetdentsClose(fd); }, "sceUsbStorageGetdentsClose not implemented"), "close a closed scan did not throw its marker"); +} + +void TestPassthroughOff(const FakeHost& host) { + SetPassthrough(false); + Inject(host, true, false); + Require(sceUsbStorageInit(nullptr) == 0, "initialize with the passthrough off"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 0, "initialize listed a host volume with the passthrough off"); + Require(sceUsbStorageRegisterCallback(UsbStorage::MountEvent, &Record, &g_mountTag) == 0, "register the mount recorder with the passthrough off"); + const auto seen = RecordedCount(); + Inject(host, true, true); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + Require(RecordedCount() == seen, "a rescan raised a mount event with the passthrough off"); + count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 0, "a rescan listed a host volume with the passthrough off"); + UsbStorageDeviceInfo info{}; + Require(sceUsbStorageGetDeviceInfo(0, &info) == UsbStorage::DeviceNotFound, "device 0 has info with the passthrough off"); + std::uint8_t exists = 1; + Require(sceUsbStorageIsExist(0, nullptr, &exists) == true && exists == 0, "device 0 exists with the passthrough off"); + Require(sceUsbStorageRequestMap(0, nullptr, 0, 0, nullptr, nullptr) == UsbStorage::DeviceNotFound, "device 0 maps with the passthrough off"); + std::error_code error; + Require(!std::filesystem::equivalent(ResolvePath_nid_no_patch("/mnt/usb0/song.txt"), host.firstMount / "song.txt", error), + "/mnt/usb0 reaches a host volume with the passthrough off"); + Require(sceUsbStorageUnregisterCallback(UsbStorage::MountEvent, &Record) == 0, "unregister the mount recorder with the passthrough off"); + Require(sceUsbStorageTerm() == 0, "terminate with the passthrough off"); +} + +void TestDevices(const FakeHost& host) { + Inject(host, true, false); + Require(sceUsbStorageInit(nullptr) == 0, "initialize with a device"); + TestDeviceInfo(host); + TestDeviceMaps(); + TestDeviceGetdents(); + Require(sceUsbStorageTerm() == 0, "terminate with a device"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 0, "terminate released the device list"); + std::error_code error; + Require(!std::filesystem::equivalent(ResolvePath_nid_no_patch("/mnt/usb0/song.txt"), host.firstMount / "song.txt", error), + "terminate removed the mount point"); +} + +void TestHotplug(const FakeHost& host) { + Inject(host, true, false); + Require(sceUsbStorageInit(nullptr) == 0, "initialize for hotplug"); + Require(sceUsbStorageRegisterCallback(UsbStorage::MountEvent, &Record, &g_mountTag) == 0, "register the mount recorder"); + Require(sceUsbStorageRegisterCallback(UsbStorage::UnmountEvent, &Record, &g_unmountTag) == 0, "register the unmount recorder"); + + Inject(host, true, true); + auto recorded = NextEvent(0); + Require(recorded.event.event == 1 && recorded.event.device == 1 && recorded.userData == &g_mountTag, "a mounted device raises a mount event"); + Require(recorded.event.flags == (UsbStorage::ExFatFlag | UsbStorage::ReadOnlyFlag), "the mount event flags a read-only exFAT volume"); + UsbStorageDeviceInfo info{}; + Require(sceUsbStorageGetDeviceInfo(1, &info) == 0 && info.vendorId == 0x090c && info.bcdDevice == 0x1100, "the mounted device is listed"); + Require(info.serialNumber[0] == '\0' && info.serialNumberLength == 0 && std::string_view(info.product) == "Flash Drive", "a device without a serial number reports an empty one"); + + Inject(host, false, true); + recorded = NextEvent(1); + Require(recorded.event.event == 2 && recorded.event.device == 0 && recorded.userData == &g_unmountTag, "a removed device raises an unmount event"); + Require(recorded.event.flags == UsbStorage::FatFlag, "the unmount event flags the removed volume"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 1 && devices[0] == 1, "the remaining device keeps its id"); + + Inject(host, true, true); + recorded = NextEvent(2); + Require(recorded.event.event == 1 && recorded.event.device == 0, "a device mounted again takes the lowest free id"); + Require(sceUsbStorageUnregisterCallback(UsbStorage::MountEvent, &Record) == 0, "unregister the mount recorder"); + Require(sceUsbStorageUnregisterCallback(UsbStorage::UnmountEvent, &Record) == 0, "unregister the unmount recorder"); + Require(sceUsbStorageTerm() == 0, "terminate after hotplug"); + std::lock_guard lock(Events().mutex); + Require(Events().events.size() == 3, "hotplug raised no other event"); +} + +void TestRootChange(const FakeHost& host) { + Inject(host, true, false); + Require(sceUsbStorageInit(nullptr) == 0, "initialize for a root change"); + std::error_code error; + Require(std::filesystem::equivalent(ResolvePath_nid_no_patch("/mnt/usb0/song.txt"), host.firstMount / "song.txt", error), "the volume starts at its first root"); + Require(sceUsbStorageRegisterCallback(UsbStorage::MountEvent, &Record, &g_mountTag) == 0, "register the mount recorder for a root change"); + std::size_t seen = 0; + { + std::lock_guard lock(Events().mutex); + seen = Events().events.size(); } - Require(threw, "clear fake write protection did not throw its marker"); - threw = false; - try { - sceUsbStorageSetFakeMapLockForDebug(); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageSetFakeMapLockForDebug not implemented"; + Inject(host, true, true, true); + Require(NextEvent(seen).event.device == 1, "the scan that moved the volume mounted the second one"); + Require(std::filesystem::equivalent(ResolvePath_nid_no_patch("/mnt/usb0/moved.txt"), host.movedMount / "moved.txt", error), + "the mount point follows a volume whose key stayed while its root changed"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 2 && devices[0] == 0, "the moved volume keeps its id"); + Require(sceUsbStorageUnregisterCallback(UsbStorage::MountEvent, &Record) == 0, "unregister the mount recorder after a root change"); + Require(sceUsbStorageTerm() == 0, "terminate after a root change"); +} + +void TestUnregisterDuringDispatch(const FakeHost& host) { + Inject(host, false, false); + Require(sceUsbStorageInit(nullptr) == 0, "initialize for unregistrations during dispatch"); + Require(sceUsbStorageRegisterCallback(UsbStorage::MountEvent, &RemoveSelf, nullptr) == 0, "register a callback that unregisters itself"); + Require(sceUsbStorageRegisterCallback(UsbStorage::MountEvent, &Record, &g_mountTag) == 0, "register the mount recorder after it"); + Require(sceUsbStorageRegisterCallback(UsbStorage::UnmountEvent, &BlockInside, nullptr) == 0, "register a callback that blocks"); + Require(sceUsbStorageRegisterCallback(UsbStorage::UnmountEvent, &Record, &g_unmountTag) == 0, "register the unmount recorder after it"); + const auto seen = RecordedCount(); + + Inject(host, true, false); + Require(NextEvent(seen).event.event == 1, "the first mount reached the recorder"); + Require(g_selfRemovalCalls == 1 && g_selfRemovalResult == 0, "a callback unregistered itself without deadlocking"); + Inject(host, true, true); + Require(NextEvent(seen + 1).event.device == 1, "the second mount reached the recorder"); + Require(g_selfRemovalCalls == 1, "a callback that unregistered itself was called again"); + + Inject(host, false, true); + auto& blocker = Blocked(); + { + std::unique_lock lock(blocker.mutex); + Require(blocker.changed.wait_for(lock, std::chrono::seconds(10), [&] { return blocker.entered; }), "the blocking callback was not called"); } - Require(threw, "set fake map lock for debug did not throw its marker"); - threw = false; - try { - sceUsbStorageSetFakeWriteProtection(); - } catch (const std::runtime_error& error) { - threw = std::string_view(error.what()) == "sceUsbStorageSetFakeWriteProtection not implemented"; + std::atomic unregistering{false}; + std::atomic returned{false}; + std::atomic result{-1}; + std::thread other([&] { + unregistering = true; + result = sceUsbStorageUnregisterCallback(UsbStorage::UnmountEvent, &BlockInside); + returned = true; + }); + while (!unregistering) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(!returned, "an unregistration returned while its callback was still running"); + { + std::lock_guard lock(blocker.mutex); + blocker.released = true; } - Require(threw, "set fake write protection did not throw its marker"); + blocker.changed.notify_all(); + other.join(); + Require(returned && result == 0, "the unregistration returned after its callback"); + Require(NextEvent(seen + 2).event.device == 0, "the first unmount reached the recorder"); + Inject(host, false, false); + Require(NextEvent(seen + 3).event.device == 1, "the second unmount reached the recorder"); + { + std::lock_guard lock(blocker.mutex); + Require(blocker.calls == 1, "a callback was called after its unregistration returned"); + } + Require(sceUsbStorageUnregisterCallback(UsbStorage::MountEvent, &Record) == 0, "unregister the mount recorder after dispatch"); + Require(sceUsbStorageUnregisterCallback(UsbStorage::UnmountEvent, &Record) == 0, "unregister the unmount recorder after dispatch"); + Require(sceUsbStorageTerm() == 0, "terminate after unregistrations during dispatch"); +} + +void TestSysfsScan(const FakeHost& host) { + WriteMounts(host, FirstLine(host) + SecondLine(host) + MountLine("8_1", "/", host.root / "media" / "efi", "rw", "vfat") + + MountLine("8_49", "/", host.root / "media", "rw", "ext4") + MountLine("8_17", "/Music", host.root / "media" / "bound", "rw", "vfat")); + SetUsbStorageHostRootsForTest_nid_no_patch(Utf8(host.mountInfo).c_str(), Utf8(host.sys).c_str()); + Require(sceUsbStorageInit(nullptr) == 0, "initialize over the fake sysfs"); + std::uint32_t devices[UsbStorage::MaxDevices]{}; + std::int32_t count = -1; + Require(sceUsbStorageGetDeviceList(devices, &count) == 0 && count == 2 && devices[0] == 0 && devices[1] == 1, + "only the whole FAT and exFAT volumes of USB mass-storage devices are listed"); + UsbStorageDeviceInfo info{}; + Require(sceUsbStorageGetDeviceInfo(0, &info) == 0, "get the info of the scanned FAT volume"); + Require(info.vendorId == 0x0781 && info.productId == 0x5581 && info.bcdDevice == 0x0100, "the scan reads the USB ids and release"); + Require(std::string_view(info.vendor) == "SanDisk" && std::string_view(info.product) == "Ultra Fit", "the scan reads the SCSI strings without trailing spaces"); + Require(std::string_view(info.serialNumber) == "4C530001" && info.flags == UsbStorage::FatFlag, "the scan reads the serial number and a writable FAT volume"); + std::error_code error; + const auto capacity = std::filesystem::space(host.firstMount, error).capacity; + Require(!error && info.volumeSize == capacity, "the scanned volume size is the capacity of the mounted file system"); + Require(sceUsbStorageGetDeviceInfo(1, &info) == 0, "get the info of the scanned exFAT volume"); + Require(info.vendorId == 0x090c && std::string_view(info.product) == "Flash Drive" && info.serialNumberLength == 0, "the scan reads a device without a serial number"); + Require(info.flags == (UsbStorage::ExFatFlag | UsbStorage::ReadOnlyFlag), "the scan reads a read-only exFAT mount"); + Require(sceUsbStorageTerm() == 0, "terminate over the fake sysfs"); } } int main() { + const auto host = CreateHost(); TestInitializeAndTerminate(); TestRegisterCallbacks(); TestMapCallbacks(); @@ -238,5 +672,19 @@ int main() { TestMapRequests(); TestGetdents(); TestThrowingExports(); + TestPassthroughOff(host); + SetPassthrough(true); + TestDevices(host); + TestHotplug(host); + TestRootChange(host); + TestUnregisterDuringDispatch(host); + if (CreateSysfs(host)) { + TestSysfsScan(host); + } else { + std::printf("USBSTORAGE: directory symbolic links are unavailable, the sysfs scan is not exercised\n"); + } + SetUsbStorageHostVolumesForTest_nid_no_patch(nullptr, 0); + std::error_code error; + std::filesystem::remove_all(host.root, error); return 0; } diff --git a/core/libs/tests/GuestUsbStorageDialog.cpp b/core/libs/tests/GuestUsbStorageDialog.cpp index 56615792bb..62ecc41378 100644 --- a/core/libs/tests/GuestUsbStorageDialog.cpp +++ b/core/libs/tests/GuestUsbStorageDialog.cpp @@ -45,8 +45,10 @@ int main() { result.result = kErrBusy; Require(sceUsbStorageDialogTerminate() == kErrNotInitialized); + Require(sceUsbStorageDialogOpen(nullptr) == kErrArgNull); Require(sceUsbStorageDialogOpen(¶m) == kErrNotInitialized); Require(sceUsbStorageDialogClose() == kErrNotInitialized); + Require(sceUsbStorageDialogGetResult(nullptr) == kErrArgNull); Require(sceUsbStorageDialogGetResult(&result) == kErrNotInitialized); Require(sceUsbStorageDialogGetStatus() == kStatusNone); Require(sceUsbStorageDialogUpdateStatus() == kStatusNone); @@ -92,6 +94,8 @@ int main() { Require(sceUsbStorageDialogClose() == kErrNotRunning); Require(sceUsbStorageDialogTerminate() == 0); Require(sceUsbStorageDialogTerminate() == kErrNotInitialized); + Require(sceUsbStorageDialogOpen(nullptr) == kErrArgNull); + Require(sceUsbStorageDialogGetResult(nullptr) == kErrArgNull); Require(sceUsbStorageDialogGetStatus() == kStatusNone); Require(sceUsbStorageDialogInitialize() == 0); diff --git a/core/libs/tests/GuestVideodec.cpp b/core/libs/tests/GuestVideodec.cpp index 236336d978..0f8e1c1d18 100644 --- a/core/libs/tests/GuestVideodec.cpp +++ b/core/libs/tests/GuestVideodec.cpp @@ -61,6 +61,7 @@ void testDecode(const std::vector>& units, std::uint32 check(units.size() == PictureHashes.size(), "unexpected access unit count"); std::vector hashes; std::vector timestamps; + std::vector attachments; const auto collect = [&](PictureInfo& picture) { if (!picture.isValid) return; check(picture.framePitch == Pitch && picture.frameHeight == PictureHeight, "unexpected picture geometry"); @@ -69,6 +70,7 @@ void testDecode(const std::vector>& units, std::uint32 check(picture.codec.avc.frameCropTopOffset == cropTop && picture.codec.avc.frameCropBottomOffset == cropBottom, "unexpected crop"); hashes.push_back(hashNv12(session.buffer.data(), Pitch, PictureHeight)); timestamps.push_back(picture.ptsData); + attachments.push_back(picture.attachedData); }; for (std::size_t unit = 0; unit < units.size(); ++unit) { const SceVideodec2::InputData input{sizeof(SceVideodec2::InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; @@ -90,6 +92,7 @@ void testDecode(const std::vector>& units, std::uint32 check(hashes[index] == PictureHashes[index], "picture " + std::to_string(index) + " differs from the reference"); const auto unit = DisplayOrderUnits[index]; check(timestamps[index] == 1000 + unit, "pictures out of display order"); + check(attachments[index] == 0xa0 + unit, "picture attachment out of display order"); } } diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 8537ed2d6c..f22f3285da 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -1,4 +1,5 @@ #include "prx/libSceVideodec2/include/Api.hpp" +#include "prx/libSceVideodec2/include/PictureMetadata.hpp" #include #include #include @@ -101,7 +102,8 @@ void testFailures() { config.maxFrameWidth = Width; check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); check(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "decoder creation failed"); - std::vector buffer(memory.maxFrameBufferSize); + std::vector storage; + const auto buffer = AlignedFrameStorage(storage, memory.maxFrameBufferSize, memory.frameBufferAlignment); FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; OutputInfo output{sizeof(OutputInfo)}; const std::array malformed{0, 0, 0, 8, 0x65}; @@ -111,10 +113,10 @@ void testFailures() { checkThrows([&] { sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output); }); check(sceVideodec2Reset_nid_postfix(handle) == 0, "reset failed"); AvcPictureInfo avc{sizeof(AvcPictureInfo)}; - checkThrows([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(nullptr, &avc, nullptr); }); - checkThrows([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(&output, nullptr, nullptr); }); + check(sceVideodec2GetAvcPictureInfo_nid_postfix(nullptr, &avc, nullptr) == static_cast(0x811D0102u), "null AVC output error"); + check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, nullptr, nullptr) == static_cast(0x811D0102u), "null AVC destination error"); output.frameBuffer = buffer.data(); - check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == 0 && !avc.isValid, "unknown frame buffer reported a picture"); + check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == static_cast(0x811D010Fu) && !avc.isValid, "missing picture count must precede destination size"); frame.frameBufferSize = 1; checkThrows([&] { for (const auto& unit : accessUnits(false)) { @@ -138,7 +140,8 @@ void testClip(std::uint32_t codec, std::uint32_t profile, const std::vector buffer(memory.maxFrameBufferSize); + std::vector storage; + const auto buffer = AlignedFrameStorage(storage, memory.maxFrameBufferSize, memory.frameBufferAlignment); std::size_t pictures = 0; const auto take = [&](const OutputInfo& output, const FrameBuffer& frame) { check(output.isValid == frame.isAccepted, "frame buffer acceptance disagrees with the output"); @@ -146,7 +149,7 @@ void testClip(std::uint32_t codec, std::uint32_t profile, const std::vector(0x811D0101u) && !info.isValid, "partial AVC destination size was accepted"); Vp9PictureInfo vp9{sizeof(Vp9PictureInfo)}; if (codec == CodecVp9) { check(sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &vp9) == 0 && vp9.isValid, "VP9 picture metadata missing"); @@ -190,9 +193,11 @@ void testVp9PictureMetadata() { std::uint64_t handle = 0; check(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "VP9 create failed"); const auto units = ivfFrames(); - std::vector firstBuffer(memory.maxFrameBufferSize), secondBuffer(memory.maxFrameBufferSize); + std::vector firstStorage, secondStorage; + const auto firstBuffer = AlignedFrameStorage(firstStorage, memory.maxFrameBufferSize, memory.frameBufferAlignment); + const auto secondBuffer = AlignedFrameStorage(secondStorage, memory.maxFrameBufferSize, memory.frameBufferAlignment); OutputInfo firstOutput{sizeof(OutputInfo)}, secondOutput{sizeof(OutputInfo)}; - const auto decode = [&](const std::vector& bytes, std::vector& buffer, OutputInfo& output, std::uint64_t tag) { + const auto decode = [&](const std::vector& bytes, std::span buffer, OutputInfo& output, std::uint64_t tag) { const InputData input{sizeof(InputData), bytes.data(), bytes.size(), tag, tag + 1, tag + 2}; FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "VP9 metadata decode failed"); @@ -201,14 +206,28 @@ void testVp9PictureMetadata() { struct Guarded { Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; std::uint64_t guard = 0x123456789abcdef0ull; } guarded; check(sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &guarded.info) == 0, "VP9 metadata query failed"); check(guarded.guard == 0x123456789abcdef0ull && guarded.info.thisSize == sizeof(Vp9PictureInfo), "VP9 info overwrote caller storage"); - return guarded.info; + struct Snapshot { Vp9PictureInfo info{}; std::uint64_t guard = 0xfedcba9876543210ull; } snapshot; + Videodec2Vp9PictureInfo_nid_no_patch(handle, output.frameBuffer, &snapshot.info); + check(snapshot.info.thisSize == sizeof(Vp9PictureInfo) && snapshot.guard == 0xfedcba9876543210ull, "internal VP9 snapshot size or boundary differs"); + const auto& same = snapshot.info; + const auto& expected = guarded.info; + check(same.isValid && same.ptsData == expected.ptsData && same.dtsData == expected.dtsData && same.attachedData == expected.attachedData && same.profile == expected.profile && same.level == expected.level && + same.keyFrameFlag == expected.keyFrameFlag && same.intraOnly == expected.intraOnly && same.bitDepth == expected.bitDepth && same.colorRange == expected.colorRange && same.colorSpace == expected.colorSpace && same.numFrames == expected.numFrames && + same.frameWidth == expected.frameWidth && same.frameHeight == expected.frameHeight && same.renderWidth == expected.renderWidth && same.renderHeight == expected.renderHeight, "internal VP9 snapshot differs from decoded picture metadata"); + return snapshot.info; + }; + const auto observeUntracked = [&](const OutputInfo& output) { + struct Guarded { Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; std::uint64_t guard = 0x123456789abcdef0ull; } guarded; + try { sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &guarded.info); } + catch (const std::runtime_error&) { check(!guarded.info.isValid, "unsupported untracked output retained validity"); } + check(guarded.guard == 0x123456789abcdef0ull, "untracked metadata crossed its output boundary"); }; Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; - checkThrows([&] { sceVideodec2GetVp9PictureInfo_nid_postfix(nullptr, &info); }); - checkThrows([&] { sceVideodec2GetVp9PictureInfo_nid_postfix(&firstOutput, nullptr); }); + check(sceVideodec2GetVp9PictureInfo_nid_postfix(nullptr, &info) == static_cast(0x811D0102u), "null VP9 output error"); + check(sceVideodec2GetVp9PictureInfo_nid_postfix(&firstOutput, nullptr) == static_cast(0x811D0102u), "null VP9 destination error"); info.thisSize = sizeof(Vp9PictureInfo) - 1; - checkThrows([&] { sceVideodec2GetVp9PictureInfo_nid_postfix(&firstOutput, &info); }); - check(!query(firstOutput).isValid, "VP9 unknown buffer reported a picture"); + check(sceVideodec2GetVp9PictureInfo_nid_postfix(&firstOutput, &info) == static_cast(0x811D0107u), "buffer error must precede VP9 destination size"); + std::vector superframe; for (std::size_t index = 0; index < 2; ++index) superframe.insert(superframe.end(), units[index].begin(), units[index].end()); superframe.push_back(0xc9); @@ -228,7 +247,8 @@ void testVp9PictureMetadata() { check(secondInfo.numFrames == 2 && secondInfo.keyFrameFlag == 0 && secondInfo.ptsData == 9000, "VP9 second superframe metadata differs"); firstOutput.frameWidth = 1; check(query(firstOutput).frameWidth == Width && query(firstOutput).keyFrameFlag == 1, "VP9 retained picture metadata changed"); - check(sceVideodec2Reset_nid_postfix(handle) == 0 && !query(firstOutput).isValid, "VP9 reset retained old picture ownership"); + check(sceVideodec2Reset_nid_postfix(handle) == 0, "VP9 reset failed"); + observeUntracked(firstOutput); auto hidden = units[0]; hidden[0] &= static_cast(~2u); decode(hidden, firstBuffer, firstOutput, 9100); @@ -257,7 +277,8 @@ void testVp9PictureMetadata() { info = query(firstOutput); check(info.isValid && info.bitDepth == 8 && info.colorRange == 1 && info.frameWidth == Width && info.frameHeight == Height && info.renderWidth == Width * 2 && info.renderHeight == Height * 2, "VP9 render size or color range metadata differs"); check(hashNv12(firstBuffer.data(), firstOutput.framePitch, firstOutput.frameHeight) == Vp9Hashes[0], "VP9 render metadata changed coded pixels"); - check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0 && !query(firstOutput).isValid, "VP9 delete retained picture ownership"); + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "VP9 delete failed"); + observeUntracked(firstOutput); } void testTenBit() { @@ -305,7 +326,8 @@ void testDecode(bool lengthPrefixed, bool separateParameters = false) { check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); std::uint64_t handle = 0; check(sceVideodec2CreateDecoder_nid_postfix(&config, &memory, &handle) == 0, "decoder creation failed"); - std::vector buffer(memory.maxFrameBufferSize); + std::vector storage; + const auto buffer = AlignedFrameStorage(storage, memory.maxFrameBufferSize, memory.frameBufferAlignment); std::size_t pictures = 0; const auto take = [&](const OutputInfo& output, const FrameBuffer& frame) { check(output.isValid == frame.isAccepted, "frame buffer acceptance disagrees with the output"); @@ -313,13 +335,9 @@ void testDecode(bool lengthPrefixed, bool separateParameters = false) { check(pictures < PictureHashes.size(), "more pictures than access units"); check(!output.isErrorFrame && output.frameWidth == Width && output.frameHeight == CodedHeight && output.framePitch == 256 && output.frameBuffer == buffer.data(), "unexpected picture geometry"); AvcPictureInfo info{sizeof(AvcPictureInfo)}; - check(sceVideodec2GetPictureInfo_nid_postfix(&output, &info, nullptr) == 0 && info.isValid, "picture info missing"); - const auto unit = DisplayOrderUnits[pictures]; - check(info.ptsData == 1000 + unit && info.dtsData == unit && info.attachedData == 0xa0 + unit, "picture timestamps out of display order"); - check(info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.profileIdc == 100, "picture flags mismatch"); + check(sceVideodec2GetPictureInfo_nid_postfix(&output, &info, nullptr) == static_cast(0x811D0101u) && !info.isValid, "partial AVC destination size was accepted"); AvcPictureInfo avc{sizeof(AvcPictureInfo)}; - check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == 0 && avc.isValid, "avc picture info missing"); - check(avc.ptsData == 1000 + unit && avc.dtsData == unit && avc.attachedData == 0xa0 + unit && avc.idrPictureFlag == (pictures == 0 ? 1 : 0) && avc.profileIdc == 100 && avc.picWidthInLumaSamples == Width && avc.picHeightInLumaSamples == CodedHeight, "avc picture info mismatch"); + check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == static_cast(0x811D0101u) && !avc.isValid, "partial typed AVC destination size was accepted"); check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); ++pictures; return true; @@ -472,25 +490,31 @@ std::uint64_t createDecoder(std::uint32_t codec, std::uint32_t profile) { return handle; } -AvcPictureInfo query(std::vector& buffer) { +Vp9PictureInfo query(std::span buffer) { OutputInfo output{sizeof(OutputInfo)}; output.frameBuffer = buffer.data(); - AvcPictureInfo info{sizeof(AvcPictureInfo)}; - check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &info, nullptr) == 0, "picture info query failed"); + output.frameBufferSize = buffer.size(); + output.pictureCount = 1; + Vp9PictureInfo info{sizeof(Vp9PictureInfo)}; + try { + check(sceVideodec2GetVp9PictureInfo_nid_postfix(&output, &info) == 0, "picture info query failed"); + } catch (const std::runtime_error&) { + check(!info.isValid, "unimplemented metadata retained validity"); + } return info; } -bool owned(std::vector& buffer, std::uint64_t pts) { +bool owned(std::span buffer, std::uint64_t pts) { const auto info = query(buffer); return info.isValid && info.ptsData == pts; } -std::uint64_t publishAvc(std::uint64_t handle, std::uint64_t base, std::vector& buffer) { - const auto units = accessUnits(false); +std::uint64_t publishVp9(std::uint64_t handle, std::uint64_t base, std::span buffer) { + const auto units = ivfFrames(); std::size_t pictures = 0; const auto take = [&](const OutputInfo& output) { if (!output.isValid) return false; - check(owned(buffer, base + DisplayOrderUnits[pictures]), "picture info does not follow the latest publication"); + check(owned(buffer, base + pictures), "picture info does not follow the latest publication"); ++pictures; return true; }; @@ -507,11 +531,11 @@ std::uint64_t publishAvc(std::uint64_t handle, std::uint64_t base, std::vector>& units, std::vector& buffer) { +void publishClip(std::uint64_t handle, const std::vector>& units, std::span buffer) { for (std::size_t unit = 0; unit < units.size(); ++unit) { const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), unit, unit, 0}; FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; @@ -526,10 +550,11 @@ void publishClip(std::uint64_t handle, const std::vector(Width), static_cast(Height), 4, 1}; +std::uint64_t frameBufferBytes(std::uint32_t* alignment = nullptr, std::uint32_t codec = 1, std::uint32_t profile = 100) { + const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 1, codec, profile, 0, static_cast(Width), static_cast(Height), 4, 1}; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); + if (alignment) *alignment = memory.frameBufferAlignment; return memory.maxFrameBufferSize; } @@ -680,7 +705,7 @@ void testDecodeAfterFlush() { std::size_t pictures = 0; const auto take = [&](const OutputInfo& output) { if (!output.isValid) return false; - check(pictures < PictureHashes.size() && owned(buffer, base + DisplayOrderUnits[pictures]), "a picture decoded after a flush is out of display order"); + check(pictures < PictureHashes.size(), "too many pictures decoded after a flush"); check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " decoded after a flush differs from the reference"); ++pictures; return true; @@ -704,31 +729,35 @@ void testDecodeAfterFlush() { } void testPictureOwnership() { - std::vector shared(frameBufferBytes()), other(frameBufferBytes()); + std::uint32_t alignment = 0; + const auto bytes = frameBufferBytes(&alignment, CodecVp9, 0); + std::vector sharedStorage, otherStorage; + const auto shared = AlignedFrameStorage(sharedStorage, bytes, alignment); + const auto other = AlignedFrameStorage(otherStorage, bytes, alignment); { - const auto older = createDecoder(1, 100); - const auto newer = createDecoder(1, 100); - publishAvc(older, 1000, shared); - const auto latest = publishAvc(newer, 5000, shared); + const auto older = createDecoder(CodecVp9, 0); + const auto newer = createDecoder(CodecVp9, 0); + publishVp9(older, 1000, shared); + const auto latest = publishVp9(newer, 5000, shared); check(owned(shared, latest), "an older decoder answered for a buffer a newer decoder wrote last"); check(sceVideodec2Reset_nid_postfix(newer) == 0, "reset failed"); check(!query(shared).isValid, "a stale picture survived the reset of the decoder that last wrote the buffer"); check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0 && sceVideodec2DeleteDecoder_nid_postfix(newer) == 0, "delete failed"); } { - const auto older = createDecoder(1, 100); - const auto newer = createDecoder(1, 100); - publishAvc(older, 1000, shared); - publishAvc(newer, 5000, shared); + const auto older = createDecoder(CodecVp9, 0); + const auto newer = createDecoder(CodecVp9, 0); + publishVp9(older, 1000, shared); + publishVp9(newer, 5000, shared); check(sceVideodec2DeleteDecoder_nid_postfix(newer) == 0, "delete failed"); check(!query(shared).isValid, "a stale picture survived the deletion of the decoder that last wrote the buffer"); check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0, "delete failed"); } { - const auto older = createDecoder(1, 100); - const auto newer = createDecoder(1, 100); - publishAvc(older, 1000, shared); - const auto latest = publishAvc(newer, 5000, shared); + const auto older = createDecoder(CodecVp9, 0); + const auto newer = createDecoder(CodecVp9, 0); + publishVp9(older, 1000, shared); + const auto latest = publishVp9(newer, 5000, shared); check(sceVideodec2Reset_nid_postfix(older) == 0, "reset failed"); check(owned(shared, latest), "resetting a decoder that no longer owns the buffer dropped its picture"); check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0, "delete failed"); @@ -737,43 +766,46 @@ void testPictureOwnership() { check(!query(shared).isValid, "a deleted decoder still owns its picture"); } { - const auto older = createDecoder(1, 100); - const auto newer = createDecoder(1, 100); - publishAvc(newer, 5000, shared); - const auto latest = publishAvc(older, 1000, shared); - const auto third = createDecoder(1, 100); - const auto separate = publishAvc(third, 9000, other); + const auto older = createDecoder(CodecVp9, 0); + const auto newer = createDecoder(CodecVp9, 0); + publishVp9(newer, 5000, shared); + const auto latest = publishVp9(older, 1000, shared); + const auto third = createDecoder(CodecVp9, 0); + const auto separate = publishVp9(third, 9000, other); check(owned(shared, latest) && owned(other, separate), "distinct buffers are not independent"); check(sceVideodec2Reset_nid_postfix(older) == 0, "reset failed"); check(!query(shared).isValid && owned(other, separate), "resetting one buffer's owner changed another buffer"); check(sceVideodec2DeleteDecoder_nid_postfix(older) == 0 && sceVideodec2DeleteDecoder_nid_postfix(newer) == 0 && sceVideodec2DeleteDecoder_nid_postfix(third) == 0, "delete failed"); } - for (const auto codec : {CodecHevc, CodecVp9}) { - const auto units = codec == CodecHevc ? hevcUnits(HevcStream.data(), HevcStream.size()) : ivfFrames(); - const auto avc = createDecoder(1, 100); - const auto foreign = createDecoder(codec, codec == CodecHevc ? 1 : 0); - publishAvc(avc, 1000, shared); + for (const auto codec : {CodecHevc, 1u}) { + const auto units = codec == CodecHevc ? hevcUnits(HevcStream.data(), HevcStream.size()) : accessUnits(false); + const auto vp9 = createDecoder(CodecVp9, 0); + const auto foreign = createDecoder(codec, codec == CodecHevc ? 1 : 100); + publishVp9(vp9, 1000, shared); publishClip(foreign, units, shared); - checkThrows([&] { query(shared); }); - check(sceVideodec2DeleteDecoder_nid_postfix(avc) == 0, "delete failed"); - const auto newer = createDecoder(1, 100); - const auto latest = publishAvc(newer, 5000, shared); - check(owned(shared, latest), "a decoder of another codec answered for an AVC picture"); + + check(sceVideodec2DeleteDecoder_nid_postfix(vp9) == 0, "delete failed"); + const auto newer = createDecoder(CodecVp9, 0); + const auto latest = publishVp9(newer, 5000, shared); + check(owned(shared, latest), "a decoder of another codec answered for a VP9 picture"); check(sceVideodec2DeleteDecoder_nid_postfix(foreign) == 0 && sceVideodec2DeleteDecoder_nid_postfix(newer) == 0, "delete failed"); } } void testConcurrentOwnership() { - std::vector shared(frameBufferBytes()); - const auto keeper = createDecoder(1, 100); - const auto kept = publishAvc(keeper, 1000, shared); + std::uint32_t alignment = 0; + const auto bytes = frameBufferBytes(&alignment, CodecVp9, 0); + std::vector sharedStorage; + const auto shared = AlignedFrameStorage(sharedStorage, bytes, alignment); + const auto keeper = createDecoder(CodecVp9, 0); + const auto kept = publishVp9(keeper, 1000, shared); std::atomic done = false; std::string failure; std::thread reader([&] { try { while (!done.load()) { const auto info = query(shared); - check(!info.isValid || info.ptsData == kept || (info.ptsData >= 5000 && info.ptsData < 5000 + PictureHashes.size()), "a query returned metadata no decoder published"); + check(!info.isValid || info.ptsData == kept || (info.ptsData >= 5000 && info.ptsData < 5000 + Vp9Hashes.size()), "a query returned metadata no decoder published"); } } catch (const std::exception& error) { failure = error.what(); @@ -781,8 +813,8 @@ void testConcurrentOwnership() { }); try { for (int round = 0; round < 8; ++round) { - const auto writer = createDecoder(1, 100); - publishAvc(writer, 5000, shared); + const auto writer = createDecoder(CodecVp9, 0); + publishVp9(writer, 5000, shared); if (round % 2 == 0) check(sceVideodec2Reset_nid_postfix(writer) == 0, "reset failed"); check(sceVideodec2DeleteDecoder_nid_postfix(writer) == 0, "delete failed"); check(!query(shared).isValid, "a removed writer left its picture behind"); diff --git a/core/libs/tests/GuestVideodec2Unimplemented.cpp b/core/libs/tests/GuestVideodec2Unimplemented.cpp index 25eb1d8a77..090fdf8327 100644 --- a/core/libs/tests/GuestVideodec2Unimplemented.cpp +++ b/core/libs/tests/GuestVideodec2Unimplemented.cpp @@ -31,6 +31,5 @@ int main() { passed = ReportsNotImplemented("sceVideodec2CreateDecoderBid", [] { return sceVideodec2CreateDecoderBid(); }) && passed; passed = ReportsNotImplemented("sceVideodec2CreateHevcDecoder", [] { return sceVideodec2CreateHevcDecoder(); }) && passed; passed = ReportsNotImplemented("sceVideodec2QueryHevcDecoderMemoryInfo", [] { return sceVideodec2QueryHevcDecoderMemoryInfo(); }) && passed; - passed = ReportsNotImplemented("sceVideodec2GetHevcPictureInfo", [] { return sceVideodec2GetHevcPictureInfo(); }) && passed; return passed ? 0 : 1; } diff --git a/core/libs/tests/GuestVrServiceDialog.cpp b/core/libs/tests/GuestVrServiceDialog.cpp index 5b7433ce53..43664c8263 100644 --- a/core/libs/tests/GuestVrServiceDialog.cpp +++ b/core/libs/tests/GuestVrServiceDialog.cpp @@ -51,6 +51,8 @@ int main() { Require(sceVrServiceDialogClose() == 0); Require(sceVrServiceDialogTerminate() == 0); Require(sceVrServiceDialogTerminate() == kErrNotInitialized); + Require(sceVrServiceDialogOpen(nullptr) == kErrNotInitialized); + Require(sceVrServiceDialogGetResult(nullptr) == kErrNotInitialized); Require(sceVrServiceDialogGetStatus() == kStatusNone); Require(sceVrServiceDialogUpdateStatus() == kStatusNone); @@ -62,4 +64,9 @@ int main() { Require(sceVrServiceDialogGetResult(result) == 0); Require(result[0] == kResultUserCanceled); Require(sceVrServiceDialogTerminate() == 0); + Require(sceVrServiceDialogTerminate() == kErrNotInitialized); + Require(sceVrServiceDialogOpen(nullptr) == kErrNotInitialized); + Require(sceVrServiceDialogGetResult(nullptr) == kErrNotInitialized); + Require(sceVrServiceDialogGetStatus() == kStatusNone); + Require(sceVrServiceDialogUpdateStatus() == kStatusNone); } diff --git a/core/libs/tests/GuestVrSetupDialog.cpp b/core/libs/tests/GuestVrSetupDialog.cpp index 3297365257..d08cedc1dd 100644 --- a/core/libs/tests/GuestVrSetupDialog.cpp +++ b/core/libs/tests/GuestVrSetupDialog.cpp @@ -25,6 +25,7 @@ int main() { std::int32_t result[2] = {-1, -1}; Require(sceVrSetupDialogTerminate() == kErrNotInitialized); + Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); Require(sceVrSetupDialogOpen(¶m) == kErrNotInitialized); Require(sceVrSetupDialogClose() == kErrNotInitialized); Require(sceVrSetupDialogGetResult(nullptr) == kErrArgNull); @@ -64,6 +65,8 @@ int main() { Require(sceVrSetupDialogClose() == 0); Require(sceVrSetupDialogTerminate() == 0); Require(sceVrSetupDialogTerminate() == kErrNotInitialized); + Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); + Require(sceVrSetupDialogGetResult(nullptr) == kErrArgNull); Require(sceVrSetupDialogGetStatus() == kStatusNone); Require(sceVrSetupDialogUpdateStatus() == kStatusNone); diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index 01fdca210e..53ce0d3955 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -31,7 +31,9 @@ int main() { Require(sceWebBrowserDialogResetCookie(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogOpen(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(result.result == 0x5a5a5a5a); @@ -44,6 +46,8 @@ int main() { Require(sceWebBrowserDialogTerminate() == 0); Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogOpen(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogInitialize() == 0); @@ -59,6 +63,8 @@ int main() { Require(sceWebBrowserDialogClose() == static_cast(0x80B8000Bu)); Require(sceWebBrowserDialogTerminate() == 0); Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogOpen(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogOpenForPredeterminedContent(param, &allowList) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index 5540d7ee60..94f1d6c431 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -2,6 +2,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/GuestTime.hpp" +#include #include #include #include @@ -219,6 +220,13 @@ static void CheckSwprintf() { Require(swprintf_nid_postfix(wide, 16, u"%5ls|%-3c|%lc", u"wide", 'z', 0x20ac) == 11, "swprintf padded length"); Require(wide == std::u16string(u" wide|z |€"), "swprintf padded output"); Require(swprintf_nid_postfix(wide, 16, u"%q") < 0 && wide[0] == 0, "swprintf invalid conversion"); + for (const char16_t* format : {u"%99999999999d", u"%.99999999999d", u"%4294967296s", u"%.4294967296s", u"%2147483648ls", u"%.2147483648ls"}) { + wide[0] = u'x'; + Require(swprintf_nid_postfix(wide, 16, format, 7, u"a") < 0 && wide[0] == 0, "swprintf width or precision beyond int"); + } + wide[0] = u'x'; + Require(swprintf_nid_postfix(wide, 16, u"%*d", INT_MIN, 7) < 0 && wide[0] == 0, "swprintf INT_MIN width"); + Require(swprintf_nid_postfix(wide, 16, u"%*d", -5, 7) == 5 && wide == std::u16string(u"7 "), "swprintf negative width"); } static bool WcsftimeThrows(char16_t* buffer, const char16_t* format, const GuestTm* time) { diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 9f8a857137..15ad0686d0 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -107,6 +107,11 @@ int main() { Require(sceZlibWaitForDone(&completed, &timeout) == static_cast(0x81120027)); const auto waited = std::chrono::steady_clock::now() - start; Require(waited >= std::chrono::microseconds(20000) && waited < std::chrono::seconds(1)); + timeout = 1000; + const auto shortStart = std::chrono::steady_clock::now(); + for (int round = 0; round < 20; ++round) Require(sceZlibWaitForDone(&completed, &timeout) == static_cast(0x81120027)); + const auto shortWaits = std::chrono::steady_clock::now() - shortStart; + Require(shortWaits >= std::chrono::milliseconds(20) && shortWaits < std::chrono::milliseconds(200)); std::thread waiter([&] { Require(sceZlibWaitForDone(&completed, nullptr) == 0); }); std::this_thread::sleep_for(std::chrono::milliseconds(10)); Require(sceZlibInflate(compressed.data(), compressed.size(), output, 8192, &id) == 0); diff --git a/core/libs/tests/HevcFixture.hpp b/core/libs/tests/HevcFixture.hpp index 7bacee3a02..39e89e0ef1 100644 --- a/core/libs/tests/HevcFixture.hpp +++ b/core/libs/tests/HevcFixture.hpp @@ -3,10 +3,20 @@ #include #include #include +#include +#include #include namespace HevcFixture { +inline std::span AlignedFrameStorage(std::vector& storage, std::size_t bytes, std::size_t alignment) { + storage.resize(bytes + alignment); + void* pointer = storage.data(); + auto space = storage.size(); + if (!std::align(alignment, bytes, pointer, space)) throw std::bad_alloc(); + return {static_cast(pointer), bytes}; +} + inline constexpr std::array HevcStream{ 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x1e, diff --git a/core/libs/tests/HevcMetadataFixture.hpp b/core/libs/tests/HevcMetadataFixture.hpp new file mode 100644 index 0000000000..8db940da26 --- /dev/null +++ b/core/libs/tests/HevcMetadataFixture.hpp @@ -0,0 +1,45 @@ +#ifndef CORE_LIBS_TESTS_HEVCMETADATAFIXTURE_HPP +#define CORE_LIBS_TESTS_HEVCMETADATAFIXTURE_HPP + +#include +#include + +namespace HevcMetadataFixture { + +inline constexpr std::array Rich{ + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, + 0xff, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9f, 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xba, + 0x02, 0x40, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9f, + 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xa0, 0x20, 0x83, 0x16, 0x5b, 0xab, 0x93, 0x2b, 0xc0, + 0x93, 0x84, 0x88, 0x04, 0xc8, 0x40, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x0c, 0x04, + 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xc0, 0x89, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, + 0x78, 0xf7, 0x04, 0x03, 0xff, 0xdb, 0xa3, 0xff, 0xed, 0x27, 0xd2, 0xf6, 0xc3, 0x94, 0x40, 0x83, + 0xc0, 0x00, 0x78, +}; + +inline constexpr std::array Windowed{ + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, + 0xff, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9f, 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xba, + 0x02, 0x40, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9f, + 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xa0, 0x20, 0x83, 0x16, 0x5b, 0xab, 0x93, 0x2b, 0xc0, + 0x93, 0x84, 0x88, 0x04, 0xc8, 0x41, 0x6c, 0xae, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03, + 0x00, 0x30, 0x10, 0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc1, 0x73, 0xc0, 0x89, 0x00, 0x00, 0x01, + 0x28, 0x01, 0xaf, 0x78, 0xf7, 0x04, 0x03, 0xff, 0xdb, 0xa3, 0xff, 0xed, 0x27, 0xd2, 0xf6, 0xc3, + 0x94, 0x40, 0x83, 0xc0, 0x00, 0x78, +}; + +inline constexpr std::array Interlaced{ + 0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10, 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, + 0xff, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x4f, 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xba, + 0x02, 0x40, 0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x4f, + 0xa8, 0x00, 0x00, 0x03, 0x00, 0x00, 0x1e, 0xa0, 0x20, 0x83, 0x16, 0x5b, 0xab, 0x93, 0x2b, 0xc0, + 0x5a, 0x1a, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x30, 0x10, 0x00, 0x00, 0x00, + 0x01, 0x44, 0x01, 0xc1, 0x73, 0xc0, 0x89, 0x00, 0x00, 0x01, 0x4e, 0x01, 0x81, 0x01, 0x0f, 0x80, + 0x00, 0x00, 0x01, 0x4e, 0x01, 0x01, 0x01, 0x11, 0x80, 0x00, 0x00, 0x01, 0x28, 0x01, 0xaf, 0x78, + 0xf7, 0x04, 0x03, 0xff, 0xec, 0x02, 0x34, 0x1e, 0x93, 0x2c, 0x7e, 0xf5, 0xd4, 0x40, 0x83, 0xc0, + 0x00, 0x78, +}; + +} // namespace HevcMetadataFixture + +#endif diff --git a/core/libs/tests/ProcessParametersFixture.hpp b/core/libs/tests/ProcessParametersFixture.hpp index 794ba40ac9..2bd89f1fc7 100644 --- a/core/libs/tests/ProcessParametersFixture.hpp +++ b/core/libs/tests/ProcessParametersFixture.hpp @@ -13,6 +13,9 @@ #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include #include +#elif defined(__APPLE__) +#include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include #endif namespace ProcessParametersFixture { @@ -20,6 +23,8 @@ namespace { #ifdef _WIN32 [[gnu::section(".proctst"), gnu::used]] alignas(4) std::uint32_t metadata[2]{}; +#elif defined(__APPLE__) +[[gnu::section("__APS5DATA,__meta"), gnu::used]] alignas(8) std::array metadata{}; #endif bool Install(std::byte* process, std::size_t size) { @@ -80,6 +85,24 @@ bool Install(std::byte* process, std::size_t size) { return 1; }, &request); return request.installed; +#elif defined(__APPLE__) + const std::uint64_t version = 2; + const std::uint64_t loadBias = 0; + const std::uint64_t count = 2; + std::memcpy(metadata.data(), &version, sizeof(version)); + std::memcpy(metadata.data() + 0x08, &loadBias, sizeof(loadBias)); + std::memcpy(metadata.data() + 0x58, &count, sizeof(count)); + Elf64_Phdr headers[2]{}; + headers[0].p_type = PT_LOAD; + headers[1].p_type = 0x61000001; + for (auto& header : headers) { + header.p_vaddr = reinterpret_cast(process); + header.p_paddr = header.p_vaddr; + header.p_filesz = size; + header.p_memsz = size; + } + std::memcpy(metadata.data() + 0x60, headers, sizeof(headers)); + return true; #else return false; #endif diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index b3eecc2a92..85cb6a00a4 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -676,6 +676,48 @@ void CheckSharedRunIsOneView() { VirtualFree(other, 0, MEM_RELEASE); CloseHandle(section); } + +void CheckCommitResetsEveryPageOfARunView() { + auto& mappings = Mappings::Get(); + constexpr std::size_t Page = 16384; + constexpr std::size_t Span = 4 * Page; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Span, nullptr); + auto* memory = static_cast(mappings.Reserve(nullptr, Span)); + Require(section && memory, "cannot allocate recommitted run test memory"); + mappings.ForgetSection(section); + const auto address = reinterpret_cast(memory); + const auto collect = [&](std::size_t page, std::size_t count, Collection collection) { + std::array pages{}; + std::size_t available = pages.size(); + Require(mappings.Collect(address + page * Page, count * Page, pages.data(), &available, true, collection), "recommitted run collection failed"); + std::set result; + for (std::size_t i = 0; i < available; ++i) result.insert((reinterpret_cast(pages[i]) - address) / Small); + return result; + }; + mappings.Map(memory, Span, section, 0, PAGE_READWRITE); + Require(MappedViews(memory, Span) == 1, "the recommitted run was not mapped as one view"); + Require(collect(0, 4, Collection::All) == Pages(0, Span / Small), "the run's first collection did not report its fresh pages"); + MEMORY_BASIC_INFORMATION armed{}; + Require(VirtualQuery(memory, &armed, sizeof(armed)) == sizeof(armed) && armed.Protect == PAGE_READONLY && armed.RegionSize == Span, "collection did not arm every page of the run alike"); + mappings.Commit(memory, Span, PAGE_READWRITE, Page, false); + Require(ProtectionAt(memory + 3 * Page) == PAGE_READWRITE, "Commit did not reprotect the whole run"); + memory[Page + 8] = 0x5a; + Require(collect(1, 1, Collection::Armed) == Pages(4, 8), "a store into the second page of a run that Commit reprotected was not collected"); + Require(ProtectionAt(memory + Page) == PAGE_READONLY, "collection did not arm the second page of the recommitted run again"); + const auto before = faults.load(); + memory[Page + Small + 8] = 0x5b; + Require(faults == before + 1, "a store into the rearmed second page of the run did not fault"); + Require(collect(1, 1, Collection::Armed) == Pages(5, 6), "a store into the rearmed second page of the run was not collected once"); + mappings.Commit(memory, Span, PAGE_READONLY, Page, false); + for (std::size_t page = 0; page < 4; ++page) { + std::uint32_t protection = 0; + Require(mappings.Protection(address + page * Page, &protection) && protection == PAGE_READONLY, ("Commit left page " + std::to_string(page) + " of a run with its old guest protection").c_str()); + } + mappings.Reset(memory, Span); + VirtualFree(memory, 0, MEM_RELEASE); + mappings.ForgetSection(section); + CloseHandle(section); +} } int main(int argc, char** argv) { @@ -706,6 +748,7 @@ int main(int argc, char** argv) { CheckPrivateCommitCarvesPlaceholders(); CheckPrivateCommitEdgeLayouts(); CheckPrivateCommitTails(); + CheckCommitResetsEveryPageOfARunView(); std::cout << "Windows deferred write-watch tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 251f62e824..2267be074f 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -696,6 +696,9 @@ if(BUILD_TESTING) add_test(NAME macos_tls_overlap COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_tls_overlap.py $) set_tests_properties(macos_tls_overlap PROPERTIES TIMEOUT 60) + add_test(NAME macos_tls_exports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_tls_exports.py $) + set_tests_properties(macos_tls_exports PROPERTIES TIMEOUT 30) add_test(NAME macos_gs_traps COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_gs_traps.py $) set_tests_properties(macos_gs_traps PROPERTIES TIMEOUT 60) @@ -975,6 +978,9 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING AND Python3_Interpreter_FOUND) + add_test(NAME macos_guest_weak_exports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_guest_weak_exports.py $) + set_tests_properties(macos_guest_weak_exports PROPERTIES TIMEOUT 30 LABELS relinker) add_test(NAME macos_executable_exports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_executable_exports.py $) set_tests_properties(macos_executable_exports PROPERTIES TIMEOUT 30 LABELS relinker) diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index 7f8032b78a..3d306c9a34 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -97,11 +97,12 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma if (symbol.Section == 0 || symbol.Section == Relinker::AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; if (symbol.Section >= 0xff00) throw Domain::RelinkerException("Unsupported guest export section: " + symbol.Name); const auto name = "_" + DynamicName(guest, index); + const auto flags = (symbol.Info >> 4) == 2 ? ExportWeakDefinition : std::uint64_t{0}; if ((symbol.Info & 15) == 6) { - input.TlsExports.push_back({name, symbol.Value}); + input.TlsExports.push_back({name, symbol.Value, flags}); continue; } - input.Exports.push_back({name, image.GetRva(symbol.Value, std::max(symbol.Size, 1))}); + input.Exports.push_back({name, image.GetRva(symbol.Value, std::max(symbol.Size, 1)), flags}); } for (const auto& dependency : dependencies) input.Dylibs.push_back(MachODependency(dependency, "@loader_path")); diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index e0e2648f1f..d539683a1e 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -549,7 +549,6 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { for (std::size_t index = 0; index < input.TlsExports.size(); ++index) { const auto& variable = input.TlsExports[index]; if (!hasTls) throw Domain::RelinkerException("Guest TLS export has no TLS block: " + variable.Symbol); - if (variable.Offset >= tls.Tls->MemorySize) throw Domain::RelinkerException("Guest TLS export is outside the TLS block: " + variable.Symbol); const auto at = tlsExports + index * 16; Io::WriteU64(meta, at, imageBase + descriptor); Io::WriteU64(meta, at + 8, variable.Offset); diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 36e88bf315..19202cd7bb 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -45,6 +45,13 @@ std::vector WindowsPePatcher::Patch(std::vector sour report(500); const WindowsRelocationBuilder relocationBuilder; auto relocations = relocationBuilder.Apply(image, dynamicSection, originalHeaders); + for (const auto& header : originalHeaders) { + if (header.Type != 7 || header.FileSize == 0) continue; + const auto templateRva = image.GetRva(header.MappedAddress, header.FileSize); + for (const auto& import : relocations.Imports) + if (import.TargetRva < static_cast(templateRva) + header.FileSize && static_cast(import.TargetRva) + 8 > templateRva) + throw Domain::RelinkerException("Runtime-bound executable TLS template relocation is not supported", header.MappedAddress + import.TargetRva - templateRva); + } report(1000); auto sections = image.BuildSections(); report(1500); diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 7cb21ce91b..4c2355bed9 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -498,7 +498,7 @@ std::set CodeInstructionCollector::Collect(const std::ve const auto offset = address - header.MappedAddress; const auto info = decoder.DecodeInstruction(text.data() + offset, text.size() - offset); addAddressTakenRoot(address, text.data() + offset, info); - if (info.HasBranchTarget && !info.HasRipRelativeDisp) addRoot(static_cast(static_cast(address + info.Length) + info.BranchDisp)); + if (info.HasBranchTarget && !info.HasRipRelativeDisp) addRoot(address + info.Length + static_cast(info.BranchDisp)); } } } while (roots.size() != previousRoots); diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index b9d67a3069..7f0f18b0da 100644 --- a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp +++ b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp @@ -27,8 +27,7 @@ void ValidationPolicy::ValidateRelocationTypeSupported(const std::uint32_t reloc } if (_supportedRelocationTypes.find(relocationTypeValue) == _supportedRelocationTypes.end()) { std::ostringstream msg; - msg << "Unsupported relocation type 0x" << std::hex << relocationTypeValue - << " at offset 0x" << fileByteOffset; + msg << "Unsupported relocation type 0x" << std::hex << relocationTypeValue; throw RelinkerException(msg.str(), fileByteOffset); } } diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index b63ed31554..ceda75412d 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -315,6 +315,9 @@ RelinkResult RelinkerPipeline::Relink() { if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize || (dynSymTabSize && (off > *dynSymTabSize || symEntSize > *dynSymTabSize - off))) throw RelinkerException("Symbol table entry out of bounds", symOff); + if (symIdx == 0 && std::any_of(raw.begin() + static_cast(symOff), raw.begin() + static_cast(symOff + symEntSize), + [](const std::uint8_t byte) { return byte != 0; })) + throw RelinkerException("Dynamic symbol table entry 0 is not a null symbol entry", symOff); return symOff; }; diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 48317b9b9a..796c5c96d9 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -252,6 +252,25 @@ void scanning() { require(progress.Events.back().Completed < progress.Events.back().Total, "Failed scan reported completion"); } +void branchAcrossSignedBoundary() { + std::vector image(64 + 32, 0x90); + image[64] = 0xc3; + const std::array jump{0xe9, 0xeb, 0xff, 0xff, 0xff}; + std::copy(jump.begin(), jump.end(), image.begin() + 64 + 16); + image.back() = 0xc3; + Domain::ProgramHeader header{}; + header.Type = 1; + header.Flags = 1; + header.Offset = 64; + header.MappedAddress = 0x7ffffffffffffff0; + header.FileSize = image.size() - 64; + header.MemorySize = header.FileSize; + Io::WriteU64(image, 24, 0x8000000000000000); + const auto instructions = Relinker::CodeInstructionCollector().Collect(image, {header}); + require(instructions.contains(0x8000000000000000), "Lost the entry above the signed boundary"); + require(instructions.contains(0x7ffffffffffffff0), "Lost the branch target below the signed boundary"); +} + class FailingProgress final : public Domain::IProgress { public: std::string InputPath; @@ -368,7 +387,7 @@ void relocationValidation() { converter.Convert(source, {}); throw std::runtime_error("Unsupported relocation type was accepted"); } catch (const Domain::RelinkerException& error) { - require(std::string(error.what()).find("Unsupported relocation type 0x99") != std::string::npos, "Wrong error for unsupported relocation"); + require(std::string(error.what()) == "Unsupported relocation type 0x99", "Wrong error for unsupported relocation"); require(error.FailureOffset == 0x2700, "Wrong failure offset for unsupported relocation"); } } @@ -407,6 +426,7 @@ int main() { runtimeLifecyclePolicy(); fixedBase(); scanning(); + branchAcrossSignedBoundary(); diagnosticContext(); cancellation(); baseRelocations(); diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index f36d44cf6d..18727334c0 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -26,10 +26,11 @@ TARGETS = {(): "linux", ("--lazy-binding",): "lazy", ("--windows",): "windows", TLS_MEMSZ, TLS_ALIGN = 0x24, 16 TLS_BLOCK = 0x30 DEFAULT_BASE = 0x400000 -STRINGS = b"\0Tls\0Data\0ImportedTls\0" +STRINGS = b"\0Tls\0Data\0ImportedTls\0Function\0libFunction.prx\0" TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10 +FUNCTION_NAME, LIBRARY_NAME = 22, 31 -NULL, TLS_SYMBOL, DATA_SYMBOL, IMPORTED = 0, 1, 2, 3 +NULL, TLS_SYMBOL, DATA_SYMBOL, IMPORTED, FUNCTION = 0, 1, 2, 3, 4 def image(relocations, tls=True, symbol_bytes=None, tls_memsz=TLS_MEMSZ): @@ -47,6 +48,7 @@ def image(relocations, tls=True, symbol_bytes=None, tls_memsz=TLS_MEMSZ): struct.pack_into("\n#include \n' + 'int main(int argc, char **argv) {\n' + ' if (argc == 1) return 0;\n' + ' void *provider = dlopen(argv[1], RTLD_NOW | RTLD_GLOBAL);\n' + ' if (!provider) { puts(dlerror()); return 1; }\n' + ' void *consumer = dlopen(argv[2], RTLD_NOW | RTLD_GLOBAL);\n' + ' if (!consumer) { puts(dlerror()); return 2; }\n' + ' int (*read)(void) = dlsym(consumer, "Read");\n' + ' if (!read) return 3;\n' + ' printf("%d\\n", read());\n' + ' return 0;\n}\n', library=False) + command = (['/usr/bin/arch', '-x86_64'] if platform.machine() == 'arm64' else []) + [str(probe)] + run = subprocess.run(command, capture_output=True, text=True, timeout=10) + if run.returncode and 'Bad CPU type in executable' in run.stderr: + assert os.environ.get('GITHUB_ACTIONS') != 'true', 'macOS CI must execute the x86_64 weak-export probe' + return None + assert run.returncode == 0, (run.returncode, run.stdout, run.stderr) + kernel = compile_c(work, 'libkernel.prx', 'int FixtureKernel(void) { return 0; }\n') + consumer = compile_c(work, 'consumer.dylib', + 'int Sample(void) __asm__("_Sample#guest");\n' + 'int Data __asm__("_Data#guest");\n' + '__attribute__((weak)) int Sample(void) { return 77; }\n' + '__attribute__((weak)) int Data = 88;\n' + 'int Read(void) { return Sample() + Data; }\n') + controls = {} + for binding in (1, 2): + attribute = '__attribute__((weak)) ' if binding == 2 else '' + controls[binding] = compile_c(work, f'native-{binding}.dylib', + 'int Sample(void) __asm__("_Sample#guest");\n' + 'int Data __asm__("_Data#guest");\n' + + attribute + 'int Sample(void) { return 7; }\n' + + attribute + 'int Data = 11;\n') + return command, kernel, consumer, controls + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix='anyps5-macos-weak-exports-') as directory: + work = Path(directory) + native = native_probe(work) + for binding in (1, 2): + for tls in (False, True): + case = work / f'{binding}-{tls}' + modules = case / 'sce_module' + modules.mkdir(parents=True) + (modules / 'sample.prx').write_bytes(module(binding, tls)) + source = case / 'input.elf' + source.write_bytes(main_fixture()) + run = subprocess.run([str(relinker), '--macos', str(source), str(case / 'output')], + capture_output=True, text=True, timeout=10) + assert run.returncode == 0, (binding, tls, run.stdout, run.stderr) + converted = case / 'app0/sce_module/sample.prx.guest.prx' + data = converted.read_bytes() + entries = exports(data) + flags = 4 if binding == 2 else 0 + if native and not tls: + command, kernel, consumer, controls = native + libraries = case / 'libs' + libraries.mkdir() + shutil.copyfile(kernel, libraries / 'libkernel.prx') + results = [] + for provider in (controls[binding], converted): + probe = subprocess.run([*command, str(provider), str(consumer)], + capture_output=True, text=True, timeout=10) + assert probe.returncode == 0, (binding, probe.returncode, probe.stdout, probe.stderr) + results.append(probe.stdout.strip()) + assert results[0] == results[1], (binding, results) + if binding == 2: + assert results[0] == '18', results + assert set(entries) == {'_Sample#guest', '_Data#guest'} | ({'_Tls#guest'} if tls else set()), entries + assert all(value[0] == flags for value in entries.values()), (binding, tls, entries) + assert bool(MachO(data).flags & 0x8000) == (binding == 2) + if native: + print('macOS guest weak exports: x86_64 function/data coalescing matches native weak and strong controls') + print('macOS guest weak export tests passed') + + +if __name__ == '__main__': + main() diff --git a/core/relinker/relinker/tests/test_macos_tls_exports.py b/core/relinker/relinker/tests/test_macos_tls_exports.py new file mode 100644 index 0000000000..d1b795a630 --- /dev/null +++ b/core/relinker/relinker/tests/test_macos_tls_exports.py @@ -0,0 +1,77 @@ +"""Check `relinker --macos` on TLS exports that end at the end of PT_TLS.""" +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import module_with_symbol +from test_macos_executable_exports import convert, executable, exports +from test_macos_output import MachO + + +def guest(value, size): + image = module_with_symbol(True) + struct.pack_into(" ResourceMaterializer::RuntimeImageModes(const ImageRe append(IrTextureNumericClass::Uint, format, IrBufferFormat::Invalid, false, false); } } + if (storage && !image.packed && image.written && !image.read && !image.atomic) { + constexpr std::array formats{IrBufferFormat::Format16Float, IrBufferFormat::Format16_16Float, IrBufferFormat::Format16_16_16_16Float}; + for (const auto format : formats) append(IrTextureNumericClass::Uint, format, IrBufferFormat::Invalid, false, false); + } if (image.depthCompare && !table && (image.emulatedCompare & EmulatedCompare::Unsupported) == 0u && (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray)) { auto mode = image; mode.numericClass = IrTextureNumericClass::Float; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index cd5552084d..1e8a30d17e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -813,6 +813,9 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce auto& state = ctx.state; const auto& mem = access.mem; const auto sint = SintStorageFormat(access.image); + const auto storageFormat = GetFormatInfo(access.image.conversionFormat); + const auto float16 = access.image.resourceClass == ImageResourceClass::Storage && access.image.numericClass == IrTextureNumericClass::Uint && + (access.image.conversionFormat == IrBufferFormat::Format16Float || access.image.conversionFormat == IrBufferFormat::Format16_16Float || access.image.conversionFormat == IrBufferFormat::Format16_16_16_16Float); std::uint32_t values[4] = {}; const auto dmask = EffectiveDmask(mem); for (std::uint32_t component = 0; component < 4u; component++) { @@ -834,13 +837,17 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce raw = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), RuntimeImageSwizzle(state, mem.resource, source), ConstantU32(state, component + 4u)), candidate, raw); } values[component] = integer ? raw : mem.dataBits == 16u ? EmitF16BitsToF32(state, raw) : Unary(state, spv::OpBitcast, TypeF32(state), raw); + if (float16 && component < storageFormat.componentCount) { + values[component] = EmitFormatStoreComponent(state, storageFormat, component, values[component]); + } if (component < sint.componentCount) { values[component] = mem.dataBits == 16u ? EmitD16StoreComponent(state, sint, component, values[component]) : EmitFormatStoreComponent(state, sint, component, values[component]); } } const auto texel = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, integer ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4), texel, values[0], values[1], values[2], values[3]); - return sint.format != IrBufferFormat::Invalid ? texel : PackImageTexel(ctx, access, texel); + const bool rawUintStorage = sint.format != IrBufferFormat::Invalid || float16; + state.module.AddFunction(spv::OpCompositeConstruct, integer || rawUintStorage ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4), texel, values[0], values[1], values[2], values[3]); + return rawUintStorage ? texel : PackImageTexel(ctx, access, texel); } std::uint32_t PackedStoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t data) { diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 8bb6b25b00..98784404a0 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -224,7 +224,7 @@ On Windows use `agc_pm4_replay.exe` and prepend `build/core/libs/libs/unpatched` ## Frame capture and replay -Set `APS5_FRAME_CAPTURE` to a directory to record a few frames of a title at the AGC driver boundary, then replay them with `agc_frame_replay` through any build of the driver with no title running. From the start of the process the driver keeps every shader registration and ABI resolution, video output registration and EOP flip reservation. At the frame boundary picked by `APS5_FRAME_CAPTURE_AT_FLIP` (default 1), or by the appearance of the file `APS5_FRAME_CAPTURE_TRIGGER` names (a number in it replaces the frame count), it waits for the driver to go idle and writes every committed, readable allocation of the guest allocation registry, the title's image included, as zstd chunks without their all-zero 64 KiB blocks. A frame ends at a CPU flip or at a submission that flips (a flip packet, or the EOP release of a reserved flip token); the reservation itself only records its token. For the next `APS5_FRAME_CAPTURE_FRAMES` (10) frames it records each submission (queue, command buffer address and size), flip and suspend point with the thread that made it and the accepted and completed serials, each preceded by the guest pages the write tracker saw the CPU write since the previous call that handed work to the GPU (`GuestMemory::CollectWrittenBlocks`, per 4 KiB where the tracker kept page stamps), read once the submissions already accepted have completed (a recorded submission, flip or suspend point reaches the driver before the recorder takes its next call, so that wait covers every call recorded before the delta); an allocation the registry gained after the snapshot is recorded there as a new range with its nonzero 64 KiB blocks. A submission ends a frame only when it flips itself, not when another thread's submission flipped while it was being submitted. Shader registrations and ABI resolutions only collect the blocks of the shader header and the arrays it points to, and the pages of the title's image only when their content changed. At the end it stores each display buffer the title presented, decoded, as a reference image. A capture is `calls.bin`, `memory.bin` and `capture.json` (commit, switches, sizes, warnings), and the recorder stops with a warning when its volume has less than 4 GiB free. A 20-frame capture in the Demon's Souls Nexus is 5.3 GiB of snapshot and 11.5 GiB of deltas before compression, 3.9 GiB on disk. +On Windows and Linux, set `APS5_FRAME_CAPTURE` to a directory to record a few frames of a title at the AGC driver boundary, then replay them with `agc_frame_replay` through any build of the driver with no title running. From the start of the process the driver keeps every shader registration and ABI resolution, video output registration and EOP flip reservation. At the frame boundary picked by `APS5_FRAME_CAPTURE_AT_FLIP` (default 1), or by the appearance of the file `APS5_FRAME_CAPTURE_TRIGGER` names (a number in it replaces the frame count), it waits for the driver to go idle and writes every committed, readable allocation of the guest allocation registry, the title's image included, as zstd chunks without their all-zero 64 KiB blocks. A frame ends at a CPU flip or at a submission that flips (a flip packet, or the EOP release of a reserved flip token); the reservation itself only records its token. For the next `APS5_FRAME_CAPTURE_FRAMES` (10) frames it records each submission (queue, command buffer address and size), flip and suspend point with the thread that made it and the accepted and completed serials, each preceded by the guest pages the write tracker saw the CPU write since the previous call that handed work to the GPU (`GuestMemory::CollectWrittenBlocks`, per 4 KiB where the tracker kept page stamps), read once the submissions already accepted have completed (a recorded submission, flip or suspend point reaches the driver before the recorder takes its next call, so that wait covers every call recorded before the delta); an allocation the registry gained after the snapshot is recorded there as a new range with its nonzero 64 KiB blocks. A submission ends a frame only when it flips itself, not when another thread's submission flipped while it was being submitted. Shader registrations and ABI resolutions only collect the blocks of the shader header and the arrays it points to, and the pages of the title's image only when their content changed. At the end it stores each display buffer the title presented, decoded, as a reference image. Without the guest write watch (`APS5_NO_WRITE_WATCH`, or no write watch on the host) the capture throws when it starts, since it could not record CPU stores. A capture is `calls.bin`, `memory.bin` and `capture.json` (commit, switches, sizes, warnings), and the recorder stops with a warning when its volume has less than 4 GiB free. A 20-frame capture in the Demon's Souls Nexus is 5.3 GiB of snapshot and 11.5 GiB of deltas before compression, 3.9 GiB on disk. `agc_frame_replay [--no-pace] [--no-release] [--release-after-ms ] [--no-synthesize] [--strict] [--dump
::]... [--frames ]` maps the captured ranges at their addresses and registers them with their access (a range the replaying process already maps writable, such as the driver's fixed memory, is only refilled, and a non-GPU range it uses itself is skipped and counted), repeats the registrations, then replays the calls of each captured thread on a thread of its own, started in captured order. Before a call that hands work to the GPU it waits until no more submissions are in flight than when the title made it (`--no-pace` turns this off). Titles release GPU waits from threads that never call the driver, so when the driver completes nothing for 3 ms the replay reads the waits its queues are blocked on (`AgcDriverAwaitedWaits`) and writes the next recorded CPU value of the awaited dword that satisfies the compare, else the last earlier one, else a value derived from the reference (`--no-synthesize` turns this last step off). It prints one JSON line with the frame count and times, the submissions, the pacing wait, the released waits and how many were derived, the submissions that never completed and, for captures with reference images, the share of identical pixels and the largest channel difference of the replayed display buffers (`--frames` writes both images). Effects that depend on timing (temporal antialiasing, particles) make pixels differ, so expect a visual match rather than identical frames. Replay times follow the GPU clock state: lock the clocks (`nvidia-smi -lgc`, `-lmc`) when comparing two drivers. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 5f175220c4..ee7f6ba09d 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -83,6 +83,9 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `ANYPS5_REQUIRE_ROBUST2` | presence | `core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp` | | fails the LDS lock-budget test when the device does not enable robustBufferAccess2, instead of only printing it | | `ANYPS5_REQUIRE_VULKAN` | presence | `core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp` | | fails a test instead of skipping when no usable Vulkan device is present | | `ANYPS5_SCREENSHOTS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Screenshot.cpp` | | directory the screenshot key saves its PNG files to, defaulting to anyps5-screenshots beside the executable | +| `APS5_AB` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp` | | `:[+...]`: alternates phase A (switches as the environment sets them) and phase B (the listed runtime switches toggled) at flip packets, each phase at least `` long; prints `[ab]` lines; a name that is not a runtime switch fails the first flip | +| `APS5_AB_SETTLE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp` | 2 | seconds into a phase after which APS5_AB prints the phase's `settled` line; shorter than the phase | +| `APS5_AB_START` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp` | 0 | flip packet count (from 1) at which APS5_AB starts, or `s` for seconds after the first flip | | `APS5_ADDRESS_COPY_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 64 | upper bound in MiB on the guest memory one address-based build copies; exceeding it aborts | | `APS5_ALL_BUFFERS_WRITTEN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | notes every shader buffer element as written, as before bufferWritten existed | | `APS5_ASSERT_GPU_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | aborts when a guarded path is reached without holding GuestMemory::GpuMutex | @@ -206,7 +209,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_ADJACENT_GENERATION` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | leaves adjacent pending images at their generation during a write-back, as before | | `APS5_NO_ALIAS_BORROW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | disables borrowing a pending aliased storage image | | `APS5_NO_ATOMIC_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds atomic shader elements in place instead of staging them through a device-local buffer | -| `APS5_NO_BARRIER_BATCH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the staging, write-back and label-run leading barriers that a covering barrier already orders, instead of leaving them out | +| `APS5_NO_BARRIER_BATCH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/RuntimeSwitches.cpp` | | records the staging, write-back and label-run leading barriers that a covering barrier already orders, instead of leaving them out; APS5_AB can toggle it | | `APS5_NO_BARRIER_ELISION` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every leading barrier instead of merging it into the following submission | | `APS5_NO_BDA_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds the written elements of address-based compute builds in place in the host imports instead of staging them device-local | | `APS5_NO_BDA_TABLE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp` | | builds every BDA descriptor table instead of reusing the recent-table cache | @@ -542,6 +545,12 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti |------|------|--------|---------|-------------| | `ANYPS5_LANGUAGE` | integer | `core/libs/prx/libSceSystemService/SystemService.hpp` | 1 | console language number returned by sceSystemServiceParamGetInt, defaulting to English US | +### libSceUsbStorage + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_USB_STORAGE_PASSTHROUGH` | presence | `core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp` | | passes the FAT and exFAT volumes the host mounted from USB mass-storage devices through to the guest, writable; unset, no host volume is scanned and the guest sees no USB storage device | + ### libSceVideoOut | Name | Kind | Source | Default | Description | diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index 51854e22c3..57168055f8 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -69,6 +69,7 @@ See [Build](BUILD.md#performance-trace) for the trace format, summary fields and | `APS5_TRACE_SUMMARY_EVERY=` | Writes periodic summary lines without recording trace events. Can be combined with either window switch. | | `APS5_TRACE_EVENTS=` | Sets the per-thread event-buffer capacity for a frame or seconds window (1–4,194,304; default 262,144). | | `APS5_TRACE_DIR=` | Writes performance capture and summary files to this directory instead of the working directory. | +| `APS5_AB=:[+...]` | Alternates phase A (the environment's settings) and phase B (the listed switches toggled) at flip packets within one run, each phase at least `` long. Every phase change prints `[ab] phase A\|B begins at present (t s)` and, `APS5_AB_SETTLE` seconds (default 2) later, `[ab] phase A\|B settled at present (t s)`; `t` counts from the first flip. `APS5_AB_START=` starts at flip ``, `APS5_AB_START=s` `` seconds after the first flip. Only `APS5_NO_BARRIER_BATCH` can be listed; any other name fails the first flip. `python tools/ab_split.py ` prints the mean of each periodic report (`[present]`, `[gputime]`, `[draw] pipelined`, `[syncwait]`) per phase, counting only reports that lie wholly inside a settled phase, and each B phase against its neighbouring A phases. | | `APS5_VIDEOOUT_TELEMETRY_DIR=` | Writes the presents and the pad changes of a run to `/videoout.jsonl`; see [Usage](../user/USAGE.md#video-out-and-pad-telemetry). | ## Shader registers diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7cc0989ba2..36563b443d 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -77,7 +77,7 @@ ### Functional - [Written staging window on discrete GPUs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written buffer element of a dispatch that is not swept is staged device-local up to 8 MiB on a discrete GPU (`Context::discreteGpu`) and up to 2 MiB otherwise; `APS5_WRITTEN_SHADOW_MAX_KIB` replaces either bound. The 8 MiB figure comes from one title on one NVIDIA Windows system (PPSA09911: a 2.5 and a 3.3 MiB skin-cache output written in place cost 55 ms of GPU time per frame, 1.8 ms of extra staging copies once staged); the point where the two copies of a larger element cost more than its in-place stores, and whether AMD or Linux drivers move it, has not been measured. The window is per element and per use, so a kernel that streams once through an element inside the window pays the copy-in and the copy-back on every dispatch. -- [Frame capture](../../../core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp) (libSceAgcDriver) records guest memory on Windows only (a VirtualQuery walk of the registry allocations and the Windows write tracker); elsewhere a capture throws when it starts. Ranges are captured by allocation and protection, not by mapping: section views, aliases of one direct memory block and file views are snapshotted and replayed as separate private memory, so a store through one alias is not seen through another, and CPU stores to them during the captured frames are not recorded (only write-watched private memory is). Pages of the title's image are recorded during the frames only when a shader header or an array it points to lies in them and their content changed. A capture starts only at a frame boundary where the driver goes idle within 10 s, and gives up after five boundaries that do not. A capture also stops when GPU work in flight at a delta waits on a later CPU store. +- [Frame capture](../../../core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp) (libSceAgcDriver) throws on macOS. Section views, aliases of one direct memory block and file views replay as separate private memory, so a store through one alias is not seen through another. Only write-watched memory records CPU stores during the frames: private memory on Windows, where a store through one alias is recorded only at that alias; registered ranges on Linux, including direct memory unless mapped more than once (libkernel leaves those views unwatched, so no store through any of them is recorded). Title image pages are recorded only when changed and holding a shader header or its arrays. Linux omits no-access pages (Windows records them unreadable) and records file mapping pages past the file's end as unreadable. A capture starts only at a frame boundary where the driver idles within 10 s (five tries), and stops when GPU work in flight at a delta waits on a later CPU store. - [Frame replay](../../../core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp) (libSceAgcDriver) orders the calls of different captured threads only by their start (a call starts once every earlier call has started), so the driver sees a schedule the title could have produced, not necessarily the one it did. CPU stores a title made from threads that never call the driver are known only by the next recorded call, so the replay applies them early or late when a GPU wait needs them and derives a value from the wait's reference when no recorded store satisfies it (`of_them_synthesized`). Pacing compares the number of submissions in flight, not what they hold. Prologue registrations of shaders whose memory the title reused after registering them fail and are counted (`stale_registrations`). The title's image range must be free in the replaying process, so the tool is linked at a high image base on Windows. - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. - [Experimental submission progress](../../../core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionProgress.hpp) (AGC driver) - the opt-in two-millisecond pacing budget is not derived from console hardware. Cross-title performance remains unvalidated; host scheduling can overshoot the timeout and pacing can reduce CPU/GPU overlap. @@ -286,3 +286,4 @@ ### Opt-in draw paths - [Draw finisher](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp) (`APS5_DRAW_PIPELINE`) - opt-in. The Linux main-image page table it adds matches what Windows already does by default. +- [Multi-submit count overflow and buffer validation](../../../core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp) (libSceAgcDriver) - whether the console driver imposes an architectural submission packet limit or allows null buffer pointers with non-zero sizes in multi-submit arrays is unmeasured; AgcDriver::SubmitMany validates count against 32-bit pointer array bounds overflow and rejects null addresses for non-empty command buffers with a runtime exception. diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index ae062de3f3..83ecbcb38d 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -5,7 +5,7 @@ - [AMPR command buffer null arguments](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the result for a null command buffer is unknown for `sceAmprCommandBufferConstructor`, `sceAmprAprCommandBufferConstructor`, `sceAmprCommandBufferReset` and `sceAmprCommandBufferSetBuffer`; these return `SCE_KERNEL_ERROR_EINVAL` here, but the behavior has not been confirmed on a PS5. - [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. -- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_KERNEL_ERROR_EBUSY` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes +- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands other than `Nop` use the emulator's own encoding with an 8-byte header, so their measured sizes are not the console's. `Nop(n)` occupies n dwords and `NopWithData(n)` (n + 1) dwords with the header word `0x5452000F | payloadDwords << 8 | (type & 0xF) << 12`, and `ConstructNop` writes its word before the payload and zeroes the payload's padding, as observed from firmware 13.42's `libSceAmpr.sprx` (#10936); a null `base` is reported before the count or payload range. One console behaviour is not reproduced: `NopWithData` with data and `ConstructNop` write four zero bytes right after the payload even where they fall past the end of the record, and of the buffer; AnyPS5 stops at the record. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [APR map begin and end](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted - [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp): colors are not recorded and command sizes use the host encoding. Native signatures confirm that set takes a color pointer and push/measure take the color value. - [__cxa_call_unexpected](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes the in-flight exception's unwind header and never returns, as [libcxxabi 18.1.8](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_personality.cpp#L1171-L1211) does. No `std::set_unexpected` is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls `std::terminate` ([cxa_default_handlers.cpp](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_default_handlers.cpp#L88-L99)); it therefore forwards to libc's `__cxa_call_terminate`, which begins the catch and runs the current terminate handler. That matches libcxxabi except for which terminate handler runs: there `std::terminate` after `__cxa_begin_catch` ([cxa_exception.cpp L470-L477](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L470-L477)) runs the handler saved in the caught exception's header ([cxa_handlers.cpp L72-L93](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_handlers.cpp#L72-L93)), which `__cxa_throw` and `__cxa_init_primary_exception` set to `std::get_terminate()` when the exception is thrown ([cxa_exception.cpp L209-L214](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L209-L214), [L279](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L279)), while here the current global handler runs, as AnyPS5's own `std::terminate` does. The two differ only if `std::set_terminate` was called between the throw and this call. A null header terminates because this emulator's libc `__cxa_begin_catch` terminates on null (libcxxabi does not define that case). Dynamic exception specifications that would let a replaced unexpected handler throw a permitted type are not modelled diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 1dbcd434cf..d0daef9004 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -6,11 +6,13 @@ - [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` and another mount point returns `NOT_MOUNTED` as in `sceAppContentTemporaryDataFormat` and `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified -- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 15 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceAppContent pending exports](../../../core/libs/prx/libSceAppContent/Export.cpp) - 8 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAppContent.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [Initialization](../../../core/libs/prx/libSceAppContent/Export.cpp) - service readiness and boot attributes are not modeled; successful initialization returns a zero boot record. Initialization-state guards, SDK 0x01500000 repeated-initialization errors and reserved-field checks are recovered. SDK-dependent service-error translation remains incomplete. - [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified +- [sceAppContentAddcontEnqueueDownloadByEntitlemetId, sceAppContentAddcontEnqueueDownloadSp, sceAppContentAddcontMountByEntitlemetId and sceAppContentAddcontShrink](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - unverified on a PS5. After the initialization guard they answer `PARAMETER` for a null argument and `NOT_FOUND` otherwise, without writing the mount point, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2125-L2209) does for content it does not know; the prototypes are [dlc_emu's](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L88-L99). dlc_emu rejects a padded label at every SDK while the label calls above check it only from SDK 0x01500000 or 0x03500000, so `Sp` and `Shrink` answer `PARAMETER` for one from SDK 0x03500000 and throw below it +- [sceAppContentGetAddcontInfoByEntitlementId, sceAppContentGetAddcontInfoListByIroTag and sceAppContentGetPftFlag](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - unverified on a PS5. After the initialization guard they give [dlc_emu's](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2040-L2086) answers for content it does not know: `DRM_NO_ENTITLEMENT` without writing the info, 0 entries for any tag (a null `hitNum` beside a list returns 0, unlike `sceAppContentGetAddcontInfoList`), and a [PFT flag of 0](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2343-L2347), and `PARAMETER` for null arguments, with dlc_emu's [prototypes](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L74-L131) - [sceAppContentGetEntitlementKey](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - the service result without an entitlement is unverified: this model returns `DRM_NO_ENTITLEMENT` and leaves the 16-byte key unchanged. Initialization, null-pointer and SDK 0x01500000 label-padding checks match the reported [firmware 13.42 module probe](https://github.com/boykopovar/AnyPS5/pull/9638#issuecomment-6390573735), run in an emulator, not on hardware. That module forwards service labels 0, 7, 8 and 0xFFFFFFFF without checking them; this library ignores the service label. The system service's own validation and errors remain unknown. - [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) console behaviour and hardware return values are unverified, including behaviour with open files across unmount; host filesystem failures throw. Storage models one `/temp0` mount, preserving files on unmount while rejecting new path resolutions until remount, and leaving existing open host descriptors usable. `sceAppContentTemporaryDataMount` delegates to `sceAppContentTemporaryDataMount2` with option 1 (`TEMPORARY_DATA_OPTION_FORMAT`) to format on mount as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/app_content/app_content.cpp#L550-L553) does. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); pointer ABI follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). diff --git a/docs/dev/debt/libSceAvSetting.md b/docs/dev/debt/libSceAvSetting.md index 6bbd62abd0..56072cafbe 100644 --- a/docs/dev/debt/libSceAvSetting.md +++ b/docs/dev/debt/libSceAvSetting.md @@ -2,6 +2,28 @@ ### Unknown function info +- [sceAvControlChangeOutputLayout, sceAvControlChangeOutputMode, sceAvControlChangeOutputModeForDiag, sceAvControlChangeProcessAttribute, sceAvControlChangeScanInMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlGetCurrentDeviceId, sceAvControlGetCurrentOutputMode, sceAvControlGetCurrentOutputModeWithSync, sceAvControlGetDetailHdcpStatus, sceAvControlGetDeviceInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlGetHdcpStatus, sceAvControlGetMonitorInfo, sceAvControlGetNativeMonitorInfo, sceAvControlGetPuStatus, sceAvControlGetPuType](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlInit, sceAvControlIsAudioOutModeSupportedByMonitorInfo, sceAvControlIsColorimetryHdr, sceAvControlIsDeepColorSupportedByMonitorInfo, sceAvControlIsDeviceConnected](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlIsHdrSupportedByMonitorInfo, sceAvControlIsModeGen1Vr, sceAvControlIsModeSystemDefault, sceAvControlIsSuspendedProcessOutputModeAvailable, sceAvControlIsVideoOutModeSupportedByMonitorInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlSet2dVrMode, sceAvControlSetAudioMuteForShutdown, sceAvControlSetAvOutputMode, sceAvControlSetColorEffect, sceAvControlSetDefaultAudioOutMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlSetDefaultVideoOutMode, sceAvControlSetDispclk, sceAvControlSetGamutMetadata, sceAvControlSetHdcpEncryption, sceAvControlSetHdcpStatus](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvControlSetHdmiGamutMetadata, sceAvControlSetHdrMetadata, sceAvControlSetVideoOutSource, sceAvControlSimulateProcessOutputModeArbitration, sceAvSettingAddCallbacks](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingAddCallbacksForLnc, sceAvSettingAddCallbacksForSocialScreen, sceAvSettingCallbackFuncsInit, sceAvSettingCallbackFuncsInitForLnc, sceAvSettingCallbackFuncsInitForLnc_](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingCallbackFuncsInit_, sceAvSettingChangeOutputMode, sceAvSettingChangeOutputMode2, sceAvSettingChangeOutputMode3, sceAvSettingChangeOutputModeForDiag](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingChangeProcessAttribute, sceAvSettingCheckCallback, sceAvSettingControlHdcpEncryption, sceAvSettingDebugAddCallbacks, sceAvSettingDebugClearDiagCommand](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingDebugSetDiagState, sceAvSettingDebugSetHdmiMonitorInfo, sceAvSettingDebugSetProcessAttribute, sceAvSettingDriverChangeConnectionStatus, sceAvSettingDriverUpdateStatus](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingEnterAudioMuteForShutdown, sceAvSettingGetCurrentDeviceInfo_, sceAvSettingGetCurrentHdmiDeviceId, sceAvSettingGetCurrentOutputMode2_, sceAvSettingGetCurrentOutputMode_](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingGetDetailedHdcpStatus, sceAvSettingGetDeviceInfo, sceAvSettingGetHdcpStatus, sceAvSettingGetHdmiConnectDisconnectNum, sceAvSettingGetHdmiKsvList](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingGetHdmiMonitorInfo, sceAvSettingGetHdmiRawEdid, sceAvSettingGetMorpheusPuStatus, sceAvSettingGetNativeHdmiMonitorInfo, sceAvSettingGetRawHdmiMonitorInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingInit, sceAvSettingIsSupportedAudioOutModeByHdmiMonitorInfo, sceAvSettingIsSupportedHdcpVersionByHdmiMonitorInfo, sceAvSettingIsSupportedVideoOutModeByHdmiMonitorInfo, sceAvSettingIsSuspendedProcessOutputModeAvailable](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingIsSuspendedProcessOutputModeAvailable2, sceAvSettingIsVrSupportedByHdmiMonitorInfo2, sceAvSettingNotifyAudioOutMode, sceAvSettingNotifyResponseForSocialScreen, sceAvSettingNotifyUmdEvent](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingNotifyVideoOutMode, sceAvSettingRemoveCallbacks, sceAvSettingRemoveProcessOutputMode, sceAvSettingSet2dVrMode, sceAvSettingSetAudioOutModeAny_](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingSetAudioOutModeInvalid_, sceAvSettingSetAvOutputMode, sceAvSettingSetDispclk, sceAvSettingSetDisplayBlackLevel, sceAvSettingSetHdcpMode](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingSetHdcpStatus, sceAvSettingSetHdmiGamutMetadata_, sceAvSettingSetVideoOutModeAny_, sceAvSettingSetVideoOutModeInvalid_, sceAvSettingSetVideoOutSource](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceAvSettingSetVideoOutputColorEffect, sceAvSettingSimulateProcessOutputModeArbitration_, sceAvSettingTerm, sceAvSettingWaitCallback](../../../core/libs/prx/libSceAvSetting/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAvSetting.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. + - [sceAvControlGetPuTypeByMonitorInfo](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature - [sceAvControlIsVideoOutModeSupportedByMonitorInfoWithOptions](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature - [sceAvControlSetDitherModeForDiag](../../../core/libs/prx/libSceAvSetting/Export.cpp) (libSceAvSetting) - unknown signature diff --git a/docs/dev/debt/libSceBluetoothHid.md b/docs/dev/debt/libSceBluetoothHid.md index faf41e09da..83461210f3 100644 --- a/docs/dev/debt/libSceBluetoothHid.md +++ b/docs/dev/debt/libSceBluetoothHid.md @@ -7,4 +7,5 @@ ### Unknown function info - [sceBluetoothHidGetDeviceName and sceBluetoothHidGetDeviceInfo](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): output pointers are forwarded to device ioctls without accessing the buffers. Their output sizes and layouts remain unresolved. Report APIs use 64-bit device handles; device-info and disconnect APIs use 32-bit device IDs. +- [sceBluetoothHidParamInitialize](../../../core/libs/prx/libSceBluetoothHid/Export.cpp) follows firmware 13.42's module as BunnyFl ran it natively ([comment](https://github.com/boykopovar/AnyPS5/pull/11124#issuecomment-6416053242); not a console, and neither the kernel side of `/dev/bt` nor the event thread's work is covered): it writes its second argument at offset 0 of the 16-byte block, clears the 32-bit word at offset 8, leaves bytes 12 to 15 and returns 0, and a null block returns `0x81540001`. [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Bluetooth/SceBluetoothHid.cs#L21-L49) types that argument as a 64-bit value; it is the address of a `BluetoothHidThreadParam`, since `sceBluetoothHidRegisterCallback` reads the event thread's priority and affinity through it. The same run covers `sceBluetoothHidThreadParamInitialize`, still unimplemented here - [sceBluetoothHidDebugGetVersion](../../../core/libs/prx/libSceBluetoothHid/Export.cpp): returns a constant without reading arguments, so the supplied implementation does not establish whether the public prototype has unused parameters. diff --git a/docs/dev/debt/libSceCdlgUtilServer.md b/docs/dev/debt/libSceCdlgUtilServer.md new file mode 100644 index 0000000000..c6a2a8990a --- /dev/null +++ b/docs/dev/debt/libSceCdlgUtilServer.md @@ -0,0 +1,7 @@ +# libSceCdlgUtilServer technical debt + +### Unknown function info + +- [_ZN3sce16CommonDialogUtil6Server13setFinishDataEPvm, _ZN3sce16CommonDialogUtil6Server6setRunEPvm, _ZN3sce16CommonDialogUtil6Server9setFinishEv, _ZN3sce16CommonDialogUtil6Server9setResultEi, _ZN3sce16CommonDialogUtil6ServerC2EPKcmmis](../../../core/libs/prx/libSceCdlgUtilServer/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCdlgUtilServer.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce16CommonDialogUtil6ServerD0Ev, _ZN3sce16CommonDialogUtil6ServerD1Ev, _ZN3sce16CommonDialogUtil6ServerD2Ev, _ZNK3sce16CommonDialogUtil6Server10getVersionERj, _ZNK3sce16CommonDialogUtil6Server10isCloseReqEv](../../../core/libs/prx/libSceCdlgUtilServer/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCdlgUtilServer.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZNK3sce16CommonDialogUtil6Server14getServerStateEv](../../../core/libs/prx/libSceCdlgUtilServer/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCdlgUtilServer.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index a28ecaf3cc..3085eb91ea 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -14,7 +14,7 @@ - [Strict JIS X 0208 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables were measured on a PS5 (`sceCesMbcToUtf32` and `sceCesUtf32ToMbc` over every one- and two-byte sequence and every scalar); the errors follow the other JIS profiles, a missing SS2 or SS3 answering `INVALID_ENCODE`. The table is CP932's without rows 13 and 89-92 and with seven cells changed (`0x213D` U+2014 and the six of [JIS0208.TXT](https://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0208.TXT)). Cell `0x2140` is U+005C in `sceCesUcsProfileInitSJis1997X0208`, whose bytes `0x5C` and `0x7E` are U+00A5 and U+203E, and U+FF3C in the EUC-JP profiles, which keep ASCII and also encode U+00A5 and U+203E as `0xA1EF` and `0xA1B1`. Per [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Text/Ces.cs#L57-L67), `X0208Ss2` adds SS2 katakana and `X0208Ss2Ss3` SS3 JIS X 0212 - [UTF-16 and UTF-32 forms of the single-byte and MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSbcTo{Ucs2,Utf16,Utf32}*`, `sceCes{Ucs2,Utf16,Utf32}*ToSbc`, `sceCesMbcsStr{To,Get}{Utf16,Utf32}*` and `sceCes{Utf16,Utf32}Str{To,Get}Mbcs*` are unverified against the console. Signatures follow the Orbital type database linked above. They follow `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the UTF-8 MBCS string forms with the profiles and tables listed above. A surrogate or a value above U+10FFFF answers `INVALID_ENCODE`, a valid code point no profile maps `UNASSIGNED_CODE`. The string forms stop at a zero code unit, write no terminator and count code units - [GBK (CP936) and EUC-CN conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUcsProfileInitGbkCp936` and `sceCesUcsProfileInitEucCnGb2312` mark the caller's sheet with an 8-byte tag; the console's profile layout is not known. Both use one table ([Cp936.hpp](../../../core/libs/prx/libSceCesCs/Cp936.hpp)) read from Microsoft's [CP936.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT); the EUC-CN profile accepts only its `0xA1`-`0xFE` lead and trail bytes, so it includes the GBK cells inside that range and the CP936 forms of `0xA1A4` (U+00B7) and `0xA1AA` (U+2014) instead of GB 2312's. That the console's GB 2312 and CP936 tables equal it is not measured. User-defined areas and `sceCesGbUcsProfileSet*` are not handled. Errors, their precedence and the length pointers follow the Shift-JIS forms above and are unverified -- [GB 18030 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the console's tables are not measured, and the sources disagree on 26 sequences. The 2000 profile follows ICU's `gb-18030-2000.ucm` at icu-data d9d3a6ed (before ICU-8427), which matches Python's codec: `0xA8BC` is U+E7C7 and `0x8135F437` U+1E3F. The 2005 profile follows `gb-18030-2005.ucm` (91850aec), which swaps those two and maps 24 PUA cells to U+FE10-U+FE19, U+9FB4-U+9FBB and six supplementary ideographs, keeping the PUA code points as encode fallbacks. Two-byte cells are CP936 plus [Gb18030.hpp](../../../core/libs/prx/libSceCesCs/Gb18030.hpp); four-byte runs include ICU's `gb18030Ranges`. The inits tag the sheet like GBK's; the `sceCesGb*`, `sceCesMbc*` and MBCS string forms accept them. The full mapping is assumed as the default of `sceCesGbUcsProfileSetUdaMapping` and `Set4ByteCharRange`, which throw. Supplementary characters to UCS-2 answer `UNASSIGNED_CODE`; other errors follow the GBK forms +- [GB 18030 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the errors follow the GBK forms, unmeasured; the tables were measured on a PS5 (`sceCesMbcToUtf32` over every one- to four-byte sequence below `0x8431A530`, sampled and boundary supplementary sequences, `sceCesUtf32ToMbc` over U+0000-U+110001). The 2000 profile matches ICU's `gb-18030-2000.ucm` at icu-data d9d3a6ed (`0xA8BC` U+E7C7, `0x8135F437` U+1E3F) for the BMP and maps no supplementary character. The 2005 profile is the same with those two swapped, keeps the 24 PUA cells that `gb-18030-2005.ucm` remaps, and maps only plane 2 (U+20000-U+2FFFF, `0x95328236`-`0x9A348431`) above the BMP. Two-byte cells are CP936 plus [Gb18030.hpp](../../../core/libs/prx/libSceCesCs/Gb18030.hpp). The inits tag the sheet like GBK's. The mapping is assumed as the default of `sceCesGbUcsProfileSetUdaMapping` and `Set4ByteCharRange`, which throw. Supplementary characters to UCS-2 answer `UNASSIGNED_CODE` - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), except bytes 0x81, 0x83, 0x88, 0x90 and 0x98, which are unassigned. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) - [sceCesSbcToUtf8 and sceCesUtf8ToSbc](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) follow a payload process on a PS5 (firmware 13.60): the three tables match the console byte for byte, with Cp1252 bytes 0x81, 0x8D, 0x8F, 0x90 and 0x9D unassigned; a null profile returns `0x805C0004`; and the length and output are cleared first, as the console does. `sceCesUtf8ToSbc` decodes leads up to 0xFD (5- and 6-byte forms), returns `0x805C0015` for overlong forms and surrogates and reports the measured lengths. The console reads any other profile pointer as a table, where these return the invalid-parameter error; which of a null profile and a scalar above U+10FFFF it reports first is not measured - [MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUtf8StrToMbcsStr`, `sceCesUcs2StrToMbcsStr`, `sceCesMbcsStrToUtf8Str`, `sceCesMbcsStrToUcs2Str` and their `Get*Len` forms follow a payload process on a PS5 (firmware 13.60) with the Cp1252 and EUC-JP profiles: the lengths are cleared and a terminator is written first, then the context, the source and the output are checked in that order; a source limit of 0 reads up to the terminating zero; the output keeps room for a terminator; and on an error the characters before it stay converted, terminated and counted. They decode UTF-8 as `sceCesUtf8ToSbc` does, and a UTF-16 surrogate answers `0x805C0015`. The UCS-2 forms and the other profiles were not measured beyond these cases. A zero-filled context converted nothing and returned 0 on the console, where this returns the invalid-parameter error diff --git a/docs/dev/debt/libSceCommonDialog.md b/docs/dev/debt/libSceCommonDialog.md index f46aaf0394..505bb357a0 100644 --- a/docs/dev/debt/libSceCommonDialog.md +++ b/docs/dev/debt/libSceCommonDialog.md @@ -7,3 +7,4 @@ ### Functional - [sceCommonDialogInitialize](../../../core/libs/prx/libSceCommonDialog/Export.cpp) (libSceCommonDialog) - returning `ALREADY_SYSTEM_INITIALIZED` (`0x80B80002`) from a repeated call is not measured on a console by the project. A third-party PS5 log ([obSCEne](https://github.com/project-oops/obSCEne/blob/67b4c529eb3c271184117e26bb54330531ef2cf6/reports/hardware/report-20261014-004800.log#L260-L261), firmware 12.40) records two calls, `0x0` and then `0x80b80002`; a third call is not covered. The SDK error names, shadPS4 (a PS4 emulator) and SharpProspero give the same value; PPSA09695 compares the result with `0x80b80002` (call site 0x5F7BD0) and PPSA06327 stops on `0x80b80004`. Closes when: measured on a PS5, including a third call. +- [sceCommonDialogIsUsed](../../../core/libs/prx/libSceCommonDialog/Export.cpp) (libSceCommonDialog) - reports whether a common dialog holds the system, like shadPS4's `g_isUsed`. Only [libSceInvitationDialog](libSceInvitationDialog.md) takes and frees it so far, through `CommonDialogAcquire_nid_no_patch` and `CommonDialogRelease_nid_no_patch`, so a dialog of another library that is in use is not seen. Not measured on a console. diff --git a/docs/dev/debt/libSceContentDelete.md b/docs/dev/debt/libSceContentDelete.md index 37b90deeae..55f8bf74b5 100644 --- a/docs/dev/debt/libSceContentDelete.md +++ b/docs/dev/debt/libSceContentDelete.md @@ -2,7 +2,7 @@ ### Functional -- [sceContentDeleteById](../../../core/libs/prx/libSceContentDelete/Export.cpp) and `sceContentDeleteByPath`: content database access and deletion are not implemented. Calls with valid arguments after initialization throw; the backend result cannot be inferred from an empty host library. Null paths after initialization throw instead of reproducing the original unchecked string access. +- [sceContentDeleteById](../../../core/libs/prx/libSceContentDelete/Export.cpp) and `sceContentDeleteByPath`: content database access and deletion are not implemented. Calls with valid arguments after initialization go through `NotImplemented_nid_no_patch`: the console's error for content that is not in the library is unknown, and it cannot be inferred from an empty host library. Null paths after initialization throw instead of reproducing the original unchecked string access. - [sceContentDeleteInitialize](../../../core/libs/prx/libSceContentDelete/Export.cpp): validates the confirmed 16 KiB heap requirement and lifecycle errors, but does not create the original allocator, mutex and database client. Initialization-parameter fields other than the 64-bit heap size at offset 8 remain unresolved. ### Unknown function info diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 69251a837f..1b5466bbaf 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -22,6 +22,7 @@ - [sceFontGraphicsDrawupFillTextureImageObject, sceFontGraphicsDrawupFillTexturePatternObject](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) (libSceFont) - still throwing. Both have bodies in the 4.03 `libSceFont.sprx` (670 and 871 bytes); the signature `(graphicsObject, fillObject, mode, out, x, y, w, h)` with an `int` error return is confirmed two ways. The body's own validation path: `out` is zeroed up front, the graphics object is checked for magic word `0xf53` at +0 and stores the error at +4 (`0x80460084` null object, `0x80460002` invalid arguments, `0x80460001` no renderer, `0x8046005c` unbound buffer, `0x804600a2` empty pool). PPSA20397's `libSceFontGsm.prx` caller confirms the argument construction: it passes the embedded object at `graphicsObject+0x28`, the mode byte at `+0xf`, and its own `out`/rect arguments verbatim after checking `[graphics+0xd]==0x58`, `[graphics+0x38]==1` and the rect against a float bound. On valid input the body takes the renderer's record pool (`graphicsObject+8` -> `+8` -> `+0x160`), allocates a 0x40+0x80 drawup record (command byte 0x40, then the rect floats and mode), enqueues it into the object's record list (+0x28/+0x30 with used/maximum counters at +0x10/+0x14) and returns it through `out`. Implementing it needs the `sceFontGraphics` drawup pipeline it feeds - the record pool, the dequeueing objects and the ~40 sibling `sceFontGraphics*` exports - all of which are stubbed, so a standalone producer would report success for work nothing consumes - [sceFontDefineAttribute, sceFontGetAttribute](../../../core/libs/prx/libSceFont/src/Style.cpp), [sceFontGlyphDefineAttribute, sceFontGlyphGetAttribute](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - the signatures `(handle, int attribute, int* oldAttribute)` and `(handle, int attribute, int* nowAttribute)`, and the same shape for a glyph, follow the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, matching the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1249-L1264), not confirmed on a title. Every handle and every glyph keeps one attribute, an opaque int whose meaning and valid values are unknown: define stores the new value and returns the previous one, and get returns the stored value with its `attribute` argument unused, as in KytyPS5. An attribute never set is 0. The null checks and the invalid-object errors follow the other libSceFont functions and diverge from [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1251-L1265), which returns `OK` for a null handle or glyph after writing 0 to the old-attribute pointer: every other handle and glyph entry point in this library reports the invalid object instead, and a title that passes one is already failing. shadPS4 leaves both exports unimplemented, so there is no second implementation to compare. PPSA01325 calls the glyph define with 0x11 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused and draws with `sceFontRenderCharGlyphImageHorizontal`; the three bindings all read the third argument as an out pointer, not a value, so this call sets the glyph's attribute to 0x11. The attribute changes neither shaping nor rendering. +- [sceFontGlyphRenderImage, sceFontGlyphRenderImageHorizontal, sceFontGlyphRenderImageVertical](../../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - follow firmware 13.42's module as BunnyFl ran it natively ([comment](https://github.com/boykopovar/AnyPS5/pull/10892#issuecomment-6415399922); one font, 'A', one pixel format, not a console): the glyph's character is drawn as `sceFontRenderCharGlyphImage` and its directional forms draw it, at the glyph's generation scale; the surface's style frame wins over the argument, and a frame that sets no scale keeps that scale. A valid `sceFontGlyphRenderImageVertical` call, and `sceFontGlyphRenderImage` on a vertical font, throw: the module reports advance and stride 40/40 there where the char call gives 90/90 at 40 px, and one case doesn't name the rule. A pointer that isn't a renderer, and `SCE_FONT_ERROR_INVALID_FONT_HANDLE` for a closed owner font, are not measured ### Functional @@ -39,7 +40,7 @@ Frame lifecycle, drawing completion, device usage, frame policy, clipping and fill setup/update argument signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Frame and resource layouts remain opaque; their runtime implementation is still unavailable. -Glyph image rendering uses six pointer arguments and two scalar coordinates, with existing style, surface, metrics and output layouts. Positioning takes a mask and two floats. Control, word-character search and resource release/exchange/render signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered; control payload types depend on the command and remain unmodeled. +Positioning takes a mask and two floats. Control, word-character search and resource release/exchange/render signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered; control payload types depend on the command and remain unmodeled. Canvas, design, surface texture and fill-pattern construction signatures in [Unimplemented.cpp](../../../core/libs/prx/libSceFont/src/Unimplemented.cpp) are recovered. Pattern mode/count semantics and the graphics surface descriptor fields still require reconstruction; distinct opaque types preserve the confirmed object categories. diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index 205ee0df69..6542a65909 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -17,7 +17,7 @@ - [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_Bitmap_New, FT_Bitmap_Copy, FT_Bitmap_Convert, FT_Bitmap_Done](../../../core/libs/prx/libSceFreeType/src/Bitmap.cpp) (libSceFreeType) - console behaviour and error codes are unmeasured; these follow bundled FreeType 2.13.3. Copy and Convert throw when source and target share a non-null buffer; Convert also throws when they are the same object. Other overlapping storage remains forbidden by the native API contract +- [FT_Bitmap_Init, FT_Bitmap_New, FT_Bitmap_Copy, FT_Bitmap_Convert, FT_Bitmap_Done](../../../core/libs/prx/libSceFreeType/src/Bitmap.cpp) (libSceFreeType) - console behaviour and error codes are unmeasured; these follow bundled FreeType 2.13.3. Copy and Convert throw when source and target share a non-null buffer; Convert also throws when they are the same object. Other overlapping storage remains forbidden by the native API contract - [FT_Request_Size](../../../core/libs/prx/libSceFreeType/src/SizeRequest.cpp) (libSceFreeType) - console signature, request layout and errors are unverified; the guest request follows the LP64 public headers of the bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/include/freetype/freetype.h). Host overflow behavior remains that of the bundled build. On Windows, native `FT_Long` is 32 bits; valid requests whose width or height exceeds that range throw before changing the size. @@ -29,12 +29,17 @@ - [FT_Get_Charmap_Index, FT_Get_CMap_Language_ID, FT_Get_CMap_Format](../../../core/libs/prx/libSceFreeType/src/Charmap.cpp) (libSceFreeType) - console signatures and return values are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c) and the existing guest LP64 model. Charmaps must be the face's original read-only guest records; copied or otherwise unregistered records with a non-null face throw instead of reading private native cmap fields from guest memory +- [FT_Property_Get, FT_Property_Set](../../../core/libs/prx/libSceFreeType/src/ModuleProperties.cpp) - console signatures, property availability and backend/error equivalence are unverified. Only `truetype/interpreter-version`, the `cff`, `type1` and `t1cid` hinting-engine, random-seed, no-stem-darkening and darkening-parameters properties, and the autofitter no-stem-darkening and darkening-parameters properties follow bundled FreeType 2.13.3. Other module/property pairs throw, including properties carrying face pointers or callbacks. Values use 32-bit signed/unsigned integers, one-byte booleans or eight 32-bit darkening parameters; getters copy only successful native results. + - [FT_Outline_Reverse, FT_Outline_Translate](../../../core/libs/prx/libSceFreeType/src/Outline.cpp) (libSceFreeType) - console signatures, count signedness and overflow behavior are unverified; the adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) with LP64 coordinates. Translation uses 64-bit modular addition to match that build's `ADD_LONG`; reversal preserves each contour's first point and toggles the reverse-fill flag. Coordinates outside the host `FT_Pos` range still throw if the modified outline is later rendered on Windows - [FT_Get_X11_Font_Format, FT_Get_FSType_Flags](../../../core/libs/prx/libSceFreeType/src/FaceInfo.cpp) (libSceFreeType) - console signatures, format labels and embedding-flag behavior are unverified; the adapter follows bundled FreeType 2.13.3's [format](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfntfmt.c) and [FSType](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftfstype.c) services. Format strings borrow native driver storage; the console's string lifetime has not been measured - [FT_Get_Kerning](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - the name and NID `LPtlB241JSg` occur in the [PS5 3.20 libSceFreeTypeOt inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c), but the console signature and kerning results are unverified. The adapter assumes the existing LP64 guest layout and follows bundled FreeType 2.13.3 for scaling, rounding and errors ### Functional +- [FT_Sfnt_Table_Info and FT_Load_Sfnt_Table](../../../core/libs/prx/libSceFreeType/src/Sfnt.cpp) (libSceFreeType) - console ABI and errors are unverified; the LP64 adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/sfnt/ttload.c). On hosts with a 32-bit `FT_Long`, tagged reads through a native stream callback throw: their table offsets cannot be recovered without changing callback state. Memory-backed streams and whole-file reads are supported; current guest face constructors use memory-backed streams. + +- [FT_Outline_Get_BBox](../../../core/libs/prx/libSceFreeType/src/CurveBounds.cpp) has no console ABI or behaviour verification; extrema and errors follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftbbox.c). Curved outlines with any coordinate outside ±536870911 throw: conic differences and their sum can overflow native 32-bit arithmetic even when each input fits `FT_Pos`. Coordinates outside native `FT_Pos` (signed 32 bits on Windows) and missing nonempty arrays or inconsistent contour partitions also throw. Native output uses a local box; errors and unsupported inputs leave the guest output unchanged. Empty outlines do not inspect their arrays. - [FT_Set_Transform](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, transform arithmetic and FreeType-version equivalence are unverified. The name and NID `bcM5xfX2SeA` come from the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the 32-byte LP64 matrix, 16-byte vector, null defaults and glyph behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c). Input components outside the native `FT_Long` range throw without changing the transform; on Windows this rejects values outside signed 32 bits. Native arithmetic limits remain those of the host library. diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 6e93bdf35f..a0f692fcaa 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 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 import-resolution helpers in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) retain the PS5 3.20 NIDs from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs), including identified names and still-unidentified exports. Their provider-specific 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, 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. diff --git a/docs/dev/debt/libSceGvMp4Parser.md b/docs/dev/debt/libSceGvMp4Parser.md new file mode 100644 index 0000000000..80098dde53 --- /dev/null +++ b/docs/dev/debt/libSceGvMp4Parser.md @@ -0,0 +1,6 @@ +# libSceGvMp4Parser technical debt + +### Unknown function info + +- [_ZN23sceMetadataReaderWriter11gvMp4Parser19gvMp4ParserFinalizeEv, _ZN23sceMetadataReaderWriter11gvMp4Parser21gvMp4ParserInitializeEv, _ZN23sceMetadataReaderWriter11gvMp4Parser23gvMp4ParserReadMetadataERKSsRKNS_8MetadataERS3_, _ZN23sceMetadataReaderWriter11gvMp4Parser24gvMp4ParserWriteMetadataERKSsRKNS_8MetadataE, _ZN23sceMetadataReaderWriter11gvMp4Parser27gvMp4ParserExtractThumbnailERKSsRNS_9ThumbnailE](../../../core/libs/prx/libSceGvMp4Parser/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceGvMp4Parser.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter11gvMp4Parser21gvMp4ParserInitializeEv, _ZN23sceMetadataReaderWriter11gvMp4Parser23gvMp4ParserReadMetadataERKSsRKNS_8MetadataERS3_, _ZN23sceMetadataReaderWriter11gvMp4Parser24gvMp4ParserWriteMetadataERKSsRKNS_8MetadataE, _ZN23sceMetadataReaderWriter11gvMp4Parser27gvMp4ParserExtractThumbnailERKSsRNS_9ThumbnailE](../../../core/libs/prx/libSceGvMp4Parser/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. diff --git a/docs/dev/debt/libSceHidControl.md b/docs/dev/debt/libSceHidControl.md index fc779520a0..372680766f 100644 --- a/docs/dev/debt/libSceHidControl.md +++ b/docs/dev/debt/libSceHidControl.md @@ -2,6 +2,13 @@ ### Unknown function info +- [sceHidControlConnectHandle, sceHidControlConnectPort, sceHidControlDisconnectDevice, sceHidControlDisconnectHandle, sceHidControlDisconnectPort](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceHidControlGetAuthInformation, sceHidControlGetBatteryState, sceHidControlGetDeviceId, sceHidControlGetDeviceInfo, sceHidControlGetDeviceName](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceHidControlGetJediExtensionUnitInfo, sceHidControlGetJediState, sceHidControlGetSparkState, sceHidControlGetVersionInfo, sceHidControlInit](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceHidControlSetApplicationFocus, sceHidControlSetHandleState, sceHidControlSetJediAudioOutPath, sceHidControlSetJediVolumeGain, sceHidControlSetProcessFocus](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceHidControlSetRevokeList, sceHidControlSetRevokeList2, sceHidControlSetUserColor](../../../core/libs/prx/libSceHidControl/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceHidControl.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [sceHidControlSetRevokeList](../../../core/libs/prx/libSceHidControl/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. + - [sceHidControlEnableMicBeamForming](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature - [sceHidControlSetAllVibrationMute](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature - [sceHidControlSetVibrationTriggerEffectWeakForce](../../../core/libs/prx/libSceHidControl/Export.cpp) (libSceHidControl) - unknown signature diff --git a/docs/dev/debt/libSceIcu.md b/docs/dev/debt/libSceIcu.md index dcf66e6027..3623dfec51 100644 --- a/docs/dev/debt/libSceIcu.md +++ b/docs/dev/debt/libSceIcu.md @@ -1,5 +1,11 @@ # libSceIcu technical debt +### Unknown function info + +- [ICU export availability](../../../core/libs/prx/libSceIcu/Unescape.cpp) across firmware versions remains unproven. The [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c) includes the implemented escape, invariant-character and version helpers. [BunnyFl’s firmware 13.42 report](https://github.com/boykopovar/AnyPS5/pull/10961#issuecomment-6400071863) records `u_getVersion` returning `78.1.0.0` under an emulator, but neither escape function is in the checked native `system_ex/common_ex/lib/libSceIcu.sprx` export table. Only 104 of the 661 inventory NIDs appear among that module’s 299 exports. The exports for compatibility with older firmware are retained; availability and routing across firmware versions remain unproven. + ### Functional +- [Escape decoding](../../../core/libs/prx/libSceIcu/Unescape.cpp) has no behavior or ABI verification on console hardware. It follows ICU 78.1's [ustring.cpp](https://github.com/unicode-org/icu/blob/049e0d6a420629ac7db77256987d083a563287b5/icu4c/source/common/ustring.cpp), including rejection of eight-digit braced hexadecimal escapes although its public header advertises up to eight digits. Null source or offset pointers, null callbacks with an in-range offset, negative capacity with a nonnull destination and source lengths exceeding `INT32_MAX` throw. Null-destination preflight ignores capacity. Closes when: compared with the console, including callback ABI, byte conversion, capacity limits and braced hexadecimal parsing. + - [Invariant character and version helpers](../../../core/libs/prx/libSceIcu/Version.cpp) are unverified on a console. `u_charsToUChars`, `u_UCharsToChars`, `u_versionFromString`, `u_versionFromUString` and `u_versionToString` follow ICU 78.1 [uinvchar.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/uinvchar.cpp) and [putil.cpp](https://github.com/unicode-org/icu/blob/release-78.1/icu4c/source/common/putil.cpp), unchanged since 65.1. Firmware 13.42 reports ICU 78.1 and exports none of them ([#11013](https://github.com/boykopovar/AnyPS5/pull/11013#issuecomment-6400470698)); the [3.20 table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceIcu.c) has all five. Version parts use the guest `strtoul`, truncated to eight bits; `u_versionToString` keeps ICU's dropped zero tens digit (105 becomes `15`). A null buffer with a positive conversion length throws; ICU dereferences it. Closes when: compared with the console. diff --git a/docs/dev/debt/libSceInvitationDialog.md b/docs/dev/debt/libSceInvitationDialog.md index 9038d9da62..9f2880d0fa 100644 --- a/docs/dev/debt/libSceInvitationDialog.md +++ b/docs/dev/debt/libSceInvitationDialog.md @@ -1,6 +1,14 @@ # libSceInvitationDialog technical debt +### Silent stubs + +- [sceInvitationDialogOpen, sceInvitationDialogGetResult](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog, PS4) - no dialog is shown and no invitation is sent: `Open` moves the dialog straight to `FINISHED`, and `GetResult` returns 0 with `errorCode` 0, `result` `USER_CANCELED` (1), the `callbackArg` of the last `Open` and a recipient count of 0, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/2cdefaa1298c6e4ece6c74bd84e90f3906282d51/src/core/libraries/invitation_dialog/invitation_dialog.cpp) reports when its window is dismissed and as [libSceNpProfileDialog](libSceNpProfileDialog.md) does. Not measured on a console. + ### Unknown function info -- [sceInvitationDialogGetResult](../../../core/libs/prx/libSceInvitationDialog/include/Api.hpp) - still throws; SDK field names and types at offsets 0x00 and 0x0C are unknown. In the supplied `libSceInvitationDialog.sprx`, export `8XKR6wa64iQ` at 0xFF0 takes one result pointer, checks reserved bytes 0x18..0x37 and writes the returned status at 0x08. The helper at 0x410 copies eight bytes from the saved open parameter to 0x00 and eight result bytes to 0x08, then copies 20-byte online IDs through the pointer at 0x10, after a four-byte count. Validation at 0xC20 limits recipients to 1..16. `InvitationDialogRecipients` provides the accessed 324-byte storage; any unused SDK tail is unverified. `InvitationDialogResult` provides the accessed 56 bytes, with unknown fields kept opaque; this is not a recovered SDK source declaration. -- [sceInvitationDialogClose](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws +- [sceInvitationDialogOpen, sceInvitationDialogGetResult](../../../core/libs/prx/libSceInvitationDialog/include/Api.hpp) (libSceInvitationDialog, PS4) - the field names and the 144-byte `InvitationDialogParam` come from the [shadPS4 header](https://github.com/shadps4-emu/shadPS4/blob/2cdefaa1298c6e4ece6c74bd84e90f3906282d51/src/core/libraries/invitation_dialog/invitation_dialog.h). They match the supplied `libSceInvitationDialog.sprx`: export `8XKR6wa64iQ` at 0xFF0 checks the reserved bytes 0x18..0x37 of the result and writes its status at 0x08, and the helper at 0x410 copies eight bytes of the saved open parameter to 0x00, the eight result bytes to 0x08 and up to 16 20-byte online ids through the pointer at 0x10, after a four-byte count. A param whose `size` is not 144, or whose `mode` is neither 1 (send) nor 2 (receive), throws, and so does a result with a non-zero reserved byte. `baseParam`, `userId` and `dataParam`, with its 1 to 16 recipients, are not checked. +- [sceInvitationDialogOpen, sceInvitationDialogClose, sceInvitationDialogGetResult](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog, PS4) - two paths throw because shadPS4 and the other common dialogs here return different errors: `Open` before `Initialize` (shadPS4 `INVALID_STATE`, the Signin family `NOT_INITIALIZED`) and `Close` on a dialog that is not running, which is every `Close` after `Initialize` here (shadPS4 `NOT_RUNNING`, the Signin family 0). `GetResult` before the dialog finishes returns `NOT_FINISHED` even for a null result, as in shadPS4. shadPS4 returns the `result` value from `GetResult`; this returns 0, which the module writes at 0x08 as `errorCode`. + +### Functional + +- [sceInvitationDialogInitialize, sceInvitationDialogTerminate](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog, PS4) - `Initialize` returns `NOT_SYSTEM_INITIALIZED` before `sceCommonDialogInitialize`, then `ALREADY_INITIALIZED`, then `BUSY` while another dialog holds the common dialog system, in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/2cdefaa1298c6e4ece6c74bd84e90f3906282d51/src/core/libraries/invitation_dialog/invitation_dialog.cpp)'s order, and `Terminate` frees the system. Only this library takes the system through [libSceCommonDialog](libSceCommonDialog.md) so far, so the other common dialogs neither return `BUSY` because of it nor make it return `BUSY`. Not measured on a console. diff --git a/docs/dev/debt/libSceJpegParser.md b/docs/dev/debt/libSceJpegParser.md new file mode 100644 index 0000000000..842286c314 --- /dev/null +++ b/docs/dev/debt/libSceJpegParser.md @@ -0,0 +1,6 @@ +# libSceJpegParser technical debt + +### Unknown function info + +- [_ZN23sceMetadataReaderWriter10jpegParser18jpegParserFinalizeEv, _ZN23sceMetadataReaderWriter10jpegParser20jpegParserInitializeEv, _ZN23sceMetadataReaderWriter10jpegParser22jpegParserReadMetadataERKSsRKNS_8MetadataERS3_, _ZN23sceMetadataReaderWriter10jpegParser23jpegParserWriteMetadataERKSsRKNS_8MetadataE, _ZN23sceMetadataReaderWriter10jpegParser26jpegParserExtractThumbnailERKSsRNS_9ThumbnailE](../../../core/libs/prx/libSceJpegParser/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJpegParser.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter10jpegParser20jpegParserInitializeEv, _ZN23sceMetadataReaderWriter10jpegParser22jpegParserReadMetadataERKSsRKNS_8MetadataERS3_, _ZN23sceMetadataReaderWriter10jpegParser23jpegParserWriteMetadataERKSsRKNS_8MetadataE, _ZN23sceMetadataReaderWriter10jpegParser26jpegParserExtractThumbnailERKSsRNS_9ThumbnailE](../../../core/libs/prx/libSceJpegParser/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. diff --git a/docs/dev/debt/libSceJsc.md b/docs/dev/debt/libSceJsc.md new file mode 100644 index 0000000000..ca4325c7b8 --- /dev/null +++ b/docs/dev/debt/libSceJsc.md @@ -0,0 +1,9 @@ +# libSceJsc technical debt + +### Unknown function info + +- [JSString ABI and ownership](../../../core/libs/prx/libSceJsc/Export.cpp): the console JavaScriptCore version, allocation policy, opaque object layout and thread initialization remain unverified. These host objects cannot be passed to a title-supplied native JavaScriptCore module. The ten names occur in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceJsc.c); ownership and signatures follow pinned WebKit [JSStringRef.cpp](https://github.com/WebKit/WebKit/blob/f274ef4c03ebace644c46f8ec55746b391acdcc8/Source/JavaScriptCore/API/JSStringRef.cpp). Null retain/release or maximum-size handles, nonempty null UTF-16 buffers, counts above INT32_MAX and reference-count exhaustion throw. The allocation-header-dependent limit below INT32_MAX is not reproduced. Other JavaScriptCore APIs remain unavailable. + +### Functional + +- [JSString conversion](../../../core/libs/prx/libSceJsc/Export.cpp): console behavior is unverified. UTF-16 storage, null/empty distinction and partial UTF-8 output follow pinned WebKit [OpaqueJSString.cpp](https://github.com/WebKit/WebKit/blob/f274ef4c03ebace644c46f8ec55746b391acdcc8/Source/JavaScriptCore/API/OpaqueJSString.cpp) and [UTF8Conversion.cpp](https://github.com/WebKit/WebKit/blob/f274ef4c03ebace644c46f8ec55746b391acdcc8/Source/WTF/wtf/unicode/UTF8Conversion.cpp). Malformed UTF-8 creates a null string, distinct from an empty string; output stops before an unpaired UTF-16 surrogate. Null UTF-8 inputs and unrepresentable maximum output sizes throw. diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 723c8f3d9d..667180532f 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -1,8 +1,12 @@ # libSceLibreSsl technical debt +### Functional + +- [SSL_select_next_proto](../../../core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp) leaves empty lists, zero-length names and truncated encodings unsupported and throws instead of following the reported firmware behavior. Null outputs also throw; their firmware behavior is unmeasured. [Independent firmware 13.42 evidence](https://github.com/boykopovar/AnyPS5/pull/9785#issuecomment-6406712390) reports host-PC execution of `libSceLibreSsl.sprx` at `0xda6d0`, with matching six-argument valid-list selection, status, borrowed pointer and length. That report also observes negotiation of zero-length entries, unchecked fallback lengths and an empty-supported-list read; these paths remain unsupported here. Null outputs and console-hardware behavior remain unmeasured. The author-sourced signature and valid-list contract follow LibreSSL 3.4.1. + ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 319 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 318 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -15,11 +19,12 @@ - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. - [sk_num, sk_value, sk_pop, sk_pop_free](../../../core/libs/prx/libSceLibreSsl/src/Stack.cpp) (libSceLibreSsl) - the 32-byte `_STACK` layout (`num`, `data`, `sorted`, `num_alloc`, `comp`) and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/stack) `stack.h` and `stack.c`, the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214); nothing is checked on the console. `sk_pop_free` releases the element array and the stack with the guest `free`, as the module would through libc. No export of this module creates a stack yet, so only stacks the caller builds reach them. A null element array that LibreSSL would read through, and a null free function with a non-null element, throw where LibreSSL faults - [RSA_flags, RSA_size](../../../core/libs/prx/libSceLibreSsl/src/Rsa.cpp) (libSceLibreSsl) - nothing is checked on the console. Layouts and results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto) `rsa.h`, `rsa_crpt.c` and `bn_lib.c`, the version in the [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export of this module creates a key yet, so only keys the caller builds reach them. `RSA_size` counts the modulus bytes from its top word, as `BN_num_bytes`, so a zero top word is not skipped. A null method, a null key or modulus, and a positive word count with null words, which LibreSSL dereferences, throw, as do a negative word count and a bit count that overflows `int` +- [EVP_PKEY_type](../../../core/libs/prx/libSceLibreSsl/src/Pkey.cpp) (libSceLibreSsl) - follows the method table of [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/ameth_lib.c#L83-L111) `ameth_lib.c` built without GOST and with no engine (every `ENGINE_` export throws): RSA (6) and its alias 19 give 6, the DSA types 66, 67, 70, 113 and 116 give 116, DH (28), EC (408), HMAC (855) and CMAC (894) give themselves, and every other type, the GOST types 811, 815, 946 and 947 included, gives 0. These results match [firmware 13.42's module run natively on a PC](https://github.com/boykopovar/AnyPS5/pull/10398#issuecomment-6406072913) for every type from -1000 to 5000 and the `int` extremes; nothing is checked on the console ### Functional - [ASN1_INTEGER_get](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) has no console verification for nonpositive lengths. It follows [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/a_int.c#L363-L389): valid integer types return zero without reading data when length is zero or negative; invalid types return -1 first. Later LibreSSL versions reject negative lengths through `ASN1_INTEGER_get_int64`; that later contract is not used for this library. -- [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and unrepresentable signed integer values throw where the source has undefined behaviour. Malformed encodings and allocation failure throw until ASN.1 error-queue reporting is implemented. Conversion buffers use the guest libc allocator and can be released with guest `free`. +- [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. They follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). These throw where 2.5.4 has undefined behaviour: a null structure passed to `ASN1_STRING_data`, `ASN1_STRING_length` or `ASN1_STRING_type`, a null conversion output, missing nonempty data, oversized converted output and unrepresentable signed integers. As in 2.5.4, `ASN1_INTEGER_get(NULL)` returns 0 and `ASN1_STRING_to_UTF8` returns -1 for a null string, leaving the output alone; malformed input and allocation failure also return -1 and queue the ASN.1 error (129, 133, 134, 124, or 65, twice for a UTF8String copy). Conversion buffers come from the guest libc allocator. - [SSL_alert_desc_string_long](../../../core/libs/prx/libSceLibreSsl/Alert.cpp) - Console execution and equivalence across other firmware revisions remain unverified. BunnyFl reports identical strings for 3,841 inputs across three runs against the firmware 13.42 module executed natively on a PC ([comparison](https://github.com/boykopovar/AnyPS5/pull/9870#issuecomment-6406774281)); this is not a console run. The mapping follows LibreSSL 2.5.4. diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md index fb9e7fe201..6607b4c8c5 100644 --- a/docs/dev/debt/libSceLibreSsl3.md +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -2,10 +2,11 @@ ### Unknown function info -- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 83 named callable exports, excluding `CRYPTO_memcmp`, `MD5`, `SHA1` and `d2i_ASN1_INTEGER`, remain unimplemented from the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c), cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl3 pending exports](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) - 81 named callable exports, excluding `CRYPTO_memcmp`, `MD5`, `SHA1`, `d2i_ASN1_INTEGER`, `EVP_PKEY_up_ref` and `EC_KEY_up_ref`, remain unimplemented from the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl3.c), cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. ### Functional - [ASN.1 integer decoding](../../../core/libs/prx/libSceLibreSsl3/src/IntegerDecoder.cpp) throws on rejected encodings/allocation failures instead of matching native NULL/error handling. Redundant-sign/empty-content rejection retains old output that the module frees and clears. The decoder does not populate the legacy queue; remaining LibreSSL3 ERR exports throw. [13.42 native-PC evidence](https://github.com/boykopovar/AnyPS5/pull/10948#issuecomment-6412137728), using a recording allocator, confirms tested replacement/free order, allocation cleanup and negative-length NULL returns. Nonnegative null input faults natively and throws here. Console/application-heap ABI and untested error-precedence/overflow combinations remain unverified. Contracts follow [LibreSSL 4.0.0](https://github.com/libressl/openbsd/tree/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/asn1). Closes when: error/replacement semantics and console/allocator interoperability are verified. - [CRYPTO_memcmp](../../../core/libs/prx/libSceLibreSsl3/Export.cpp) follows the public [LibreSSL signature and equality contract](https://man.openbsd.org/CRYPTO_memcmp.3), without PS5 verification. The exact nonzero result for unequal buffers is unspecified by that contract. Content-independent execution of its volatile byte reads still depends on the compiler; inspect optimized code when changing toolchains. Closes when: measured on a PS5 and the supported toolchains' generated code is checked. - [MD5 and SHA1](../../../core/libs/prx/libSceLibreSsl3/src/Digest.cpp) (libSceLibreSsl3) - follow LibreSSL's [md5.c, sha1.c](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto) and hash through libSceLibreSsl's `MD5_*` and `SHA1_*`. A null digest buffer, which LibreSSL 4.0.0 does not check, throws. Nothing is checked on the console +- [EVP_PKEY_up_ref and EC_KEY_up_ref](../../../core/libs/prx/libSceLibreSsl3/src/Key.cpp) - console layouts and retain/release interoperability are unverified. The 32-byte EVP and 64-byte EC layouts, with counters at offsets 4 and 48, follow [LibreSSL 4.0.0](https://github.com/libressl/openbsd/tree/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto) `evp/evp_local.h`, `evp/p_lib.c`, `ec/ec_local.h` and `ec/ec_key.c`. Atomic retains replace its internal mutex; future releases must synchronize with the same counters. With no key constructors/destructors in this module, tests use caller-owned objects without validating allocator or destruction compatibility. Null keys and signed-counter overflow, undefined upstream, throw without mutation. Closes when: console layouts and lifetime interoperability are measured. diff --git a/docs/dev/debt/libSceMetadataReaderWriter.md b/docs/dev/debt/libSceMetadataReaderWriter.md new file mode 100644 index 0000000000..d0c5c103e9 --- /dev/null +++ b/docs/dev/debt/libSceMetadataReaderWriter.md @@ -0,0 +1,16 @@ +# libSceMetadataReaderWriter technical debt + +### Unknown function info + +- [_ZN23sceMetadataReaderWriter10initializeEv, _ZN23sceMetadataReaderWriter11getMimeTypeERKSsRSt6vectorIjSaIjEE, _ZN23sceMetadataReaderWriter12getMimeTypesERKSsiPi, _ZN23sceMetadataReaderWriter12readMetadataERKSsRKNS_8MetadataEjRS2_, _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEj](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEjPS2_, _ZN23sceMetadataReaderWriter15ParserInterfaceC1ERKS0_, _ZN23sceMetadataReaderWriter15ParserInterfaceC2ERKS0_, _ZN23sceMetadataReaderWriter16extractThumbnailERKSsjRNS_9ThumbnailE, _ZN23sceMetadataReaderWriter16getNumOfMimeTypeERKSsPi](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter17getParserInfoListERSt6vectorINS_10ParserInfoESaIS1_EE, _ZN23sceMetadataReaderWriter18registerParserInfoERKSt6vectorINS_10ParserInfoESaIS1_EE, _ZN23sceMetadataReaderWriter20unregisterParserInfoEv, _ZN23sceMetadataReaderWriter23registerParserInterfaceEjRKNS_15ParserInterfaceE, _ZN23sceMetadataReaderWriter5ValueaSERS0_](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter8Metadata11DeserializeERN3sce4Json5ValueERS0_, _ZN23sceMetadataReaderWriter8Metadata11deleteFieldERKSs, _ZN23sceMetadataReaderWriter8Metadata15iterateAllFieldEPFiRKSsRKNS_5ValueEbE, _ZN23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueERb, _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSs](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter8Metadata8setFieldERKSsRKNS_5ValueE, _ZN23sceMetadataReaderWriter8Metadata9SerializeERS0_RN3sce4Json5ValueE, _ZN23sceMetadataReaderWriter8MetadataC1EPFPvmEPFvS1_E, _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_, _ZN23sceMetadataReaderWriter8MetadataC1ERKS0_PFPvmEPFvS3_E](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter8MetadataC1Ev, _ZN23sceMetadataReaderWriter8MetadataC2EPFPvmEPFvS1_E, _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_, _ZN23sceMetadataReaderWriter8MetadataC2ERKS0_PFPvmEPFvS3_E, _ZN23sceMetadataReaderWriter8MetadataC2Ev](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter8MetadataD0Ev, _ZN23sceMetadataReaderWriter8MetadataD1Ev, _ZN23sceMetadataReaderWriter8MetadataD2Ev, _ZN23sceMetadataReaderWriter8MetadataaSERKS0_, _ZN23sceMetadataReaderWriter8finalizeEv](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZNK23sceMetadataReaderWriter8Metadata13getFieldCountEv, _ZNK23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueE, _ZNK23sceMetadataReaderWriter8Metadata8getValueERKSsRNS_5ValueE, _ZNK23sceMetadataReaderWriter8Metadata9dumpValueEv](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceMetadataReaderWriter.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN23sceMetadataReaderWriter10initializeEv, _ZN23sceMetadataReaderWriter11getMimeTypeERKSsRSt6vectorIjSaIjEE, _ZN23sceMetadataReaderWriter12getMimeTypesERKSsiPi, _ZN23sceMetadataReaderWriter12readMetadataERKSsRKNS_8MetadataEjRS2_, _ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEj](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. +- [_ZN23sceMetadataReaderWriter13writeMetadataERKSsRKNS_8MetadataEjPS2_, _ZN23sceMetadataReaderWriter16extractThumbnailERKSsjRNS_9ThumbnailE, _ZN23sceMetadataReaderWriter16getNumOfMimeTypeERKSsPi, _ZN23sceMetadataReaderWriter17getParserInfoListERSt6vectorINS_10ParserInfoESaIS1_EE, _ZN23sceMetadataReaderWriter18registerParserInfoERKSt6vectorINS_10ParserInfoESaIS1_EE](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. +- [_ZN23sceMetadataReaderWriter8Metadata11deleteFieldERKSs, _ZN23sceMetadataReaderWriter8Metadata15iterateAllFieldEPFiRKSsRKNS_5ValueEbE, _ZN23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueERb, _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSs, _ZN23sceMetadataReaderWriter8Metadata8setFieldERKSsRKNS_5ValueE](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. +- [_ZNK23sceMetadataReaderWriter8Metadata8getFieldEiRSsRNS_5ValueE, _ZNK23sceMetadataReaderWriter8Metadata8getValueERKSsRNS_5ValueE](../../../core/libs/prx/libSceMetadataReaderWriter/Export.cpp) - not exported by any firmware 13.42 module, so on 13.42 an import of these NIDs resolves to nothing in the firmware; the 3.20 declarations remain for titles built against older firmware. Not measured here. diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index ef536e17eb..cd4e3f809d 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -1,9 +1,5 @@ # libSceNetCtl technical debt -### Functional - -- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) - NAT discovery is not implemented and throws after native initialization and argument checks. NAT information is 16 bytes with a 32-bit size prefix; the remaining fields are unresolved. - ### Unknown function info - [sceNetCtlScanIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - argument registers and IPC transfer directions are recovered for the info, connection, scan, NAT, bandwidth and Wi-Fi queries. The connection configuration is 1464 bytes, scan options 88, BSSID output 96, scan info 112, IPv6 info 256, internal NAT info 196, bandwidth info 32, bandwidth-estimation info 28 and Wi-Fi info 48. Their field semantics still require the receiving service; incomplete declarations preserve this gap. Scan queries transfer a six-byte BSSID and use a 64-bit in/out buffer size. @@ -11,15 +7,16 @@ - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface with an IPv4 address, preferring the default-route one: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is always reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, and the Ethernet address is the one from `sceNetGetMacAddress`. A missing gateway or DNS server is `0.0.0.0`; the console value is unknown. The DHCP host name (12), PPPoE user name (13), HTTP proxy server (20) and Wi-Fi BSSID and SSID (5, 6) are empty strings and the other Wi-Fi codes (7-10) and the proxy port (21) are 0, as that configuration sets none of them; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware. One title (PPSA02225) calls code 12 at 0x1056f0f, 0xa0387f and 0xa26de5 and code 14 at 0x1056fa1; the meaning of the flag byte it checks is not shown. - [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address - [sceNetCtlGetResult](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - `(int eventType, int* errorCode)` reports no error: event types 1-3 (disconnected to IP obtained, the numbering `sceNetCtlCheckCallback` delivers) write 0 to `errorCode`, as `sceNetCtlGetResultV6` does. The error code a console reports for each event, a disconnection's cause in particular, is unconfirmed. Callers found: PPSA20052 (Worms Armageddon: Anniversary Edition 01.003.000) calls it 2 s into boot, and PPSA24191 (Monster Hunter Wilds) without a network, with event 1 (disconnected) as its first call. A null `errorCode` answers `INVALID_ADDR` and other event types throw, as their error code on a console is unknown -- [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. Callbacks registered with `sceNetCtlRegisterCallbackV6` receive no event, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown +- [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. The host backend has no IPv6 service errors, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown - [sceNetCtlRegisterCallbackForNpToolkit, sceNetCtlUnregisterCallbackForNpToolkit, sceNetCtlCheckCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - a table of eight callbacks apart from the IPv4 and IPv6 ones, with the `(int eventType, void* arg)` callback, follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net_ctl_obj.h), whose NpToolkit check delivers the same events to that table. The three share the register, unregister and dispatch of `sceNetCtlRegisterCallback`, `sceNetCtlUnregisterCallback` and `sceNetCtlCheckCallback`, so they return the same initialization, address, slot-limit and ID errors, and the check reports the state `sceNetCtlCheckCallback` does. Whether the console keeps this table apart, and what `sceNetCtlCheckCallback` does with it, is unverified on hardware - [sceNetCtlGetStateIpcInt, sceNetCtlGetStateV6IpcInt, sceNetCtlGetResultIpcInt, sceNetCtlGetResultV6IpcInt, sceNetCtlGetInfoIpcInt, sceNetCtlGetInfoV6IpcInt, sceNetCtlRegisterCallbackIpcInt, sceNetCtlUnregisterCallbackIpcInt, sceNetCtlRegisterCallbackV6IpcInt and sceNetCtlUnregisterCallbackV6IpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answer as their public twins, whose recovered argument registers they share: the getters return and write what `sceNetCtlGetState`, `GetStateV6`, `GetResult`, `GetResultV6`, `GetInfo` and `GetInfoV6` do, throws included, and the callback forms use the IPv4 and IPv6 tables, as no IpcInt check is exported to deliver a table of their own. This rests on the matching call shapes, not on a measurement; whether the internal client answers a null pointer or a bad code before its service does is unknown - [ApRp exports](../../../core/libs/prx/libSceNetCtl/Export.cpp) are constant-success functions in the supplied native module and consume no arguments. No callers were found in the supplied system-library decompilations, so unused callback, output and configuration argument types remain unresolved. Protected stub bodies are unchanged. ### Functional -- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) - host address changes stand in for the native service event queues. IPv4 callbacks receive connected/disconnected transitions and an initial event; intermediate connection states and errors are not represented. Native CheckCallback drains both IPv4 and IPv6 queues, but the host backend never delivers IPv6 events. Dispatch copies slots before invoking callbacks, so unregistering concurrently may leave one callback in flight; native dispatch holds its registration lock. Slot limits, invalid/deleted IDs and the no-op Term follow the supplied module. +- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) - host address changes stand in for native service events, following the existing [KytyPS5 state-transition model](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3648-L3661). Each IPv4 or IPv6 callback receives an initial connected/disconnected event and changes matching `sceNetCtlGetState` or `sceNetCtlGetStateV6`; intermediate connection states and errors are not represented, and console timing remains unverified. Native CheckCallback drains both service queues, as recorded in [the recovered-contract correction](https://github.com/boykopovar/AnyPS5/commit/ea086b23745cb167780f5aed0a3b9600a4e8dd0a). Dispatch snapshots both tables before invoking any callback, so a registration made during a callback waits for another check and unregistering concurrently may leave one callback in flight; native dispatch holds its registration lock. - [libSceNetCtl initialization](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the library starts initialized, as titles find it: it is the internal sysmodule `0x80000009`, and PPSA11386, PPSA20049, PPSA02530, PPSA14632 and PPSA15878 register and check NetCtl callbacks without importing `sceNetCtlInit`. PPSA11386 executes `int 0x42` at start-up unless `sceNetCtlRegisterCallback` returns 0. So the supplied module's `SCE_NET_CTL_ERROR_NOT_INITIALIZED` answers are unreachable, since Term is a no-op. Whether the console's process start or the module's start sets that state is not measured. - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) finds the host's default route in `/proc/net/route` on Linux and, on macOS, in the routing table read with `sysctl(NET_RT_FLAGS, RTF_GATEWAY)`, where an interface-scoped default route ranks after the unscoped one - [sceNetCtlGetIfStat](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) throws on macOS: an unprivileged process reads the interface byte counters (`getifaddrs` and `sysctl(NET_RT_IFLIST2)` alike) reduced to their low 32 bits, so a total would wrap every 4 GiB - [Host interface cache](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - on Windows, `sceNetCtlGetState`, `sceNetCtlGetStateV6`, `sceNetCtlCheckCallback`, `sceNetCtlGetIfStat` and `sceNetCtlGetInfo` reuse the last host interface query until `NotifyUnicastIpAddressChange`, `NotifyIpInterfaceChange` or `NotifyRouteChange2` reports a change, instead of calling `GetAdaptersAddresses` on every call. A DNS server change with no address, interface or route change is seen only at the next such change. If the notifications cannot be registered, every call queries the host as before. +- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - performs RFC 5389 STUN binding probes from the active host IPv4 interface against `stun.playstation.net` and fallback third-party STUN servers (`stun.cloudflare.com`, `stun.l.google.com`, `stun1.l.google.com`) instead of reading cached state from the console's background `netctl` service, which probes `*.np.stun.playstation.net` with authenticated Classic STUN (RFC 3489); those third-party hosts receive the host's IPv4 address when an uncached call resolves or probes them. Each discovery call has a 1000 ms total time budget (DNS resolution included, 300 ms per UDP probe), and successful results are cached for 60 s per local IPv4 address while failures re-probe on the next call. Console captures confirm NAT type 2, NAT type 3, and STUN failure; NAT type 1 when the mapped endpoint matches the local socket is not measured on a console with a direct public IP. diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 3f50d7f70a..e1cf9b0e6a 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -49,6 +49,7 @@ - [NGS2 patch destinations](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) - a rack handle with input 0 resolves to its first voice, matching the Dead Cells call in #4334; selection on multi-voice racks remains unverified. Input 1 is supported only for a voice handle in a two-input custom submixer. Nonzero rack-handle inputs, other unsupported inputs and nonzero inputs on disconnected ports throw because their contracts are unknown. GetPortInfo reports the resolved voice handle and selected input, with zero delay. Changes to a port destination or input while its source is rendered throw before mixing: callback routing order is unverified, and combining the old destination with the new input is invalid. - [NGS2 PCM waveform parsing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - `sceNgs2ParseWaveformData` reads integer PCM (tag 1, or `WAVE_FORMAT_EXTENSIBLE` with the PCM GUID) of 8, 16, 24 and 32 bits as types 0x11, 0x12, 0x14 and 0x16, and float (tag 3) of 32 and 64 bits as 0x18 and 0x1a, and returns 0x804A8432 for other depths, after [prosper's reading of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/30572df43ae20ff28f995db56149f7af42235741/prosper/src/hle/audio/ngs2_waveform.cpp#L194-L223); not measured on a console. Every depth keeps the checks of the 16-bit path (channels, rate, block align, whole frames, a looped `smpl` chunk throws), which that reading does not apply - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw +- [NGS2 waveform format routing](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - an extensible fmt chunk of 0x34 bytes whose GUID is neither PCM nor ATRAC9, which the module parses as ATRAC9 without checking its config, and the GUID of waveform type 0x43 throw. Otherwise a tag other than 1, 3 or 0xFFFE, an extensible fmt chunk under 0x28 bytes and an unknown GUID return `0x804A8432`, per [prosper's reading of the module](https://github.com/mattias800/prosper/blob/8dcb5dfdb9997ede7c76783995010d6abd1222f8/prosper/src/hle/audio/ngs2_waveform.cpp#L190-L208); not measured on a console - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - inputs on which the PS5 `libSceNgs2.native.sprx` of firmware 3.20 and 4.03 did not return within 60 s throw; whether it returns at all for them is unknown. They are an infinite speaker or param angle, an angle too large for a turn to change it, a speaker angle that wraps to exactly the unit angle, and a param angle that wraps onto the first speaker plus one turn, such as one float below the first speaker in radians - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` is now a minimal pass-through that follows [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `Ngs2GeomApply` (shadPS4 only stubs it): pitch 1, source position, volume the larger of the source minimum and maximum level, and a diagonal of the source maximum level (1 when it is not positive) over `matrix_format` channels (0 is two, at most 8). The listener and `flags` are ignored, so source attenuation, distance rolloff, panning and Doppler stay unimplemented; neither KytyPS5 nor this has been measured on a PS5, and a null output address returns `INVALID_OUT_ADDRESS` where KytyPS5 exits diff --git a/docs/dev/debt/libSceNpCppWebApi.md b/docs/dev/debt/libSceNpCppWebApi.md index e796fc8e2f..f471b02d0b 100644 --- a/docs/dev/debt/libSceNpCppWebApi.md +++ b/docs/dev/debt/libSceNpCppWebApi.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `TransactionBase<...>` (13 instantiations), `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish. `Transaction<...>::setOnFinishedCallback` and `TransactionBase<...>::setResponseInformationOption` (5 instantiations each, the `UserProfile::V1`, `ProfanityFilter::V2` and `CommunicationRestrictionStatus::V3` responses) return success without storing the callback or the option: `start` fails, so neither is ever used. The constructors and destructors of the same five `Transaction<...>` instantiations succeed without keeping state +- [sce::Np::CppWebApi::Common](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the `finish` overrides of `DownStreamTransaction<...>` (2 instantiations) and `Vector>::pushBack` return success without doing work; a failed request never produces a transaction to finish. `Transaction<...>::setOnFinishedCallback` and `TransactionBase<...>::setResponseInformationOption` (5 instantiations each, the `UserProfile::V1`, `ProfanityFilter::V2` and `CommunicationRestrictionStatus::V3` responses) return success without storing the callback or the option: `start` fails, so neither is ever used. The constructors and destructors of the same five `Transaction<...>` instantiations succeed without keeping state - [sce::Np::CppWebApi::Leaderboards::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - request-body and parameter bookkeeping succeeds without storing state: `GetRankingRequestBody::set{StartSerialRank,Group,Limit,Users,Offset}`, `RecordScoreRequestBody::set{Comment,SmallData,NeedsTmpRank,ComparedDateTime,PcId}`, `User::{setAccountId,setPcId}`, `BoardsApi::ParameterToGetBoardDefinition::{initialize,terminate}`, `RecordApi::ParameterToRecordLargeData::{initialize,terminate}`, `RecordApi::ParameterToRecordScore::{initialize,setxPsnAtomicOperation,terminate}`, `ViewApi::ParameterToGetLargeDataByObjectId::{initialize,terminate}`, `ViewApi::ParameterToGetRanking::{initialize,setgetRankingRequestBody,terminate}` - [sce::Np::CppWebApi::SessionManager::V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - parameter bookkeeping succeeds without storing state: `PlayerSessionsApi::ParameterToCreatePlayerSessions::{ctor,dtor,initialize}`, `PlayerSessionsApi::ParameterToGetPlayerSessionInvitations::{ctor,dtor,initialize,setfields,setinvitationInvalidFilter}`, `PlayerSessionsApi::ParameterToJoinPlayerSessionAsPlayer::{ctor,dtor,initialize,terminate}`, `PlayerSessionsApi::ParameterToLeavePlayerSession::{ctor,dtor,initialize}`, `RequestPlayerSession::set{MaxSpectators,InvitableUserType,JoinableUserType,ExclusiveLeaderPrivileges}`, `RequestPlayerSessionPlayer::setPushContexts` - [sce::Np::CppWebApi::TitleCloudStorage::V1 and InGameCatalog::V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - `DataApi::ParameterToDownloadData`, `DataApi::ParameterToGetMultiDataStatusesByUser` and `ContainerApi::ParameterToGetContainer` keep only whether they are initialized. `initialize`, `terminate` and the string setters return the codes of the module bundled with PPSA21564: 0x80553503 for a null `LibContext` or string, 0x80553502 for a `LibContext` that `Common::initialize` did not set or a parameter not yet initialized, 0x8055350f for a second `initialize` and 0x80553510 for `terminate` without one. They store none of the values, and `ParameterToGetContainer::set{sort,offset,limit,serviceLabel}` store nothing either: only a request would read them @@ -17,5 +17,5 @@ - [sce::Np::CppWebApi::SessionManager2V1](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by PPSA03671. Offline, the factories, `PlayerSessionsApi` calls and transaction `start`/`getResponse`/`readData` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter setters, `initialize`, `terminate`, constructors and destructors succeed without keeping state, as no request is ever sent. Response-body accessors (`getSessionId`, `getPlayerSessions`, `getInvitations`, the `IsSet` queries) throw: a failed request never produces a response object - [sce::Np::CppWebApi::UserProfile::V1, ProfanityFilter::V2, CommunicationRestrictionStatus::V3](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - imported by an Epic Online Services module whose dump ships no `libSceNpCppWebApi.prx`. The reference-count offset 0x00 of `ResponseHeaderBase` and `WebApiFilterRequest` is assumed from the other objects, not measured. Offline, the five API calls, `WebApiFilterRequestFactory::create` and the transactions' `start`/`getResponseHeaders` fail with `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`. Response accessors, `ResponseHeaderBase::getHeaderValue` and the `Vector`/`Iterator` members over `IntrusivePtr` and `unsigned long` throw: a failed request never produces a response - [libSceNpCppWebApi](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - unimplemented stubs have their parameter lists, const qualifiers and return types aligned with the demangled symbols confirmed by NID matching in [#8143](https://github.com/boykopovar/AnyPS5/issues/8143). They cover response-body accessors, iterators, and uninstantiated transactions in Leaderboards::V1, SessionManager::V1, InGameCatalog::V5, TitleCloudStorage::V1, and Common templates: each mangled name gives the method and parameter types, but the runtime layout of response objects and errors are unverified on console, so each call continues to throw NotImplemented_nid_no_patch. No call site is recorded for them -- [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib. API calls, factories and transaction `start`/`getResponse*` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter bookkeeping succeeds without state; `IntrusivePtr`/`Vector` helpers follow the layouts above, with `Vector::empty`/`size`/`operator[]` reading host-created bodies and throwing on title-provided ones. Response accessors, iterators, `Vector::begin`/`end`/`copyFrom` and transaction `abort`/`getId`/`operator=` throw: a failed request produces no response object, and aborting or renumbering a transaction that never ran has no defined outcome +- [sce::Np::CppWebApi::SessionManager2V1 and sce::Np::CppWebApi::InGameCatalog2V5](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 198 further exports imported by PPSA20397, named from aerolib. API calls, factories and transaction `start`/`getResponse*` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter bookkeeping succeeds without state; `IntrusivePtr`/`Vector` helpers follow the layouts above, with `Vector::empty`/`size`/`operator[]` reading host-created bodies and throwing on title-provided ones. Response accessors, iterators, `Vector::begin`/`end`/`copyFrom` and transaction `operator=` throw: a failed request produces no response object. As `start` never succeeds here, `TransactionBase::finish` and `abort` return 0x80553508 and `getId` returns 0, which is what the module bundled with PPSA21564 returns while the two words its `Transaction` constructor zeroes are still unset - [sce::Np::CppWebApi::UserProfile::V1, sce::Np::CppWebApi::CommunicationRestrictionStatus::V3 and the remaining sce::Np::CppWebApi::Common instantiations](../../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - 87 further exports imported by PPSA20397, plus the previously-throwing `ContainerApi::ParameterToGetContainer` lifecycle and `getContainer`. API verbs and transaction `start`/`getResponse` fail `SCE_NP_WEBAPI_ERROR_NOT_SIGNED_IN`; parameter and model bookkeeping (`initialize`, `terminate`, `set*`, constructors, destructors) succeeds without state. `BasicProfile`/`BasicPresence`/`PersonalDetail` response accessors, `*IsSet` queries, `Get*Response` getters, vector `begin`/`end`, iterator dereference/compare and transaction `abort`/`getId`/`operator=` throw: no response object exists offline and the model layouts are unverified diff --git a/docs/dev/debt/libSceNpEntitlementAccess.md b/docs/dev/debt/libSceNpEntitlementAccess.md index 7d54ea4fe1..7ce2b8189c 100644 --- a/docs/dev/debt/libSceNpEntitlementAccess.md +++ b/docs/dev/debt/libSceNpEntitlementAccess.md @@ -4,7 +4,7 @@ - [sceNpEntitlementAccessGetAddcontEntitlementInfoIndividual, sceNpEntitlementAccessPollConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumableEntitlementInfo, sceNpEntitlementAccessRequestConsumeEntitlement](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccess) - their names and NIDs are in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpEntitlementAccess.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim - [sceNpEntitlementAccessGetEntitlementKey](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified -- [Entitlement requests and polls](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp): native argument lists are restored for single-item, list and consume operations, plus abort/delete. Request IDs are 64-bit, labels occupy 20 bytes and transaction IDs occupy 72 bytes. List options occupy 32 bytes and results have a 64-byte stride; their field semantics and three list-result scalars remain unresolved. The existing signed-out responses do not reproduce native initialization, validation or asynchronous request handling; abort/delete throw until request tracking is implemented. +- [Entitlement requests and polls](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp): native argument lists are restored for single-item, list and consume operations, plus abort/delete. Request IDs are 64-bit, labels occupy 20 bytes and transaction IDs occupy 72 bytes. List options occupy 32 bytes and results have a 64-byte stride; their field semantics and three list-result scalars remain unresolved. The existing signed-out responses do not reproduce native initialization, validation or asynchronous request handling; abort and delete go through `NotImplemented_nid_no_patch` until request tracking is implemented, as the library keeps no request table. ### Functional diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md index cbc943be5e..d9602f375d 100644 --- a/docs/dev/debt/libSceNpUniversalDataSystem.md +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -3,6 +3,7 @@ ### Unknown function info - [Events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemCreateEvent` copies `prop` and returns the event's own property object through `prop_ptr`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) does; `sceNpUniversalDataSystemEventPropertyObjectSetObject`, `SetArray` and the array setters for objects and arrays store a copy of `value` and return the stored copy through `value_ptr`. Destroying a property object or array that its Create function did not return (an event's property object or a stored copy) does nothing, as its owner frees it. These ownership rules are not verified on a console, nor are the output of `sceNpUniversalDataSystemEventToString` (still `{}`) and `sceNpUniversalDataSystemEventEstimateSize` (still 3). +- [sceNpUniversalDataSystemCreateContext](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp) and [sceNpUniversalDataSystemCreateHandle](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Handle.cpp) (libSceNpUniversalDataSystem) - native return codes for null output pointers (`context == nullptr` and `handle == nullptr`) are unresolved and unverified on a console. The reimplementation in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp#L620-L640) returns `0x80553102` (`SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT`), but this error return is unverified against native hardware; host paths continue to throw `std::invalid_argument` until the native contract is established. - [sceNpUniversalDataSystemIntCreateRecordArray, sceNpUniversalDataSystemIntCreateRecordData and sceNpUniversalDataSystemIntCreateRecordObject](../../../core/libs/prx/libSceNpUniversalDataSystem/PendingExports.cpp) (libSceNpUniversalDataSystem) - The list is the intersection of the export table of the console's libSceNpUniversalDataSystem (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/libSceNpUniversalDataSystem.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. - [sceNpUniversalDataSystemIntDeleteAllRecord](../../../core/libs/prx/libSceNpUniversalDataSystem/PendingExports.cpp) (libSceNpUniversalDataSystem) - It is in the intersection of the export table of the console's libSceNpUniversalDataSystem (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/libSceNpUniversalDataSystem.c) and the [verified NID name database](https://github.com/zecoxao/sce_symbols/blob/2883963a0a514ba6e77407a08ef4730a65e88254/aerolib.csv), but no credible prototypes were found. It is declared without parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified. diff --git a/docs/dev/debt/libSceNpWebApi.md b/docs/dev/debt/libSceNpWebApi.md index af76876d59..ce821ad674 100644 --- a/docs/dev/debt/libSceNpWebApi.md +++ b/docs/dev/debt/libSceNpWebApi.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceNpWebApiGetHttpStatusCode, sceNpWebApiReadData](../../../core/libs/prx/libSceNpWebApi/Export.cpp) - throw through `NotImplemented_nid_no_patch`; behaviour and the PS5 prototypes are unmeasured. The only source of the parameter lists is shadPS4's PS4-derived prototypes (GPLv2, used as a reference only), with no second reference; structure pointers are `void*` because no layout is known. - [sceNpWebApiInitialize, sceNpWebApiTerminate, sceNpWebApiCreateHandle, sceNpWebApiDeleteHandle, sceNpWebApiCreatePushEventFilter, sceNpWebApiDeletePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - signatures, id ranges (library contexts 1 to 0x7fff, handles and filters below 0xf0000000) and errors (`LIB_CONTEXT_NOT_FOUND`, `HANDLE_NOT_FOUND`, `PUSH_EVENT_FILTER_NOT_FOUND`, `INVALID_ARGUMENT` for a missing filter parameter) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api/np_web_api_internal.cpp); they are not checked on a console. The libHttp context and pool size are kept but not used, since no request is ever sent. Limbo (CUSA01664) creates a context, a handle and filters when its game starts. - [sceNpWebApiCreateServicePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - a filter for a PSN service needs the signed-in user, and no user is signed in to PSN, so a valid call returns `SIGNED_IN_USER_NOT_FOUND` after the context and handle checks, as shadPS4 does with its network disabled. `sceNpWebApiDeleteServicePushEventFilter` therefore never finds a filter. - [sceNpWebApiCreateContext, sceNpWebApiDeleteContext, sceNpWebApiCreateRequest, sceNpWebApiDeleteRequest, sceNpWebApiAbortRequest, sceNpWebApiSendRequest](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - the online id is not looked up: as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api/np_web_api_internal.cpp), a user context belongs to the initial user, so a library context holds at most one. With no user signed in to PSN, `sceNpWebApiSendRequest` returns `NOT_SIGNED_IN` unless the request is aborted. Id layouts and the delete, abort and not-found results match an offline PS4 Pro probe on firmware 10.71 (#9797); other checks follow shadPS4. That homebrew probe, with a non-retail SDK version, accepted method 99, so `INVALID_ARGUMENT` for a method after DELETE from SDK 2.50 is unverified. `sceNpWebApiTerminate` also deletes user contexts and requests; shadPS4 stops after the first user context. diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index 923515fda6..d033c1ede5 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -10,7 +10,7 @@ - [sceNpWebApi2AddMultipartPart, sceNpWebApi2IntCreateRequest, sceNpWebApi2IntPushEventCreateCtxIndFilter and sceNpWebApi2SetMultipartContentType](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpWebApi2.c); no-argument prototypes follow [shadPS4's PS4 stubs](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp), not measured on a console. These declarations have no parameters solely to resolve known imports and make no ABI claim. - [sceNpWebApi2InitializeForPresence, sceNpWebApi2IntInitialize and sceNpWebApi2IntInitialize2](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - create a library context like `sceNpWebApi2Initialize`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L174-L222) does on the PS4: the argument structures (32 and 40 bytes) and the `struct_size` check that returns `SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT` follow its [declarations](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/np/np_web_api2/np_web_api2.h#L15-L31). The context type shadPS4 records (2 for the internal initializers, 3 for presence), the context name and the push configuration group are not kept, and the 0x8000 context limit is not applied. Not measured on a PS5 -- [sceNpWebApi2CreateMultipartRequest and sceNpWebApi2SendMultipartRequest](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceNpWebApi2.c). The signatures follow the PS4 implementations in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L52-L221) and [its multipart sender](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L388-L399); opaque struct pointers use `const void*` or `void*` to preserve source constness. These prototypes are not measured on a console. +- [sceNpWebApi2CreateMultipartRequest and sceNpWebApi2SendMultipartRequest](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - signatures and checks follow the PS4 implementations in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0845a12dc05f2fde85e7b203c76de03f93a585d/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L52-L72) and [its multipart sender](https://github.com/shadps4-emu/shadPS4/blob/e0845a12dc05f2fde85e7b203c76de03f93a585d/src/core/libraries/np/np_web_api2/np_web_api2.cpp#L388-L399): a null API group, path or method, a part index below 1, a null part or an empty part returns `SCE_NP_WEBAPI2_ERROR_INVALID_ARGUMENT`. As `sceNpWebApi2CreateRequest` and `sceNpWebApi2SendRequest` do here, a create takes the next shared ID (written only for a non-null ID pointer) and a send answers `SCE_NP_WEBAPI2_ERROR_UNAVAILABLE`; user contexts are not checked and parts are not kept. Not measured on a PS5 - [sceNpWebApi2IntPushEventCreateDestPs4CtxIndFilter](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - no credible prototype was found; it is declared without parameters solely to resolve known imports and makes no ABI claim. - [Push handle lifecycle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp): PS5 create/delete/abort signatures and invalid-context/invalid-handle errors are confirmed. Handles track ownership, timeout and abort state; deletion and context termination remove them. Network push operations and cancellation of active requests remain unimplemented. - [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp): PS5 confirms the context/statistics arguments and 32-byte output. An invalid context returns `0x80553404` without changing output; a valid context with null output succeeds. Allocator accounting remains absent: peak and current usage still report zero instead of native pool allocations. diff --git a/docs/dev/debt/libSceOpusCeltEnc.md b/docs/dev/debt/libSceOpusCeltEnc.md index 6ef2fceb48..6540f74459 100644 --- a/docs/dev/debt/libSceOpusCeltEnc.md +++ b/docs/dev/debt/libSceOpusCeltEnc.md @@ -2,4 +2,18 @@ ### Unknown function info -- [sceOpusCeltEncTerminate](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp): the context-ID pointer is confirmed. The null-input path does not define the return register, so an integer return contract is not established. The current integer return declaration is retained because changing it to `void` would require altering the protected stub body, which contains `return 0`. +- [sceOpusCeltEncCtl, sceOpusCeltEncEncodeFloatWithPriority and sceOpusCeltEncEncodeWithPriority](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) (libSceOpusCeltEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusCeltEnc.c). The prototypes mirror the sibling encoder declarations and libopus's own ctl and priority entry points, but the console's request codes, priority and batch semantics were not recovered. They throw through `NotImplemented_nid_no_patch`; this is not an ABI claim. + +### Functional + +- [libSceOpusCeltEnc](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) is an AJM frontend on the console: apart from libc runtime support and `sceKernelUsleep` the module imports only `sceAjmi*` calls, Create runs `sceAjmiInstanceCreate` with codec 15 and the flags 0x200000009 mono or 0x200000011 stereo, `sceAjmiBatchJobControlBufferRa` with request 0x6001 over state +0x10 with size 0x38 and results at state +0xA0, then `sceAjmiBatchStartBuffer` and `sceAjmiBatchWait`, Encode hands the samples to `sceAjmiBatchJobRunBufferRa`, and Destroy calls `sceAjmiInstanceDestroy` [per module runs with stubbed AJM](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282). Our encoders build libopus without AJM, so packets come from libopus and whether they equal the console's is unknown. Closes when: encoder packets are compared against console-produced ones or the library runs through our AJM + +- [Create and CreateEx](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) zero-fill the 728-byte state and write the word table [measured on the modules](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): the instance id at +0x00, this implementation's id because our AJM does not run, 8 × channels at +0x08, channels at +0x10, rate at +0x14 and rate/50 at +0x40 and +0xC8, the bitrate at +0x1C and +0xC0 with the per-rate per-channel table 11000/19000 to 51000/99000, the constants 2, 0x451, 0x5E0, 10, 0x803, 1, 0xFFFFFC18 and 0x10, and the 0x17B2 marker at +0xCC. The console's live AJM may write further result bytes into +0xA0 and +0xD8. Closes when: a console run with live AJM settles those result bytes + +- [Create and CreateEx](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) replacement rule is this implementation's choice: a create over a marked state retires the encoder its instance id names before any state write or allocation, so a later failure cannot strand an entry the state no longer names; explicit libopus error branches leave the state zeroed, while make-unique throwing after setupState or a later registry insert throw leaves the filled state without a live encoder. The console behaviour for its own allocation failure after a create over a live state is unmeasured, and the tests cannot force our failure paths because the library is a shared module against libopus, which has no allocator hook, while a process-wide memory limit fails an unknown allocation first. Closes when: a deterministic allocation-failure injection or a console measurement of the failed replace exists + +- [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) refuse with the [measured codes](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): null state +0x01, null samples +0x02 before the state marker, a markerless state +0x16, null data +0x04 before the frame check and maxDataBytes 0 +0x05; maxDataBytes below zero is not refused and reaches our libopus backend, where a rejection throws. Frame sizes outside the durations 2.5, 5, 10, 20, 40 and 60 ms, the measured 120 to 2880 at 48000, give +0x03; we scale the counts by rate while the module's rule for other rates is unmeasured. Closes when: the frame rule and the console outcome for a buffer AJM rejects are measured + +- [sceOpusCeltEncEncodeFloat](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) checks null state, null samples, frame 0 and the marker, then reserves the converted frame on the stack before its remaining checks; the reservation frame × channels × 4 bytes kills the console module for the [measured frame -1](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282), and our implementation throws once it exceeds 8 MiB instead, so the failure reports. The bound beyond the measured deaths is our host stack model, not a console measurement. Closes when: the console's stack behaviour for such frames is measured + +- [Create, CreateEx, Destroy, GetSize, Terminate and Initialize](../../../core/libs/prx/libSceOpusCeltEnc/Export.cpp) refuse with base 0x813F0000, the null pointer +0x01, the sample rate +0x10, channels +0x0D, application +0x11, the marker in Destroy +0x16 and `Initialize(nullptr)` +0x19, all from the [firmware 13.42 export measurements](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6401539314); Destroy clears the marker and leaves the other bytes, GetSize returns 728 on channels 1 and 2 and refuses other counts with +0x0D. CreateEx context handling, duplicate initialization and termination with live encoders throw as the recovered decoder analogue does. Closes when: those paths are measured on the console diff --git a/docs/dev/debt/libSceOpusSilkEnc.md b/docs/dev/debt/libSceOpusSilkEnc.md index 266dfe90fd..ef82072a5a 100644 --- a/docs/dev/debt/libSceOpusSilkEnc.md +++ b/docs/dev/debt/libSceOpusSilkEnc.md @@ -2,4 +2,18 @@ ### Unknown function info -- [sceOpusSilkEncTerminate](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp): the context-ID pointer is confirmed. The null-input path does not define the return register, so an integer return contract is not established. The current integer return declaration is retained because changing it to `void` would require altering the protected stub body, which contains `return 0`. +- [sceOpusSilkEncCancel, sceOpusSilkEncCtl, sceOpusSilkEncEncodeFloatWithPriority, sceOpusSilkEncEncodeFloatWithPriorityAndTimeout, sceOpusSilkEncEncodeWithPriority and sceOpusSilkEncEncodeWithPriorityAndTimeout](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) (libSceOpusSilkEnc) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceOpusSilkEnc.c). The prototypes mirror the sibling encoder declarations and libopus's own ctl, priority and batch-id entry points, but the console's request codes, priority, timeout and batch semantics were not recovered. They throw through `NotImplemented_nid_no_patch`; this is not an ABI claim. + +### Functional + +- [libSceOpusSilkEnc](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) is an AJM frontend on the console: apart from libc runtime support and `sceKernelUsleep` the module imports only `sceAjmi*` calls, Create runs `sceAjmiInstanceCreate` with codec 17 and the flags 0x9 mono or 0x11 stereo, `sceAjmiBatchJobControlBufferRa` with request 0x6001 over state +0x10 with size 0x4C and results at state +0xD0, then `sceAjmiBatchStartBuffer` and `sceAjmiBatchWait`, Encode hands the samples to `sceAjmiBatchJobRunBufferRa`, and Destroy calls `sceAjmiInstanceDestroy` [per module runs with stubbed AJM](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282). Our encoders build libopus without AJM, so packets come from libopus and whether they equal the console's is unknown. Closes when: encoder packets are compared against console-produced ones or the library runs through our AJM + +- [Create and CreateEx](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) zero-fill the 752-byte state and write the word table [measured on the modules](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): the instance id at +0x00, this implementation's id because our AJM does not run, 8 × channels at +0x08, 0x800 at +0x10, rate at +0x14, channels at +0x18 and rate/50 at +0x58 and +0xE4, the bitrate at +0x1C and +0xDC with the per-rate per-channel table 12000/19000 to 51000/99000, the constants 0x44F, 0x5E0, 10, 0x3E8, 0xFFFFFC18 and 0x10, and the 0x17B2 marker at +0xE8. The console's live AJM may write further result bytes into +0xD0 and +0x11C. Closes when: a console run with live AJM settles those result bytes + +- [Create and CreateEx](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) replacement rule is this implementation's choice: a create over a marked state retires the encoder its instance id names before any state write or allocation, so a later failure cannot strand an entry the state no longer names; explicit libopus error branches leave the state zeroed, while make-unique throwing after setupState or a later registry insert throw leaves the filled state without a live encoder. The console behaviour for its own allocation failure after a create over a live state is unmeasured, and the tests cannot force our failure paths because the library is a shared module against libopus, which has no allocator hook, while a process-wide memory limit fails an unknown allocation first. Closes when: a deterministic allocation-failure injection or a console measurement of the failed replace exists + +- [Encode and EncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) refuse with the [measured codes](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282): null state +0x01, null samples +0x02 before the state marker, a markerless state +0x15, null data +0x04 before the frame check and maxDataBytes 0 +0x05; maxDataBytes below zero is not refused and reaches our libopus backend, where a rejection throws. Frame sizes outside the durations 10, 20, 40 and 60 ms, the measured 480 to 2880 at 48000 with 120 and 240 refused, give +0x03; we scale the counts by rate while the module's rule for other rates is unmeasured. Closes when: the frame rule and the console outcome for a buffer AJM rejects are measured + +- [sceOpusSilkEncEncodeFloat](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) shares the CELT module's check order: null state, null samples, frame 0 and the marker, then the frame × channels × 4 byte stack reservation for the converted frame before its remaining checks, which kills the console module for negative frames [per the measured CELT run](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6412138282); our implementation throws once it exceeds 8 MiB instead, so the failure reports. The bound beyond the measured deaths is our host stack model, not a console measurement. Closes when: the console's stack behaviour for such frames is measured + +- [Create, CreateEx, Destroy, GetSize, Terminate and Initialize](../../../core/libs/prx/libSceOpusSilkEnc/Export.cpp) refuse with base 0x81770000, the null pointer +0x01, the sample rate +0x0F, channels +0x0C, application +0x10, the marker in Destroy +0x15 and `Initialize(nullptr)` +0x18, all from the [firmware 13.42 export measurements](https://github.com/boykopovar/AnyPS5/pull/10563#issuecomment-6401539314); Destroy clears the marker and leaves the other bytes, GetSize returns 752 on channels 1 and 2 and refuses other counts with +0x0C. CreateEx context handling, duplicate initialization and termination with live encoders throw as the recovered decoder analogue does. Closes when: those paths are measured on the console diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 5c75033c4d..676c5ec69e 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -14,6 +14,7 @@ - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per port slot. One title (PPSA04354) disassembled for this entry calls it once (0xead26b), only when a change bit of its own is set, with a 32-bit value read through a pointer and 0 or 1 taken from the 0x4 bit of a flags value, and ignores the result. The call does not show the threshold or the default. - [PadQueueLegacyAudioHaptics, PadClearLegacyAudioHaptics](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - PadQueueLegacyAudioHaptics and PadClearLegacyAudioHaptics route legacy audio haptics only to port 0; multi-port queueing is not modelled - [scePadGetControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - scePadGetControllerInformation always reports connectedCount = 1 and connected = true; behaviour across multiple open controller handles is not measured on hardware +- [scePadGetExtControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - on a standard-port handle it returns `NOT_PERMITTED` (0x80920101) and leaves the buffer as it is. Halo CE Universal (PPSA99500) [measured](https://github.com/thelinkin3000/halo-ce-universal/blob/377459f9655971a2f59c09e192c5220042121e6f/port/ps5/probe/results/pad-probe-run2.txt#L2-L29) that answer on firmware 13.59 for two connected DualSenses opened by the title, in three runs, with its [probe](https://github.com/thelinkin3000/halo-ce-universal/blob/377459f9655971a2f59c09e192c5220042121e6f/port/ps5/probe/pad/main.c#L263-L268) zeroing the buffer first. The probe logged only bytes 28 to 47, which stayed zero, so whether the console writes any of the buffer is not known. The special port (type 2) still answers 0 with 0x2c zeroed bytes, which is not measured, and so are the order of the handle and null checks and the answer on a console that grants the permission. - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, 16 slots for the console's 16 accounts, a 17th user gets the unknown-user code 0x809b0081) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, no special controller) did not cover; it measured open, `ALREADY_OPENED`, `scePadGetHandle`, both reads, both informations, the five setters above, close, and that two logged-in users get independent handles. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 skips a user on any negative result. `scePadOpenExt` throws for other port types - [scePadOpen, scePadGetHandle, scePadRead and scePadReadState for the remote control port](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - whether a console without a media remote opens the system user's remote control port (type 16), returns `DEVICE_NOT_CONNECTED` or fills other fields is not measured. The host has no remote, so the port opens its own handle (`PAD_REMOTE_HANDLE`) that reads one zeroed entry with no buttons and `connected` false; the other pad calls reject it with `INVALID_HANDLE`. SDL's PS5 video driver maps this port's buttons to keys, so a remote that read the DualSense turned Cross into Return (Raptor PPSA94002). - [scePadOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - port types 3 and 4 throw: a payload process on a PS5 (firmware 13.60) let them past the type check, but it cannot open a port for its own logged-in user (0x809b0081), so what they open is unknown. The argument checks it did reach are followed: types 1, 5, 6, 255 and -1, and 16 for a user other than the system user, fail with `INVALID_ARG` before the user is looked at, user -1 with 0x809b0001 on types 0, 2, 3 and 4 and on index 1, and index 1 of a logged-in user with 0x809b0003; whether index 1 of types 3 and 4 fails the same way was not measured; the payload never tried type 16 with the system user (255), which opens the remote control port as below @@ -22,6 +23,9 @@ - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured +- [scePadIsValidHandle](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle, int portType)` signature is recovered. Firmware 13.42's module, as BunnyFl ran it natively over a slot table he wrote ([comment](https://github.com/boykopovar/AnyPS5/pull/10880#issuecomment-6415854351); the driver emulated, the table not built by `scePadOpen`), answers 1 only when a slot holds the handle with exactly that port type, and 0 while not initialised. Here it is the same match on each open handle's type: 0 for a standard handle while its port is open, 2 for a special handle while its user's slot is open, 16 for the remote handle while the system user's remote port is open, and no match before `scePadInit`. `scePadOpen` opens no type-1 handle here, so no handle matches type 1. A closed standard handle is not valid, although the light bar, vibration and other setters keep accepting it +- [scePadResetLightBarAll and scePadResetLightBarAllByPortType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `(void)` and `(uint32_t portType)` signatures are recovered, and both follow firmware 13.42's module as BunnyFl ran it natively ([comment](https://github.com/boykopovar/AnyPS5/pull/10880#issuecomment-6415854351); the driver emulated, port type 1 unknown, the table not built by `scePadOpen`). By port type, any type but 0, 1 and 2 returns `INVALID_ARG` before the initialisation check (3, 4, 16 and -1 measured). Otherwise each open handle of that type is reset as `scePadResetLightBar` resets it, and the call returns `PAD_OK`, also before `scePadInit`, when it does nothing; `scePadResetLightBarAll` is type 0. In this model a special handle's reset has no effect, as special handles have no light bar, and `scePadOpen` opens no type-1 handle +- [scePadSetVibrationForce](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `(int handle, const PadVibrationParam*)` signature is recovered. Firmware 13.42's module, as BunnyFl ran it natively ([comment](https://github.com/boykopovar/AnyPS5/pull/10880#issuecomment-6415854351); the driver emulated), returns `0x80920005` before `scePadInit`, even for a null `param`, then `INVALID_ARG` for a null `param`; otherwise it sends `scePadSetVibration`'s request with a force byte set, without looking the handle up, and returns the driver's answer. There is no pad driver here: a special handle returns `PAD_OK` without effect, as with `scePadSetVibration`, and a standard handle sets its pad port's motors whether or not it is still open. A handle with no pad port throws, as the driver's answer for it is not measured. In this model the force byte has no effect, as there is no system vibration-off setting to override (`scePadSetVibrationMode` only stores its mode) - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown - [scePadGetControllerType](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad reports `PAD_CONTROLLER_TYPE_DUAL_SENSE` (2), following [duaLib](https://github.com/WujekFoliarz/duaLib/blob/master/src/include/duaLib.h)'s `s_SceControllerType` enum where `UNKNOWN = 0` and `DUALSENSE = 2`; special handles report 0 (`UNKNOWN`). The signature `(int handle, int32_t* type)` follows duaLib, while the validation order (checking initialization, null pointer, then handle) follows the library's other pad queries; duaLib does not validate null pointers and returns `PAD_ERROR_DEVICE_NOT_CONNECTED` for an invalid pad. diff --git a/docs/dev/debt/libScePsm.md b/docs/dev/debt/libScePsm.md new file mode 100644 index 0000000000..f0c83d9eae --- /dev/null +++ b/docs/dev/debt/libScePsm.md @@ -0,0 +1,17 @@ +# libScePsm technical debt + +### Unknown function info + +- [ScePsmMonoArrayAddrWithSize, ScePsmMonoArrayLength, ScePsmMonoDomainGet, ScePsmMonoFree, ScePsmMonoGcWbarrierGenericStore](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [ScePsmMonoStringChars, ScePsmMonoStringFromUtf16, ScePsmMonoStringLength, ScePsmMonoStringNew, ScePsmMonoStringNewWrapper](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [ScePsmMonoStringToUtf8, _ZN3sce3pss4core14IntHandleTable13NextIntHandleEv, _ZN3sce3pss4core8graphics14NativeGraphics18ReleaseVideoMemoryEPv, _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjj, _ZN3sce3pss4core8graphics14NativeGraphics19AllocateVideoMemoryEjjPv](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss4core8graphics14NativeGraphics19ReleaseSystemMemoryEPv, _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjj, _ZN3sce3pss4core8graphics14NativeGraphics20AllocateSystemMemoryEjjPv, _ZN3sce3pss4core9PsmObjectC1Ev, _ZN3sce3pss4core9PsmObjectC2Ev](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss4core9PsmObjectD0Ev, _ZN3sce3pss4core9PsmObjectD1Ev, _ZN3sce3pss4core9PsmObjectD2Ev, _ZN3sce3pss4core9threading4Cond4WaitEi, _ZN3sce3pss4core9threading4Cond6NotifyEv](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss4core9threading4CondC1ERNS2_5MutexEPKci, _ZN3sce3pss4core9threading4CondC2ERNS2_5MutexEPKci, _ZN3sce3pss4core9threading4CondD1Ev, _ZN3sce3pss4core9threading4CondD2Ev, _ZN3sce3pss5orbis9framework11PsmArrayObj6LengthEv](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework11PsmArrayObj9ToPointerEv, _ZN3sce3pss5orbis9framework11PsmCallList10UnregisterEPFvPvES4_, _ZN3sce3pss5orbis9framework11PsmCallList6UpdateEv, _ZN3sce3pss5orbis9framework11PsmCallList8RegisterEPFvPvES4_, _ZN3sce3pss5orbis9framework11PsmCallListC1Ev](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework11PsmCallListC2Ev, _ZN3sce3pss5orbis9framework12PsmEventData8SetValueERKNS_4Json5ValueE, _ZN3sce3pss5orbis9framework12PsmFramework10InitializeERKNS2_12PsmInitParamEiPPc, _ZN3sce3pss5orbis9framework12PsmFramework20RegisterInternalCallEPKNS2_12PsmCallEntryE, _ZN3sce3pss5orbis9framework12PsmFramework24RegisterPInvokeCallTableEPKNS2_15PsmLibraryEntryE](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework12PsmFramework3RunEv, _ZN3sce3pss5orbis9framework12PsmFramework8FinalizeEv, _ZN3sce3pss5orbis9framework12PsmInitParamC1Ev, _ZN3sce3pss5orbis9framework12PsmInitParamC2Ev, _ZN3sce3pss5orbis9framework12PsmStringObj17NewAndStoreStringEPvPc](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework12PsmStringObj6ToUtf8Ev, _ZN3sce3pss5orbis9framework12PsmStringObj8FreeUtf8EPc, _ZN3sce3pss5orbis9framework14PsmDelegateObj11FreePointerEv, _ZN3sce3pss5orbis9framework14PsmDelegateObj15AllocatePointerEv, _ZN3sce3pss5orbis9framework18PsmFrameworkCsharp21SetEnableNativeMethodEb](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework21PsmMainThreadCallList10UnregisterEPFvPvES4_, _ZN3sce3pss5orbis9framework21PsmMainThreadCallList6UpdateEv, _ZN3sce3pss5orbis9framework21PsmMainThreadCallList8RegisterEPFvPvES4_, _ZN3sce3pss5orbis9framework23TerminatePsmEventCsharpEv, _ZN3sce3pss5orbis9framework24InitializePsmEventCsharpEv](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework27TerminatePsmFrameworkCsharpEv, _ZN3sce3pss5orbis9framework28InitializePsmFrameworkCsharpEv, _ZN3sce3pss5orbis9framework8PsmEvent10UnregisterEiPFvRKNS2_12PsmEventDataEPvES7_, _ZN3sce3pss5orbis9framework8PsmEvent17RegisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_, _ZN3sce3pss5orbis9framework8PsmEvent19UnregisterDelegatorEPFvRKNS2_12PsmEventDataEPvES7_](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. +- [_ZN3sce3pss5orbis9framework8PsmEvent4PostERKNS2_12PsmEventDataE, _ZN3sce3pss5orbis9framework8PsmEvent4SendERKNS2_12PsmEventDataE, _ZN3sce3pss5orbis9framework8PsmEvent8RegisterEiPFvRKNS2_12PsmEventDataEPvES7_, scePsmRegisterInternalCall, scePsmRegisterPInvokeCallTable](../../../core/libs/prx/libScePsm/Export.cpp) - names and NIDs match the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libScePsm.c); signatures and behavior are unverified. No-parameter declarations resolve imports and throw through `NotImplemented_nid_no_patch` when called. diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 7b099df461..1d60af4288 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -8,7 +8,7 @@ - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_YEAR` when the zone-shifted tick is past 9999 as a signed value, otherwise `INVALID_VALUE` when the source tick or the zone-shifted tick is negative as a signed value, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick formatting the current time and the ±1439 minute offset limit come from the PS5 measurement in the next entry and are not measured on the PS4 -- [sceRtcFormatRFC3339, sceRtcGetDayOfWeek, sceRtcSetTime_t](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001, not on a PS5. From 0x03000000 on: `FormatRFC3339` returns `INVALID_VALUE` when adding the zone shift to the tick overflows or underflows 64 bits (earlier SDKs wrap); `GetDayOfWeek` checks the month before the year and rejects a year past 9999 (earlier SDKs check the year first, with no upper bound); `SetTime_t` rejects negative seconds with `INVALID_VALUE` and converts all 64 bits (earlier SDKs take the low 32 bits). 0x02ffffff still behaves the earlier way and 0x03000000 the later way. The earlier rules apply only to a PS4 title (a CUSA title ID): a PS5 title has its own SDK numbering, so it always gets the later rules, the behaviour main had before; those are not measured on a PS5. +- [sceRtcFormatRFC3339, sceRtcGetDayOfWeek, sceRtcSetTime_t](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001, not on a PS5. From 0x03000000 on: `FormatRFC3339` returns `INVALID_VALUE` when adding the zone shift to the tick overflows or underflows 64 bits (earlier SDKs wrap); `GetDayOfWeek` checks the month before the year and rejects a year past 9999 (earlier SDKs check the year first, with no upper bound); `SetTime_t` rejects negative seconds with `INVALID_VALUE` and converts all 64 bits (earlier SDKs take the low 32 bits). 0x02ffffff still behaves the earlier way and 0x03000000 the later way. The earlier rules apply only to a PS4 title (a CUSA title ID): a PS5 title has its own SDK numbering, so it always gets the later rules, which PS5 firmware 13.60 follows; there `SetTime_t` checks for negative seconds before a null pointer. - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured in a payload process on a PS5 (firmware 13.60), and all 33 cases match: the standard FAT layout with seconds halved; a year before 1980 writes 0 and one after 2107 writes `0xff9fbf7d`, both returning `INVALID_YEAR`; year 0 and an invalid month, day, hour, minute, second or microsecond return their own error without writing; a null argument returns `INVALID_POINTER`; `SetDosTime` decodes every field unchecked (`0xffffffff` gives 2107-15-31 31:63:62) and clears the microseconds. What a title process sees is assumed to be the same - [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [Local time beyond the host's range](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - when the host cannot convert an instant to local time (on Windows, before 1970 or after 3000), the zone offset is taken from the same instant moved by whole 400-year Gregorian cycles into the host's range, which keeps a POSIX `TZ` rule's offset; a zone with historical transitions gets its current rule there, not the console's offset, which is not measured outside the probe's -03:00 zone diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index 31b53a4283..1259a258ef 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -9,8 +9,9 @@ ### Unknown function info - [sceRudpGetStatus](../../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. -- [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. +- [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. No source says whether a size stored by `sceRudpSetMaxSegmentSize` is reported instead, so 1410 is always reported. +- [sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - unverified on a PS5. `(uint16_t mss)` and the checks follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp#L1573-L1584), with the argument in [RPCS3](https://github.com/RPCS3/rpcs3/blob/8b9ee60569753db8406b66d135cce112156172a1/rpcs3/Emu/Cell/Modules/cellRudp.cpp#L149-L153): `RUDP_ERROR_NOT_INITIALIZED` before `sceRudpInit`, `RUDP_ERROR_NOT_ACCEPTABLE` while a context exists, otherwise stored without a range check and kept across `sceRudpEnd`, as Vita3K keeps it in its library state. Nothing reads the stored size: Vita3K [leaves the getter unimplemented](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp#L1080-L1082), so `sceRudpGetMaxSegmentSize` keeps the measured 1410 - [sceRudpGetOption, sceRudpSetOption](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - full PS5 option contracts are unverified; defaults and validation follow [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). Options 12 and 13 have unknown meanings; last-error option 14 is not maintained. Their setters store all 32 bits per context and return 0 for valid four-byte buffers, preserving acceptance on `main`; their getters throw. Static inspection of PPSA06888 v01.001.008's bundled Unity transport shows its context-set constructor configuring IDs 2, 3, 5 and 6; runtime usage and other paths remain unverified. The plugin wrappers pass four-byte values. No context can connect, so stored settings cannot affect traffic. Closes when: PS5 option contracts are verified and the remaining behavior is implemented. - [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) - console hardware behavior and removal of the last entry during an existing wait remain unverified. A [firmware 13.42 report](https://github.com/boykopovar/AnyPS5/pull/10954#issuecomment-6398457761) executes the actual module under emulation with kernel/libc imports stubbed and condition waits scripted, not on console hardware: an empty poll waits for its remaining timeout, including after spurious wakes, and leaves event output untouched. This agrees with the retained wait behavior here and conflicts with the immediate empty return in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/c86ac732bc811057e690bed280c5076bdf95385e/vita3k/modules/SceRudp/SceLibRudp.cpp). The argument checks, control operations and 8-byte event layout retain their existing sources; the firmware report has not been independently reproduced here. - [sceRudpPollCancel, sceRudpPollDestroy and sceRudpEnd](../../../core/libs/prx/libSceRudp/Export.cpp) - cancellation remains incomplete. PollCancel still throws through its parameterless placeholder; [Orbital b321fcd](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json) declares `int sceRudpPollCancel(int pollId)`. The [detailed firmware 13.42 report](https://github.com/boykopovar/AnyPS5/pull/10954#issuecomment-6399054346), under the emulated/stubbed imports described above, observes a signed low-32-bit argument and initialization, negative-ID and unknown-ID errors `0x80770001`, `0x80770004` and `0x8077001F`. Two idle cancels coalesce into one per-poll cancellation consumed even by a zero-timeout wait. In the scripted two-waiter Cancel/Destroy cases both waiters return `0x80770014`; those calls drain the waiters before returning, and Destroy then destroys the condition variable. A later wait on that destroyed ID returns `0x8077001F`. End similarly drains its active waiter with `0x80770014`; subsequent Wait/Cancel calls return `0x80770001`. Here Destroy/End return without draining and wake existing waiters with `0x8077001F`/`0x80770001`. Real-kernel wake/timeout races, simultaneous control calls, End/re-init overlap and extreme timeout boundaries remain untested; the report is not a console hardware measurement or an independent reproduction here. -- [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo, sceRudpListen and sceRudpSetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) - remain throwing placeholders. Their names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c); their parameterless declarations solely resolve known imports and do not establish the ABI. +- [sceRudpAccept, sceRudpFlush, sceRudpGetLocalInfo and sceRudpListen](../../../core/libs/prx/libSceRudp/Export.cpp) - remain throwing placeholders. Their names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceRudp.c); their parameterless declarations solely resolve known imports and do not establish the ABI. diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 807c0f4265..3249bb0f9a 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -19,6 +19,6 @@ - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. One title (PPSA35669, in a plugin) disassembled for this entry has one call site (0x2eda71); it clears 56 bytes for the result structure first (the size of the structure used here), puts a value returned by a call with the argument 0x8000 at offset 8 and 0x400 at offset 0x10, and afterwards branches on a negative result and reads the 32-bit field at offset 0, skipping its loop when it is 0. That agrees with the result structure of `sceSaveDataDirNameSearch`. The condition structure was not read. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2`. In PPSA05347 the function that calls it at 0x235fbb5 zeroes its 0x40-byte result before the call and, for a negative result other than 0x809f0008 and 0x809f000f (BUSY included), logs the code and returns it without recording the mount; no abort follows. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware -- [sceSaveDataBackup, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount3, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup +- [sceSaveDataBackup, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataDirNameSearchPs4, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount, sceSaveDataMount3, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup - [sceSaveDataSyncSaveDataMemory](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob diff --git a/docs/dev/debt/libSceUsbStorage.md b/docs/dev/debt/libSceUsbStorage.md index c901faca35..08030f3d5d 100644 --- a/docs/dev/debt/libSceUsbStorage.md +++ b/docs/dev/debt/libSceUsbStorage.md @@ -1,19 +1,22 @@ # libSceUsbStorage technical debt -### Silent stubs - -- [sceUsbStorageInit, sceUsbStorageTerm](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - no USB mass-storage device is ever attached, so there is nothing to initialize or tear down: both accept every call and keep no state, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L91-L99) does with its plain ORBIS_OK returns. A passthrough that lists devices must build its list in `Init` and free it in `Term`. - ### Unknown function info -- [sceUsbStorageRegisterCallback, sceUsbStorageUnregisterCallback, sceUsbStorageRegisterCallbackForMapAvailable, sceUsbStorageUnregisterCallbackForMapAvailable, sceUsbStorageRegisterCallbackForMapUnavailable, sceUsbStorageUnregisterCallbackForMapUnavailable](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - what a successful registration stores, what unregistering a never-registered callback answers, and when any callback fires is unverified: no device is ever attached, and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L110-L174) logs all six as `(STUBBED)` and returns 0 without storing anything, the success-without-a-reference pattern #6288 was closed for. The argument validation is implemented (0x80f40002 for a null callback, an event type outside 1 and 2, or a null directory); past it they keep the marker until a reference or a measurement says what success does. -- [sceUsbStorageRequestUnmap, sceUsbStorageGetdentsClose](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - what the console answers for a directory that was never mapped and a getdents descriptor that was never opened is unknown: nothing can be mapped or opened in this model, shadPS4's [unmap](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L281-L288) only logs without keeping state and its [close](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L378-L383) succeeds only because `erase` ignores a missing key, and the no-device libraries throw on this path (`sceMoveClose` for a handle that was never opened, `sceContentSearchCloseMetadata` because no metadata id is ever handed out). Both keep the marker after their argument validation. +- [sceUsbStorageRegisterCallback, sceUsbStorageUnregisterCallback](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the answers for a registration before `sceUsbStorageInit`, a second registration of the same callback for the same event type and the unregistration of a callback that is not registered are unknown, so the first two throw and the third keeps the marker. Whether the console raises mount events for devices attached before the registration is unknown; none are raised. A registration keeps its user data and receives [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L186-L199)'s 16-byte event (1 for a mount and 2 for an unmount, the device id, the device flags) for its event type only, on a service thread that rescans the host once a second instead of the automounter's IPC thread; an unregistered callback is not called again. Closes when: measured on a PS5 +- [sceUsbStorageRegisterCallbackForMapAvailable, sceUsbStorageUnregisterCallbackForMapAvailable, sceUsbStorageRegisterCallbackForMapUnavailable, sceUsbStorageUnregisterCallbackForMapUnavailable](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - what a successful registration stores and when the callbacks fire is unverified: [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L128-L174) logs all four as `(STUBBED)` and returns 0 without storing anything, the success-without-a-reference pattern #6288 was closed for. The argument validation is implemented (0x80f40002 for a null callback or a null directory); past it they keep the marker until a reference or a measurement says what success does. +- [sceUsbStorageRequestUnmap, sceUsbStorageGetdentsClose](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - what the console answers for a mount point that is not mapped and a getdents descriptor that is not open is unknown: shadPS4's [unmap](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L281-L288) only logs without keeping state and its [close](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L378-L383) succeeds only because `erase` ignores a missing key. Both keep the marker on that path, after their argument validation; unmapping a mount point a map request returned, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L386-L398) does, and closing an open scan succeed. - [sceUsbStorageClearFakeWriteProtection, sceUsbStorageSetFakeMapLockForDebug, sceUsbStorageSetFakeWriteProtection](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceUsbStorage.c), but no credible prototypes were found. They are declared without parameters solely to resolve known imports; this is not an ABI claim. -- [sceUsbStorageGetMountPointOfShellCore](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the prototype is known, but no reference says what a console without a mapped device writes into the mount-point buffer or returns; it keeps the marker until a reference or a console measurement answers it. +- [sceUsbStorageGetMountPointOfShellCore](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - no reference says what the console writes or returns for a device that is not attached or a null buffer; both keep the marker. An attached device writes its mount point, `/mnt/usb` after [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L22-L25)'s `/mnt/usb0`. +- [sceUsbStorageInit](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the parameter is the attribute of the service's own thread ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L241-L242)), but its type is unresolved, so a non-null attribute throws. Initializing again while initialized succeeds without a rescan; that answer is not measured. ### Functional -- [the argument validation](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the null-pointer, null-callback, event-type and null-directory refusals with 0x80f40002, and the getdents read of a never-opened descriptor answering 0x80f40005 with zero bytes, follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L176-L383); the validation runs before the markers the register, unregister, unmap and getdents-close exports keep above. None of this is measured on a console. -- [the no-device answers](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - AnyPS5 has no USB mass-storage passthrough, so no device is ever attached: the device list is empty, `sceUsbStorageIsExist` succeeds and reports non-existence for every device id and directory, and the device-info query and the map and getdents-open requests answer 0x80f40004 for every device id. A passthrough that lists devices follows shadPS4's virtual-device model at the same commit for the filled paths, including its packed dirent. -- [the device-info record](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the 0x338-byte record a passthrough fills for `sceUsbStorageGetDeviceInfo` is unverified on hardware; its layout follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L34-L45), which recovered it from the module's own export and was checked against #6288's "volume size at 0x0": the u64 volume size at offset 0, the vendor, product and serial strings at 0x16, 0x120 and 0x228, and the flags field the service masks with 0xd0 at 0x330. shadPS4's record starts with a u32 device id at offset 0; filling it that way puts a device id where a title reads the volume size. +- [the host passthrough](../../../core/libs/prx/libSceUsbStorage/src/HostVolumes.cpp) (libSceUsbStorage) - only with `APS5_USB_STORAGE_PASSTHROUGH` set; without it no host volume is scanned. The devices are the FAT and exFAT volumes the host mounted from USB mass-storage devices: on Linux the whole-file-system `vfat`, `msdos` and `exfat` entries of `/proc/self/mountinfo` whose block device sits under a USB interface of class 08 in sysfs, on [Windows](../../../core/libs/prx/libSceUsbStorage/src/HostVolumesWindows.cpp) the drive letters whose FAT, FAT32 or exFAT volume reports `BusTypeUsb`, and none on macOS. NTFS and a FUSE exFAT (`fuseblk`) are not listed. `sceUsbStorageInit` scans the host and a service thread rescans it once a second; a volume keeps its id, the lowest free of eight slots, while it stays mounted, and `/mnt/usb` resolves to its current root until a scan drops it or `sceUsbStorageTerm`. How the console numbers its devices is unknown. Closes when: measured on a PS5 +- [sceUsbStorageUnregisterCallback](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - it waits for a callback that is running, except when called from inside a callback, so a callback must not wait on a thread that is unregistering. Whether the console's unregistration waits is unknown. Closes when: measured on a PS5 +- [the device-info record](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the 0x338-byte record is unverified on hardware; its layout follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L34-L45), which recovered it from the module's own export: the u64 volume size at offset 0, the vendor, product and serial strings at 0x16, 0x120 and 0x228, and the flags the service masks with 0xd0 at 0x330. The vendor id, product id and release at 0x10, 0x12 and 0x14, the string lengths at 0x118, 0x220 and 0x328 and flag bits 4 (read-only), 6 (FAT) and 7 (exFAT) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L28-L55), whose record otherwise starts with a device id where a title reads the volume size. Closes when: measured on a PS5 +- [the device-info values](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - which strings and which size the console reports is unknown. The volume size is the capacity of the mounted file system, not of the partition; the vendor and product are the SCSI inquiry strings (sysfs `vendor` and `model`, or the storage device descriptor on Windows) without trailing spaces, and the serial number is the USB one, empty when the device has none. A string longer than 255 bytes throws. Closes when: measured on a PS5 +- [the map requests](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - `sceUsbStorageRequestMap` and `sceUsbStorageRequestMapWSB` follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L245-L279): the mount point is `/mnt/usb` followed by the requested directory (the device root for a null or empty one), the last argument receives 0 and null outputs are skipped. The directory must exist on the device; a missing one, a `.` or `..` component, a component with a root name (a drive letter on Windows), a backslash, a mount point longer than 127 bytes, a second map of the same directory, nonzero flags or volume size, and a nonzero fourth `RequestMapWSB` argument throw, as their answers are unknown. Unlike shadPS4, a directory that starts with `/mnt/usb` is not returned unchanged. The mapping limit SharpProspero names (0x80f40015) is not enforced. Closes when: measured on a PS5 +- [sceUsbStorageIsExist](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - it reports whether the attached device holds the directory, resolved and refused as for a map request (the device root for a null or empty one), where shadPS4's [one-argument version](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L237-L243) has no directory; what the console does with the directory or a null output, and whether the bool result reports success or failure, is unknown; a null output throws. Closes when: measured on a PS5 +- [the getdents scan](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - `sceUsbStorageGetdentsOpen` lists the root of the device into descriptors numbered from 1 with a 0x8000-byte buffer size, skipping names that start with a dot, and `sceUsbStorageGetdentsRead` writes shadPS4's [packed dirent](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L290-L376) (u32 file number from 1, u16 record length rounded to 8, type 4 for a directory and 8 otherwise, u8 name length, NUL-terminated name) until the buffer is full and zero bytes at the end. A name longer than 255 bytes, a host listing error and a buffer too small for the next record throw where shadPS4 truncates, ignores or answers zero bytes. None of this is measured on a console. Closes when: measured on a PS5 +- [the argument validation](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the null-pointer, null-callback, event-type and null-directory refusals with 0x80f40002, the answers 0x80f40004 for a device that is not attached, and the getdents read of a descriptor that is not open answering 0x80f40005 with zero bytes, follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L176-L383); the validation runs before the markers the exports keep above. None of this is measured on a console. - [the prototypes of sceUsbStorageRequestMap and sceUsbStorageGetDeviceList](../../../core/libs/prx/libSceUsbStorage/Export.cpp) (libSceUsbStorage) - the signatures follow the ABI restored on `main` (the firmware-database sweep of 2026-10-21), which matches [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/f68923fe807f657348e6719ce658bcbd0a2b1c72/src/core/libraries/usbd/usbstorage.cpp#L176-L383): `RequestMap`'s six parameters, the typed `UsbStorageCallback` of the register/unregister family, `IsExist`'s three-argument bool form and `GetDeviceList`'s `std::int32_t*` count (the count also follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Platform/UsbStorage.cs#L233-L235), whose earlier eight-parameter `RequestMap` reading carried the trailing reserved pair the restored ABI drops). diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 6cea4cb6a6..66e1a1ff5e 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -3,18 +3,31 @@ ### Unknown function info - [sceVideodec2InitLibrary](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) throws: the FFmpeg decoders this module creates need no library-level initialisation, and neither SharpProspero nor EVO-PLAYER-PS5 declares this call, so its arguments and result are unknown. A title imports it from this module -- [sceVideodec2GetAvcPictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - runs the `sceVideodec2GetPictureInfo` body (KytyPS5 binds both NIDs to one function; SharpProspero gives both the same `outputInfo, first, second` signature); the second (bottom field) picture info is never written, since only progressive H.264 output is modelled. prosper calls the AVC layout unestablished (no title observed calling it) - -- [sceVideodec2TermLibrary and twelve other exports](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) - report that they are not implemented; what they do on the console is not known. The others are `QueryLibraryInfo`, `QueryDecoderInfo`, `MapMemory`, `MapDirectMemory`, `PresetDecoderConfigInfo` (plain, `Avc`, `Hevc`, `Vp9`), `CreateDecoderBid`, `CreateHevcDecoder`, `QueryHevcDecoderMemoryInfo` and `GetHevcPictureInfo`. They are exported so that a title which imports them with `sceVideodec2InitLibrary` and calls none of them directly loads. Only `sceVideodec2MapDirectMemory` has a known parameter list, `(decoder, memory)`, from [EVO-PLAYER-PS5's `sce_videodec2.h`](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/projects/evoplayer/media/include/sce/sce_videodec2.h#L173); the others take none here +- [sceVideodec2TermLibrary and eleven other exports](../../../core/libs/prx/libSceVideodec2/Unimplemented.cpp) (libSceVideodec2) - report that they are not implemented; what they do on the console is not known. The others are `QueryLibraryInfo`, `QueryDecoderInfo`, `MapMemory`, `MapDirectMemory`, `PresetDecoderConfigInfo` (plain, `Avc`, `Hevc`, `Vp9`), `CreateDecoderBid`, `CreateHevcDecoder` and `QueryHevcDecoderMemoryInfo`. They are exported so that a title which imports them with `sceVideodec2InitLibrary` and calls none of them directly loads. Only `sceVideodec2MapDirectMemory` has a known parameter list, `(decoder, memory)`, from [EVO-PLAYER-PS5's `sce_videodec2.h`](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/projects/evoplayer/media/include/sce/sce_videodec2.h#L173); the others take none here ### Functional +- [sceVideodec2Decode and sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): the decoder core's discarded flag is not modelled: a returned picture always has `OutputInfo.isDiscardedFrame` (byte 0xb) at 0, and an unshown VP9 frame returns no picture. Per a #10913 review reading firmware 13.42's module (not reproduced here), the byte is written only with a returned picture and is left as the caller set it otherwise, as here. Name and offset: [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs#L150-L192) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/83f8bfd3c9c35fcb135ef6c33dfd88bbfe066ee2/src/libs/libVideoDec2.cpp#L91-L105); shadPS4 leaves it as padding. PPSA05110 sets only `thisSize` before its VP9 Decode (0x13728dd) and Flush (0x1371f7e) and takes a picture only when the byte is 0 + +- [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - One title (PPSA02225) disassembled for this entry calls it once (arguments set up at 0x8b5095-0x8b50a6) with three pointers: a structure built on its stack, a structure inside its decoder object and the address of a field of the same object. A nonzero result makes it leave the decoder setup; on one path (a flag byte set and two 32-bit fields equal to values it computed before) it passes that field to the next Videodec2 call. The codec type it requests and the meaning of the structure fields are not shown. + - [AVC parameter-set units](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - an H.264 access unit with no slice NAL (types 1 to 5), such as the sequence and picture parameter sets Twispero (PPSA99474) submits on their own before a stream's first slices, is kept and put in front of the next unit that has a slice, and `sceVideodec2Decode` returns 0 with no picture for it, since libavcodec refuses a packet without a slice. What the console returns for such a unit is not known. HEVC units of parameter sets alone are still passed to libavcodec as they are -- [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is NV12 for 8-bit pictures. A 10-bit picture is written as 16-bit little-endian words with the sample in the low 10 bits, Y rows then interleaved UV rows at `framePitchInBytes` times the picture height, with `framePitch` in samples and `framePitchInBytes` in bytes (twice the width, aligned to 256), and `QueryDecoderMemoryInfo` sizes a frame of an HEVC or VP9 profile 2 configuration with two bytes per sample. Evidence is from [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/tree/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783), which decodes and presents 1080p HEVC Main10 on hardware through this path (`docs/evo-pro/on-demand-decoder-handoff.md`): it takes a picture as 10-bit when `pitch_bytes >= pitch * 2`, puts UV at `pitch_bytes` times the height and samples with a low-bit factor (`projects/evoplayer/media/src/evo_vdec_native.c`, `ro_harvest`), and its hardware logs give 6128 KB and 24496 KB frame buffers for 1920x1088 and 3840x2176 Main10 decoders after rounding to 16 KiB. The 1080p size is what this formula gives; at 3840x2176 it gives 16 KiB less, so the console's size there has another term that is not known. Unverified: `frameFormat` (still 0; no consumer found reads it), VP9 profile 2 output (no hardware evidence), and the layout of 12-bit pictures and of decoded formats other than yuv420p10le, which throw. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC pictures (`SceVideodec2HevcPictureInfo` is not filled), and `sceVideodec2GetHevcPictureInfo` reports that it is not implemented. One title (PPSA02225) disassembled for this entry calls it once (arguments set up at 0x8b5095-0x8b50a6) with three pointers: a structure built on its stack, a structure inside its decoder object and the address of a field of the same object. A nonzero result makes it leave the decoder setup; on one path (a flag byte set and two 32-bit fields equal to values it computed before) it passes that field to the next Videodec2 call. The codec type it requests and the meaning of the structure fields are not shown. +- [HEVC and VP9 output gaps](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): console `frameFormat` remains unknown and is 0; VP9 profile 2 output has no hardware evidence. Twelve-bit pictures and formats other than yuv420p10le throw. HEVC Main10 uses low-bit 16-bit little-endian samples, with interleaved UV after `framePitchInBytes * height`, following [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/tree/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783) `projects/evoplayer/media/src/evo_vdec_native.c`, `ro_harvest`: the hardware consumer detects ten bits when byte pitch is at least twice sample pitch and uses a low-bit factor. + +- [HEVC frame-buffer memory sizing](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): the console has an unknown extra term at 3840x2176. The two-byte-per-sample formula gives 16 KiB less than the hardware size of 24496 KB after 16 KiB alignment, while matching 6128 KB at 1920x1088. Evidence is in [EVO-PLAYER-PS5 on-demand-decoder-handoff.md](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/9715f6ee9bf9b53461d6fdf9a2e11b4c48357783/docs/evo-pro/on-demand-decoder-handoff.md). + +- [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp): console field values remain unverified. The 88-byte projection uses [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs) and the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2. Level is 0 because the elementary stream does not signal it. Pictures deeper than 10 bits throw. Superframes are submitted as individual coded frames to retain input metadata association. Only tracked single-picture VP9 metadata is projected; untracked frame-buffer metadata and other payload versions remain unsupported. -- [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Pictures deeper than 10 bits are rejected. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. `sceVideodec2GetPictureInfo` writes this layout for a VP9 picture, as shadPS4's `videodec2.cpp` writes the decoder's own layout; Mortal Shell 2 (PPSA34008) calls it with `thisSize` 88. - [sceVideodec2Decode after sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) without `sceVideodec2Reset` restarts FFmpeg's decoder (`avcodec_flush_buffers`) and decodes the unit, where it threw before. TEKKEN DARK RESURRECTION (PPSA31371), which runs in Sony's PSP emulator, decodes after a flush without a reset (#7118), so the console accepts the call. Pictures the flush decoded but has not returned yet are returned before the new ones. A flush with no picture pending (each decoded unit has returned its picture) leaves the decoder and its reference pictures as they are, so a stream that continues without a new IDR picture keeps decoding: Daxter (PPSA09695), also in the PSP emulator, flushes after every access unit of its H.264 movies, and restarting the decoder there lost every P picture until the next keyframe. What the console returns for pending pictures, and for a stream that continues across a flush that had pictures pending, is not measured. - [sceVideodec2Decode](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) and `sceVideodec2Flush` output the coded picture, with the chroma plane after its luma rows, as the console does for 1080p H.264 and HEVC streams coded at 1920x1088. An HEVC or VP9 picture whose size is not a multiple of 16, such as HEVC coded at 1920x1080, is output at that size; whether the console pads it is unverified. `sceVideodec2GetPictureInfo` reports no frame cropping - [sceVideodec2Decode of an empty access unit](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) returns `0x811D010E` for a null `auData` and `0x811D010D` for an `auSize` of 0, with no picture, where it threw before. The values are `ACCESS_UNIT_POINTER` and `ACCESS_UNIT_SIZE` in the error table of [shadPS4's videodec_error.h](https://github.com/shadps4-emu/shadPS4/blob/f3f0c14c1350a768558dfd1fd058d259ccecaca2/src/core/libraries/videodec/videodec_error.h), whose libSceVdecsw codes this repository already uses; they are not measured on a PS5, and neither is whether the console leaves the decoder's state alone on that call, as this does. Media Player Classic PS5 (PPSA99015) submits empty units: its own converter reads a 4-byte length prefix of `00 00 01 xx` as a start code and drops the rest as a non-base HEVC layer. It goes on after the error. -- [sceVideodec2Decode and sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2): the decoder core's discarded flag is not modelled: a returned picture always has `OutputInfo.isDiscardedFrame` (byte 0xb) at 0, and an unshown VP9 frame returns no picture. Per a #10913 review reading firmware 13.42's module (not reproduced here), the byte is written only with a returned picture and is left as the caller set it otherwise, as here. Name and offset: [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs#L150-L192) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/83f8bfd3c9c35fcb135ef6c33dfd88bbfe066ee2/src/libs/libVideoDec2.cpp#L91-L105); shadPS4 leaves it as padding. PPSA05110 sets only `thisSize` before its VP9 Decode (0x13728dd) and Flush (0x1371f7e) and takes a picture only when the byte is 0 + +- [Picture getter argument compatibility](../../../core/libs/prx/libSceVideodec2/Export.cpp): other firmware/process modes, native pointer faults and physical-console behavior remain unverified. The common prologue follows BunnyFl's [native-code emulator report](https://github.com/boykopovar/AnyPS5/pull/10959#issuecomment-6404011201) and [clarification](https://github.com/boykopovar/AnyPS5/pull/10959#issuecomment-6404317631) for 13.42 module SHA256 `5825514d06cee0b7fdc87de4f428065ff7c2b761b40443f95ee580e1be18776d`, with `sceKernelIsPs4Process` returning 0. This was not independently reproduced here and establishes no valid metadata fields. Named getters share ordered errors, early four-byte validity/padding clears and the unsigned `frameBufferSize > pictureCount * 0x400` check. Host-write refusal throws without inventing a console fault result. + +- [HEVC picture metadata](../../../core/libs/prx/libSceVideodec2/HevcMetadata.hpp): exact console field values remain unverified. The complete 184-byte layout follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs), and syntax follows ITU-T H.265 (09/2023). Named getter wrappers share argument validation and select the supported projection from tracked decoded-frame metadata, independently of the caller's codec tag. The 184-byte HEVC and 88-byte VP9 projections retain delayed/flushed picture association. Other accepted payload versions, two-picture projections and untracked frame-buffer metadata throw after the early validity clears; their native payload behavior is not inferred from successful argument checks. + +- [AVC picture-info payload](../../../core/libs/prx/libSceVideodec2/include/Api.hpp): the existing 56-byte host prefix is not a supported native picture-info version and returns the reported structure-size error. Accepted native AVC payloads have an unimplemented tail and are not filled by these getters. H.264 decoding remains available; no padded native AVC layout or missing field values are fabricated. + + +- [Unsupported HEVC metadata projections](../../../core/libs/prx/libSceVideodec2/HevcMetadata.cpp): crop/default-display windows, temporal sublayers, multilayer streams, SPS extensions, VPS-only timing, unavailable parameter sets, unsupported SEI/NAL syntax and ambiguous picture-timing association throw at query time after the common validity-word clears and before other metadata writes. Differing parameter sets under an existing ID and malformed parameter histories keep queries unsupported through Reset until decoder recreation, since FFmpeg retains parameter state. Alternative-transfer persistence follows CLVS boundaries; conflicts, pictureless transfer messages and ambiguous continuation after reset/flush reject metadata. Unknown crop units, singular sublayer selection and console handling of these cases are not guessed. Decode behavior is retained when metadata cannot be projected. diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e3907c610c..c3a5564270 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -6,6 +6,8 @@ ### Unknown function info +- [frexpf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - follows the bundled PPSA03766 implementation: exponent-bit decomposition, conditional single-precision subnormal scaling, zero exponent for zero/NaN/infinity, and unchanged signaling NaNs. PPSA07923 agrees on results/exponents but unconditionally scales finite inputs, additionally raising overflow/inexact on large values; that SDK variant is not reproduced. Masked-exception observations cover rounding, DAZ/FTZ and sticky flags; unmasked traps and other SDK versions are unverified. + - [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. It is not exported by `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), so a payload process cannot call it there - [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Linux and macOS use `backtrace` and `dladdr`; Windows uses `_Unwind_Backtrace` and module lookup, and stops at a frame without `.ehmeta` or `.ehfram` tables whose PE unwind data has an exception or termination handler, leaving the trace empty or cut short while still returning 0; other platforms throw @@ -33,7 +35,8 @@ - [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, PosixMemalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks; `Memalign` and `PosixMemalign` forward to their `Mspace` counterparts with owner ignored and are unverified on console. `Create`'s options block (version, type 1 or 2 at +2, field at +0x10) throws when non-null as its meaning is unknown. Block footers are not modelled, so owners are ignored: on PS5 firmware 13.60 mismatched owners in `Reallocalign` or in-place `Realloc` kill the process and `MallocStats` returns 1 if non-heap-owned blocks exist, which is not reproduced. `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented. - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. -- [malloc, calloc, realloc, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. `reallocalign(ptr, 0, alignment)` for a block without that alignment was not measured and throws +- [malloc, calloc, realloc, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. +- [reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - `reallocalign(ptr, 0, alignment)` for a block without that alignment throws, where the console's heap in a payload process on PS5 firmware 13.60 returned a different block with that alignment and 0 usable bytes, `errno` untouched (a 64-aligned block given 256); whether it frees the old block is not known. There, as here, `reallocalign(ptr, 0, 16)` on a 256-aligned block returned it unchanged, `reallocalign(NULL, 0, 256)` a 256-aligned block with 0 usable bytes, and alignment 3 a moved block that kept its bytes - [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, as a payload process on a PS5 (firmware 13.60) does with the default heap (null and 0, size 0, 2^40 with `errno` 22 and the block freed, `SIZE_MAX` with `errno` untouched): size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails, the block is freed through the same allocator and null is returned with the `errno` that `realloc` left. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) @@ -79,6 +82,7 @@ - [std::thread::hardware_concurrency](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `CHrhwd8QSBs`) returns the host's `std::thread::hardware_concurrency()` (which may be 0 when unknown) instead of the console's count. The system `libSceLibcInternal` of PS5 firmware 13.60 returns the number of bits in the calling thread's `scePthreadGetaffinity` mask: 5 in a payload process whose main and second threads both had mask `0xea00`, while `hw.ncpu` and `_SC_NPROCESSORS_ONLN` read 16 and `sceKernelGetAvailableCpumask` `0x1fff`. Whether it follows a later affinity change was not measured, since `scePthreadSetaffinity` returned `0x80020001` there. libc has no way to read the libkernel mask yet. PPSA34015 imports it - [fmodf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - console `errno`, exception flags and NaN payloads remain unverified. The wrapper preserves `errno` like the existing double `fmod`, following FreeBSD 11.4 [`e_fmodf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_fmodf.c); host `std::fmod` still supplies the result and exception flags. - [_LCosh and _LSinh](../../../core/libs/prx/libc/src/Math.cpp) (libc) follow the two-argument Dinkumware CRT helpers `y * cosh(x)` and `y * sinh(x)`, including scaled large-finite-input paths; the signatures are confirmed by Dinkumware's public `ymath.h`, but the exact console results, range-error thresholds and floating-point exception behavior are not verified. +- [modf and modff](../../../core/libs/prx/libc/src/Math.cpp) (libc) - native PS5 execution, Windows and unmasked exceptions remain unverified. Linux comparisons with bundled libraries from PPSA03766 and PPSA07923 cover result and integral bits, errno, SSE/x87 flags and control state. Signaling NaNs retain their payload and quiet bit; the measured float path raises SSE invalid while the double path leaves flags unchanged. - [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. Against `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), 12 of 36 measured results differ by one unit in the last place (`_FSinh(0.5, y)` and `_FCosh(1, y)`, `_FCosh(10, y)` for y of 0.5, 1 and 2); zeros, the overflow to infinity at 89 and 90 and the tiny-argument results matched - [_Divide, _FDivide, _Recip and _FRecip](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow the Dinkumware CRT division and reciprocal contracts with IEEE-754 signed-zero, infinity and NaN behavior. The exact console results, signaling-NaN handling and floating-point exception behavior are not verified. @@ -89,7 +93,6 @@ - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name; `strftime` does not read it. - [cbrtf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_cbrtf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_cbrtf.c): a 5-bit first guess from the exponent bits, then two Newton steps in double precision. It sets no `errno`, as the `libc.prx` that titles ship does not. Floating-point exceptions are not compared. It gives the same bits as the `cbrtf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host. -- [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. On a PS5 (firmware 13.60) `__isfinitel` returned 1 for 0, -0, 1, a denormal and ±1e4000, and 0 for a NaN and for the maximum exponent without the integer bit, which this classification gives too - [logf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_logf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_logf.c), with `vzero` read at run time as msun's `volatile` read does. As in the `libc.prx` that titles ship, `errno` is set to `ERANGE` for ±0 and to `EDOM` for every argument with the sign bit set, negative NaNs and -inf included; positive NaNs and +inf leave it unchanged. Floating-point exceptions are not compared. It gives the same bits and `errno` as the `logf` of the `libc.prx` bundled with PPSA07923 and PPSA05916 for all 2^32 arguments, run on a Windows host. The FMA-contracted build bundled with PPSA13934 differs from it by 1 ulp on 961,881 arguments, so no single port matches every title's last bit. - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. @@ -104,14 +107,15 @@ - [sinf, cosf, tanf and sincosf](../../../core/libs/prx/libc/src/Math.cpp) (libc) are FreeBSD msun's [`s_sinf.c`, `s_cosf.c` and `s_tanf.c`](https://github.com/freebsd/freebsd-src/tree/release/11.4.0/lib/msun/src) over its double-precision kernels and `e_rem_pio2f.c`, with `k_rem_pio2.c` (for `|x| >= 2^28 * pi/2`) ported only for one input word and `prec` 0; `sincosf` returns the two. As in the `libc.prx` that titles ship, only an infinite argument sets `errno` (`EDOM`); NaN leaves it unchanged. Floating-point exceptions are not compared. All four give the same bits and `errno` as the copies bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host. - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. The multibyte conversions follow the C locale of the console's `libSceLibcInternal.sprx`: UTF-8 of at most three bytes, so `_Getmbcurmax` returns 3, overlong forms and encoded surrogates decode, and four-byte forms are invalid. `__mb_cur_max` stays 1 like the console's variable; `___mb_cur_max` is not in that module and also stays 1, unverified. Other locales are not supported. With a null destination, `wcstombs` returns the byte count; the console's count is wrong when the capacity argument equals the running count, because it reads past the terminator -- [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. +- [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) with two gaps. A host symbolic link with a relative target under the guest root is expanded as `realpath1` does (a dangling link fails with `ENOENT`, more than 32 links with `ELOOP`), but a link with an absolute target is not expanded, nor is anything below it, because the target is a host path with no guest path to map to. Only Windows symbolic links are expanded there, not junctions. Expansion works on the guest path, so a host link whose text climbs above a path alias root is resolved against the guest tree, not the host one. Otherwise `..` drops the previous component lexically, as `Resolve` does for every guest path. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [strtod](../../../core/libs/prx/libc/src/Strings.cpp) (libc) sets `ERANGE` for tested decimal inputs with a nonzero subnormal result, including the exact decimal expansion of 2^-1074, matching nine distinct title-bundled `libc.prx` builds measured on Windows. Two other `libc.prx` builds and the console's `libSceLibcInternal.sprx` leave `errno` alone for decimal subnormals. `wcstod` still parses through the host `std::strtod` path without this explicit rule; title and console `wcstod` behavior for decimal subnormals is unverified. The title-bundled hexadecimal subnormal rule remains unverified. - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [_Stod, _Stodx, _Stofx, _Stoldx, _Stof and _Stold](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throw for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999. A nonzero subnormal result sets neither `errno` nor the error argument, as measured in firmware 13.60's `libSceLibcInternal.sprx` run in a Linux host process, not on a console; there about a fifth of decimal inputs with a subnormal result come out one unit apart in the last place (`_Stodx("2.2250738585072014e-308", 0)` is the largest subnormal), which is not reproduced -- [_LDivide, _LRecip and _LLog](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follow the public Dinkumware CRT contracts: long-double division and reciprocal, and the `_LLog` base flag (`0` natural, positive base 10, negative base 2). The exact console results, signaling-NaN behavior and floating-point exception behavior are not verified. +- [_LLog](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libc) follows the public Dinkumware CRT contract for its base flag (`0` natural, positive base 10, negative base 2). The exact console results, signaling-NaN behavior and floating-point exception behavior are not verified. - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [atanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_atanf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_atanf.c), with `atanlo[3]` read at run time as msun's `volatile` does. Tiny arguments return `x` without raising `FE_INEXACT`. It gives the same bits as the `atanf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host, and none of those sets `errno`. Whether other titles' builds differ is unverified. +- [atan2f](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_atan2f.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_atan2f.c) over this `atanf`, with `tiny` and `pi_lo` read at run time as msun's `volatile` reads do. With two NaN arguments it returns `y`'s, as the titles' compiled `x + y` does. On 200,001,024 argument pairs (random, near-equal exponents, x near ±1, zero y, and every pair of 32 special values), run on a Windows host, it gives the same bits as the `atan2f` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934, and none of those sets `errno`. The other pairs are unverified. - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `unlink`, `rmdir`, `sceKernelUnlink` and `sceKernelRmdir` follow the same rule. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) imports nanf. - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified @@ -119,13 +123,14 @@ - [drand48 and erand48](../../../core/libs/prx/libc/src/Random.cpp) (libc) return the upper 32 bits of the new 48-bit state over 2^32, as the `libc.prx` bundled with eleven titles does on 10,000 values per function; FreeBSD returns all 48 bits. Whether the console's `libSceLibcInternal.sprx` copy does the same is unmeasured. - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [coshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows FreeBSD msun's [`e_coshf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_coshf.c), using the `expm1f` and `expf` paths already inlined for `sinhf` below; the Windows UCRT differed from the `libc.prx` copies in PPSA07923 and PPSA13934 on 500,554 and 500,614 of 20 million inputs in the #9707 survey. The exact system `libSceLibcInternal` behavior and the FMA-contracted `libc.prx` results remain unverified +- [powf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `powf` of the `libc.prx` that titles ship: FreeBSD msun's [`e_powf.c`](https://github.com/freebsd/freebsd-src/blob/release/9.0.0/lib/msun/src/e_powf.c) as of 9.0.0, which has 11.4's arithmetic but no `1**y` shortcut, so a NaN argument returns its NaN (`powf(1, NaN)` is NaN, not 1), and `(±1)**±inf` returns `y - y`, a NaN. With two NaN arguments the first one's NaN is returned, quieted. `errno` is set to `EDOM` in the two `(x - x) / (x - x)` branches (negative finite `x`, non-integer `y`), and to `ERANGE` only in the final overflow and underflow returns; the `|y| > 2^27` shortcut and the zero and infinity cases leave it unchanged. Floating-point exceptions are not compared. On 100M sampled argument pairs it gives the same bits as the copies bundled with PPSA07923 and PPSA05916, with the same `errno`; the FMA-contracted copy of PPSA13934 differs by 1 ulp on 26,405 of them. Pairs outside the sample are unverified. - [expm1](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_expm1.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_expm1.c) with `huge` read at run time, and sets `ERANGE` only when a finite argument overflows to infinity, as the titles' `libc.prx` does. It gives the same bits and `errno` as the `expm1` bundled with PPSA07923 and PPSA05916 on 320 million random and 47,222 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 43,433 of the random ones. - [log1p](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_log1p.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_log1p.c). As the titles' `libc.prx` does, it sets `EDOM` at msun's NaN return (arguments below -1, `-inf` and negative NaNs) and leaves `errno` alone for `log1p(-1)`, where it returns `-inf`. It gives the same bits, NaN payloads included, and `errno` as the `log1p` bundled with PPSA07923 and PPSA05916 on 320 million random and 68,216 structured arguments on a Windows host. The FMA-contracted build bundled with PPSA13934 differs on 73,682 of the random ones. - [nextafter and nextafterf](../../../core/libs/prx/libc/src/Math.cpp) (libc) have no physical-console or unmasked-exception validation. A Linux CPU probe compares result bits, `errno` and masked floating-point flags with the unchanged math code in FAR: Changing Tides PPSA03766 bundled `libc.prx` (`nextafter` 0x51b30, `nextafterf` 0x51c80, `_Feraise` 0x11da0): 16,864 calls across both precisions, four rounding modes and all DAZ/FTZ combinations, not exhaustive inputs. Earlier Windows sampling of eleven bundled libraries compares only bits and `errno`; exception behavior outside the FAR module remains unverified. - [tanh](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_tanh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_tanh.c) over this file's msun `Expm1`, with `tiny` read at run time; it sets no `errno`, as the titles' `libc.prx` does not. It gives the same bits as the `tanh` bundled with PPSA07923 and PPSA05916 on 16 million random and 68,211 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 1,054 of them. - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [acoshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) exact system `libSceLibcInternal` behavior and FMA-contracted copies remain unverified. Positive finite inputs use [msun](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_acoshf.c) range handling and this file's `logf`; the middle range divides -1 before addition under directed rounding. Bits, errno and SSE/x87 status and controls match the supplied PPSA03766/PPSA07923 copies on 236,592 positive finite calls (4,929 inputs, four rounding modes, DAZ/FTZ combinations and sticky flags); these samples are not exhaustive. Invalid-domain errno and SSE/x87 invalid flags are checked against two supplied bundled copies; those samples are not exhaustive either. -- [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two +- [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). `tgammaf` delegates to the host's double `tgamma` as FreeBSD 11 does ([`s_tgammaf.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_tgammaf.c)), then restores the sign of a zero result for negative non-integer inputs from the parity of `floor(-x)`; nine title-bundled `libc.prx` builds return that sign for underflowing inputs, where Windows UCRT can return the opposite sign ([#9707](https://github.com/boykopovar/AnyPS5/issues/9707)). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [acosf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - for |x| > 1 and NaN the console stores `EDOM` and raises `FE_INVALID` before returning `(x - x) / (x - x)`; here those return the same NaNs and leave `errno` as it was (#3688). For |x| <= 1 it follows the `acosf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_acosf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_acosf.c), with the same constants, `pio2_lo` read at run time and the `0xfffff000` mask on `sqrtf(z)`, which the binary holds. Titles ship different builds of that `libc.prx`: PPSA13934's is FMA-contracted and differs from this one by 1 ulp on 129,170 inputs (#9668), so no single port matches every title's last bit. - [asinf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - for |x| > 1, the infinities and NaN, it stores `EDOM` before returning `(x - x) / (x - x)`, as the `libc.prx` bundled with eleven titles does, PPSA13934's included. Tiny arguments return `x` without raising `FE_INEXACT`. Otherwise this is FreeBSD msun's [`e_asinf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_asinf.c), which gives the same bits as the `asinf` of the `libc.prx` bundled with PPSA07923 and PPSA05916 for all 2^32 arguments (run on a Windows host). The FMA-contracted builds bundled with PPSA13934 and PPSA09614 differ from it by 1 ulp on 391,714 arguments, so no single port matches every title's last bit. @@ -141,9 +146,9 @@ - On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps the range up to 0x70_0000_0000 unmappable for every process (max protection none). It starts at 0x10_0000_0000 on macOS 26.5 (M1 Pro, arm64 and x86_64 under Rosetta) and at 0xF_C000_0000 on macOS 27 (27.0.1 on an M4 Pro, arm64 and x86_64 under Rosetta; 27.2 on an M2 Max, arm64). With dyld and the Rosetta runtime near 0x2_0200_0000 and 0x3_0400_0000, a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way -- [`fopen` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows: `freopen` onto a directory in a read mode fails with `EACCES`, where FreeBSD opens it. Reading a directory stream from `fopen` fails with `EISDIR` and sets the error indicator on every host, as FreeBSD 11 tmpfs does; FreeBSD 11 FFS returns the directory's raw blocks instead, and what the console's filesystems return is unverified +- [`fopen` and `freopen`](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - in a payload process on a PS5 (firmware 13.60) a directory opens in a read mode (`r`, `rb`, `rw`) and `fread` returns its raw directory blocks (`/data`, `/system`) or fails with `EIO` and sets the error indicator (`/`); `freopen` onto a directory in a read mode succeeds too. Here reading a directory stream fails with `EISDIR` and sets the error indicator on every host, since the host's directory blocks are not the console's, and on Windows `freopen` onto a directory fails with `EACCES`. The other measured answers match: `EISDIR` for a directory in a write or update mode, `EEXIST` for `wx` on an existing file, `EINVAL` for an unknown or empty mode, and `ENOENT` for a missing file - [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry -- [Writes on an update stream](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows go through the UCRT, which drops a write that directly follows a read without `fflush`, `fseek`, `fsetpos` or `rewind` unless the read reached end of file: `fwrite` returns 0, and `fputc` and `fprintf` report success, without the error indicator. FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74) discards the read buffer and writes at the descriptor's offset, past a read-ahead of `st_blksize` bytes for a regular file, so where the console writes depends on its block size. Reads directly after writes follow FreeBSD's `__srefill` +- [Writes on an update stream](../../../core/libs/prx/libc/include/FileStream.hpp) (libc) directly after a read differ from the console in where they land. Windows follows FreeBSD 11.4's [`__swsetup`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/wsetup.c#L59-L74): the read buffer and `ungetc` data are dropped and the write goes to the descriptor's offset, but past the UCRT's read-ahead (4096 bytes by default, 2 unbuffered) where the console reads `st_blksize` bytes (1 unbuffered), so the two agree while the rest of the file fits in both. Linux glibc writes at the stream position: after `fgetc` on an `r+` stream over `0123456789`, `fputs("XY")` gives `0XY3456789` where FreeBSD gives `0123456789XY`; matching FreeBSD there needs the stream to track whether it was positioned since its last read. Reads directly after writes follow FreeBSD's `__srefill` - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way - [RemoveTree](../../../core/libs/prx/libc/include/RemoveTree.hpp) (libc) removes a host tree itself on Windows instead of calling `std::filesystem::remove_all`: MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link, so `remove_all` recurses through it and never terminates, and any tree that holds a symlink hangs. It builds the entry list from `GetFileAttributesW`, removes a reparse point as the link it is rather than its target, clears the read-only attribute and retries as `remove_all` does, and reports the first native error. The libstdc++ fix is in GCC 17 (commit 88856ad855ae); other hosts call `remove_all`. - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked @@ -204,3 +209,4 @@ - [fprintf_s, vfprintf_s, vprintf_s, fwprintf_s, wprintf_s, vfwprintf_s and vwprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follow C11 K.3.5.3.1, K.3.5.3.10 and K.3.9.1.1 with the constraints of `snprintf_s` and `swprintf_s`: a null stream, format or argument list, any `%n`, and a null `%s`, `%ls` or `%S` argument return -1 and write nothing, because the output is formatted completely before it is written. A violation only returns: the constraint handler is not invoked, `errno` is untouched, and the value is the one the narrow `snprintf_s` returns on this branch, not a result measured for a stream function on a PS5. A closed stream sets `EBADF` (9) as `fprintf` does. `vprintf_s` and `vwprintf_s` write to the guest standard output stream, and the wide forms count characters while the narrow ones count bytes, as their unchecked siblings do - [scanf_s, fscanf_s, vscanf_s and vfscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) run the scanner of `sscanf_s` over the stream, taking the size of `%s`, `%c` and `%[` as `unsigned int` after the pointer, so everything listed for `sscanf_s` applies, including the count of items converted before an undersized destination, whose first element is set to zero. Each directive is read from the stream with the host `fscanf`, with one character of look-ahead pushed back, so the stream stops after the last character a conversion used. A null stream, null format or null destination returns `EOF`, a closed stream also sets `EBADF`, and the constraint handler is not invoked. The 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) throw on Linux, as in `fscanf`. None of the stream forms were measured on a PS5, and the size type follows `sscanf_s` rather than `rsize_t` - [freopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows C11 K.3.5.2.2 through `freopen`: a null result, mode or stream returns `EINVAL` (22) with `*result` set to null when it exists, and a null name is passed to `freopen`, which reports mode-only reopening as unsupported (`ENOTSUP`, 45) with `*result` null, and the constraint handler is not invoked, as in `fopen_s`. A successful reopen stores the stream in `*result` and returns 0, and a failed one stores null and returns the errno `freopen` left, closing the stream as `freopen` does. The console's return values are unverified +- [logb and logbf](../../../core/libs/prx/libc/src/Math.cpp) (libc) do not compare floating-point exceptions, and their double inputs are checked by sampling, not exhaustively. They are FreeBSD msun's [`s_logb.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_logb.c) and [`s_logbf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_logbf.c): ±0 returns `-inf` without `errno`, and a signalling NaN is returned quieted. The `libc.prx` bundled with eleven titles (PPSA04158 to PPSA35169) gives the same bits and no `errno` on 4,032,768 inputs per function on a Windows host. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index ee3f857f6d..0fe8091311 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -8,7 +8,7 @@ - [pthread_mutexattr_setprotocol, pthread_mutexattr_getprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A payload process on a PS5 (firmware 13.60) accepts protocols 0 to 2, starts the ceiling at 767 when protocol 2 is set, accepts ceilings 256 to 767 and returns `EINVAL` for other protocols and ceilings and for a null, uninitialized or destroyed attribute; a null output of `pthread_mutexattr_getprotocol` crashed it. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority - [sceCoredumpSetUserDataType](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Uxqkdta7wEg`) - `(int type)` follows the PS5 13.60 `libkernel.sprx` at 0x138B0: the sign-extended `type` goes to syscall 601 (command 29, third argument 0), and the raw result comes back with no SCE code mapping, so a kernel error is -1 with `errno` set. Which types the kernel accepts is not measured. No dump is produced here, so every type returns 0. Palworld (PPSA23126, #2598) and Hello Neighbor (PPSA35154, discussion #6991) import it; #4144 left it throwing because no signature was known. PPSA02225 also imports it: its one call (at 0x6a1b2) passes 1 and does not read the result. - [pthread_rwlockattr_settype_np](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - stores types 1 and 2 through `scePthreadRwlockattrSettype` and returns 0, but the type is not applied: `scePthreadRwlockInit` ignores its attribute. Any other type, 0 included, and a null attribute or object return `EINVAL` and store nothing, as in [BunnyFl's measurements](https://github.com/boykopovar/AnyPS5/pull/9632#issuecomment-6363402814) of firmware 13.42's libkernel. Only the name is public; the `(attr, int)` signature follows the SCE twin and shadPS4, which also registers it as `scePthreadRwlockattrSettype` -- [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched +- [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, where a payload process on a CFI-2014 PS5 (firmware 13.60) got 0x840f80, 1 and 800000000. The SoC id may differ between console models, and the CPU mode of a title may differ from a payload's (shadPS4 takes the PS4's from the param.sfo flags), so neither is copied. `sceKernelGetMainSocId` ignores a pointer argument there, as here. PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`) - [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified - [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - internal pool allocation and statistics are not implemented, so a non-null query throws without changing its output. Native null output returns -1; an absent pool returns -2, a failed statistics query returns -3, and success writes total pool bytes minus used bytes. - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) - the native zero-argument query returns bit 3 of the process sanitizer flags loaded from sysctl `{1, 14, 41}`. The host always reports disabled because guest sanitizer activation is not modeled. @@ -54,13 +54,14 @@ - [sceCoredumpConfigDumpMode](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `gzLt9Qrauk0`) - `(int mode)` follows the same `libkernel.sprx` at 0x134D0: a mode other than 1 returns `0x81180000`; mode 1 issues syscall 601 (command 16, arguments 0 and 1) and returns 0 whether or not it fails, since only a raw return of 22 would map to `0x81180000`. No dump is produced here, so mode 1 returns 0 without the syscall. PPSA16833 imports it - [sceCoredumpGetStopInfoGpu_Agc](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Jrs7UUkGOFo`) - `(void* info, size_t size)` follows the same `libkernel.sprx` at 0x137E0: a null `info` returns `0x81180000`; syscall 601 (command 21) failing with `EPERM` returns `NOT_IN_COREDUMP_HANDLER`; otherwise two `libSceAgcDriver` imports (NIDs `+NIAY3DWpOw` and `LgctzJSGy7s`, names unknown) decide between `0x81180005` and, for a `size` of 8, storing the second one's 64-bit result and returning 0, with any other size returning `0x81180000`. The handler is never invoked here, so a non-null `info` returns `NOT_IN_COREDUMP_HANDLER` without writing, assuming `EPERM` outside the handler as above; the AGC branch is not reached - [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - returns the main program's first segment, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L343-L355) does through its `sceKernelGetLibkernelTextLocation`. In a payload process on a PS5 (firmware 13.60) both returned 0 with address and size 0, and a null result faulted (here it throws); what a title process gets is not known. 44 audited titles that bundle their own `libc.prx` import it; no call has been observed +- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - reports the mapped image path as the name and answers for any mapped region, where a payload process on PS5 firmware 13.60 got an empty name for libkernel and libSceLibcInternal and `0x80020003` (ESRCH), with `st_size` cleared to 0, for an address in the payload, which is in no loaded module. Segment 0 matched each module's first segment there - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, and on macOS from the relinked image's program headers; both report an `eh_frame_size` that includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped after a loader free (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release) when the module is no longer loaded. A table changed in place, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call - [sceKernelGetModuleInfoFromAddr](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - as in a payload process on a PS5 (firmware 13.60), flags 1 finds any module and flags 2 refuses the system ones (`ESRCH`), here the host libraries; other flags clear the 0x1A8 bytes and return `EINVAL`, any failure clears them, `st_size` is left 0, and a null info returns `EFAULT` (with other flags the console faulted; here it throws). Flags 2 finding a title module was not measured. The layout follows [shadPS4](https://github.com/shadps4-emu/shadPS4). The id is the loader handle or a stable id from that range, and the name is the host file name without `.guest.prx`, where the console keeps `.sprx`. Only the first 4 load segments are reported. `tls_offset` is 0 and `ref_count` 1 (the console gave 5 for system modules). On Windows a relinked image reports its `.elf` sections and the `.ehmeta` eh_frame_hdr; the TLS fields and init/fini are 0 - [sceKernelGetModuleList](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order with the ids `sceKernelGetModuleInfoFromAddr` reports, where a payload process on PS5 firmware 13.60 got only its main program, as handle 0, from both `sceKernelGetModuleList` and `sceKernelGetModuleList2`, though libkernel (handle 1) and libSceLibcInternal (handle 2) were loaded; what a title gets was not measured. Otherwise it follows that console: a null handle array returns `EFAULT` without writing `actual`; with more images than `count` the first `count` handles are written and `ENOMEM` returned without writing `actual` (nothing is written for a count of 0); a null `actual` returns `EFAULT` after the handles are written. The `(int32_t* handles, size_t count, size_t* actual)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264) - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it - [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`, as a payload process on PS5 firmware 13.60 got for libkernel's handle, a size of 0, a null info and a bad handle. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Which modules the console lists is in the entry above. PPSA34063 imports both and fails to load without them -- [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. +- [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - returns 0 and the main program's first segment, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348) does, where a payload process on PS5 firmware 13.60 got 0 with address 0 and size 0, twice, though libkernel's text was at 0x800000000; what a title gets is not measured. A null address or size faulted there and throws here. libkernel.sprx and libkernel_sys.sprx export the function at the same address - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. The host pages behind a released range stay committed until `sceKernelMunmap`, as `madvise` keeps them for every advice; freeing them would lose the contents the console still returns. - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [sceKernelAvailableDirectMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - its arguments follow a payload process on a PS5 (firmware 13.60) ([probes](https://gist.github.com/orezraey/42a06a7ea0d9fb57ac9c06eaf9e6756c)). One answer is not modelled: in a process that had not yet called `sceKernelGetDirectMemorySize` or otherwise touched direct memory, the console returned `EBADF` (`0x80020009`); here the call always searches the pool. Titles open the direct-memory device during start-up, so they are not expected to see it @@ -70,19 +71,16 @@ - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected - [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the mutex, condition variable, read-write lock, legacy lock and `SEM2` operations throw, and so does a misaligned object. Implemented as FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE`, `UMTX_OP_NWAKE_PRIVATE`, `UMTX_OP_SEM_WAIT` and `UMTX_OP_SEM_WAKE` (the last two built under `COMPAT_FREEBSD9`/`10`). Operations 0 and 1 fail with `EOPNOTSUPP`, as measured on a PS5 (firmware 12.70). The `SEM2` operations, 23 and 24, were measured on the console and returned `0` for a count of 0 where FreeBSD would time out, so they stay unimplemented. - [sceKernelCreateSema, sceKernelPollSema, sceKernelSignalSema, sceKernelDeleteSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) - a poll, a signal or a second delete of a deleted semaphore reads freed memory here, where the console returns `0x80020003` (ESRCH) ([measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernel.golden.tsv#L17-L19)). A null output pointer to `sceKernelCreateSema` (a fault on PS5 firmware 13.60) and attributes 0x100 to 0x102 (accepted there, effect unknown) throw. Other attributes above 2 and negative maximum and signal counts return `EINVAL` here, as there. `EINVAL` for an initial count above the maximum or negative, a maximum of 0, a null name, a poll of 0 or above the maximum and a signal of 0 is [measured](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernel.golden.tsv#L10-L23). -- [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored -- [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed +- [sceKernelSyncOnAddressWait, sceKernelSyncOnAddressWait8/16/32/64 and sceKernelSyncOnAddressWake](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - follow a payload process on a PS5 (firmware 13.60), not a title. Only a null address is checked for `EFAULT`: any other unreadable address is read and faults the host, where the console returned `EFAULT` for address 0x10. The time left that a woken wait writes back is taken from the host clock, so it can differ from the console's by the cost of the call - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded -- [scePthreadMutexTimedlock](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp), [scePthreadRwlockTimedrdlock, scePthreadRwlockTimedwrlock](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - a timed lock of a mutex the caller holds, or of a rwlock it holds for writing, sleeps until its timeout before returning `ETIMEDOUT` (`0x8002003c`), as FreeBSD 11.4 libthr does ([mutex](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L878-L928)); whether the console waits is not measured. The prosper oracle measured only the result, for a default mutex and a rwlock with a 1000 us timeout ([kernelx.golden.tsv](https://github.com/mattias800/prosper/blob/3ea5cb9a846b6dcd00f5073360462bbda4f72288/prosper/tests/data/console_oracle/kernelx.golden.tsv#L16-L47)); the other mutex types and the POSIX timed locks follow the same rule unmeasured. - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - the mutex's owner is checked before waiting, as in FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029). Measured in a payload process on a PS5 (firmware 13.60): `EPERM` (`0x80020001`) for an unowned mutex of every type, the adaptive initializer (slot 1), an uninitialized slot and a mutex owned by another thread, `EINVAL` for the destroyed mutex (slot 2), and a recursive mutex is released completely and relocked to its depth. `scePthreadCondDestroy` stores 1, which every condition call refuses with `EINVAL`, and leaves an uninitialized slot alone. Cancellation restores the mutex and depth before cleanup and a canceled waiter forwards a wake, per POSIX; its ordering is not measured -- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. A null output with the name of an openable flag returns `SCE_KERNEL_ERROR_EFAULT`, the host's answer to the console's fault there, not a measured code. With a plain flag's name it returns `SCE_KERNEL_ERROR_ESRCH`, as measured; with a missing name it also returns `SCE_KERNEL_ERROR_ESRCH`, which was not measured. -- [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - when a call has both an invalid attribute and a name of 32 bytes or more, `SCE_KERNEL_ERROR_EINVAL` is returned, because the attribute checks run before the name length check. The console was only measured with long names and valid attributes (0x20 and 0x120), so which error it returns first is not known. Event flag handles are host addresses here, while the console returns small IDs. Of 114 call sites read in 12 titles (PPSA02530 not included), single sites pass the attribute 0x10 (PPSA24779 0x943887) and 0x22 (PPSA30601 0x16157f1), and 0x21 in PPSA04354 (0xee7dc7) and PPSA35169 (0x5a539e), all with the initial value 0; how the attributes are distributed over the other sites is not shown. +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. A null output with the name of an openable flag returns `SCE_KERNEL_ERROR_EFAULT`, the host's answer to the console's fault there, not a measured code. With a plain flag's name it returns `SCE_KERNEL_ERROR_ESRCH`, as measured; with a missing name, or a null output and a missing name, it returns `SCE_KERNEL_ERROR_ESRCH`, as a payload process on PS5 firmware 13.60 got. +- [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - event flag handles are host addresses here, while the console returns small IDs (0x22 for the first in a payload process on PS5 firmware 13.60). That console checks the attribute before the name length, as here: attributes 0x3, 0x30, 0x40 and 0x200 return `SCE_KERNEL_ERROR_EINVAL` with a 32-byte name too, and attributes 0 and 0x100 return `SCE_KERNEL_ERROR_ENAMETOOLONG` with it. Of 114 call sites read in 12 titles (PPSA02530 not included), single sites pass the attribute 0x10 (PPSA24779 0x943887) and 0x22 (PPSA30601 0x16157f1), and 0x21 in PPSA04354 (0xee7dc7) and PPSA35169 (0x5a539e), all with the initial value 0; how the attributes are distributed over the other sites is not shown. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - one id per request for the Multiple submits and wait mode 2 returning once one request completed follow shadPS4. Requests run synchronously on submit, so cancel never finds one processing. The console's ids are not the small numbers used here (it returned 33, 0x2021, 0x4021) - [sceKernelWaitSema, sceKernelPollSema, sceKernelSignalSema, sceKernelCancelSema, sceKernelDeleteSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) - `SCE_KERNEL_ERROR_ESRCH` for a null handle follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/60c42b925c3d1e00008621b8587819999260ec31/src/core/libraries/kernel/threads/semaphore.cpp#L329-L366) (PS4), unverified on PS5. Handles are pointers, so a non-null handle that is not a live semaphore is not detected; shadPS4 returns `ESRCH` for any unallocated handle. - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - a new attribute holds a 1 MiB stack size here and 64 KiB on the console, because host code runs on guest threads; a null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` throw, where the console crashed. Measured on a PS5 (firmware 13.60): a null or uninitialized attribute, and a null output of the stack size, detach state, scheduling parameter, policy and inheritance getters, return `EINVAL`; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted; a new attribute holds policy 1 and inheritance 4 - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - `scePthreadAttrGet` reports the inheritance a thread was created with and the policy and priority it runs with, as a payload process on a PS5 (firmware 13.60) does: 4 with the creator's policy and priority for an inheriting thread, 0 with the attribute's for an explicit one, and policy 1, inheritance 4 and priority 700 for a thread created with a freshly initialised attribute object; a null attribute was not measured. `pthread_attr_get_np` returned `EINVAL` for the payload's main thread, so its answer for other threads is not measured; it reports the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) - [pthread_getprio, scePthreadSetprio and pthread_setprio](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - measured in a payload process on a PS5 (firmware 13.60): `pthread_getprio` returns the priority, -1 with `ESRCH` for a joined thread and -1 with `EINVAL` for a null one; the setters accept a priority whose low 16 bits are 256 to 767 or 896 to 959 (so 65792 and -65280 too, stored as given), return `EINVAL` otherwise and set `errno` to it, and return `ESRCH` for a joined thread. As in firmware 13.42's `libkernel` run in an emulator, a null thread returns `EINVAL` and a finished or joined thread `ESRCH` before the priority is checked, both leaving `errno` alone; a finished thread that is not joined yet was not measured on a console. The guest priority is recorded and reported, not applied to the host thread -- [pthread_rwlock_reltimedrdlock_np, pthread_rwlock_reltimedwrlock_np](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) take `(rwlock, usec)`, a relative timeout in microseconds, and are `scePthreadRwlockTimedrdlock` and `scePthreadRwlockTimedwrlock` with POSIX error numbers, so they return what those return (`ETIMEDOUT` when the time runs out, at once for 0 while another thread holds the lock, and `EINVAL` for a destroyed lock); a zero lock is initialized as by `pthread_rwlock_rdlock`. FreeBSD 11.4 libthr has no such functions; the signature comes only from shadPS4, which registers one function for each NID and its `scePthreadRwlockTimed*` twin ([`rwlock.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/rwlock.cpp#L167-L179), 32-bit `usec`). Unverified on the console - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it @@ -146,7 +144,7 @@ - [Linux runtime module initialization](../../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) (libkernel) - modules in explicit runtime directories can be mapped for bindings before an explicit guest load runs their entry and init array once with the caller arguments. Finalizers run only after initialization completed. Console dependency-start and finalization ordering are unmeasured. Windows and macOS use their existing lifecycle paths. - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. -- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute +- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, in the POSIX `mprotect` as well (FreeBSD 11.4 masks the argument, `sys/vm/vm_mmap.c:613`; the console was measured only through `sceKernelMprotect`), as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`); here both throw for undefined bits - [JIT shared memory on macOS](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - the object is an unlinked `mkstemp` file in the temporary directory, as `shm_open` makes its anonymous objects. macOS refuses an executable shared file mapping (`EPERM`), so an executable view is mapped without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view after it ran from another keeps its old translation until the executable view's protection changes; first writes and a single read-write-execute view, as XPSemu PPSA97358 and VLC PPSA85300 map, are seen. `mmap` of a JIT descriptor is not supported, as file-backed guest `mmap` throws on macOS. - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing here is taken from a console except what the kevent measurements entry lists. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters except `EVFILT_AIO`, `EVFILT_LIO`, `EVFILT_SENDFILE` and `EVFILT_PROCDESC` (see the kevent measurements entry), `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry) and every change to an existing timer except `EV_DELETE` and the `EV_ADD` re-adds that the kevent measurements entry describes throw. A `NOTE_NSECONDS` period above one second with a fraction is kept to the nanosecond, where 11.0 converts the fraction with the microsecond macro. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event fires once the send buffer's free space reaches the mark, as `filt_sowrite` does with `sbspace`; the host reports that space as `SO_SNDBUF` less `SIOCOUTQ` on Linux and as `SO_SNDBUF` alone elsewhere, so off Linux it does not shrink while data is queued. Marks below 1, which make FreeBSD's read filter always fire, throw @@ -160,6 +158,7 @@ - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is treated as a link only when `ReadJunction` in [WindowsSymbolicLink.hpp](../../../core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp) accepts a drive-path target (`X:\...`) after stripping `\??\` / `\\?\`; volume mounts and other non-drive substitutes are not. Consumers of that drive-target path: [NativeStat.cpp](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (`lstat`/`readlink`), [Open.cpp](../../../core/libs/prx/libkernel/File/src/Open.cpp) (`O_NOFOLLOW` → `EMLINK`, and `unlink` removing the junction), [Stdio.cpp](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (`rmdir` → `ENOTDIR`), and [Export.cpp](../../../core/libs/prx/libkernel_sys/Export.cpp) (`unlinkat` with and without `AT_REMOVEDIR`). `unlink` / plain `unlinkat` leave the target, including when it is not empty - [lstat of a Windows reparse point that is not a link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a mounted volume (`\??\Volume{GUID}\`) or another name surrogate as the reparse point itself, not followed, with the mode of its attributes, as CPython's `os.lstat` does, and a reparse point without the name surrogate bit (deduplication, cloud files) as `stat` reports it +- [sceKernelMapDirectMemory with execute](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a protection with CPU execute (bit 0x4) returns `EINVAL` (0x80020016) and maps nothing, and `sceKernelMprotect` grants execute afterwards, as [PS5_PayloadSDK measured](https://github.com/mihawk-99/PS5_PayloadSDK/blob/3e31e0b1b68ada28beca68adc480cf0c3d17b9df/platform/docs/PROBE.md#L28-L34) in a title on firmware 12.09 for read-execute and read-write-execute. `sceKernelMapDirectMemory2`, `sceKernelMapNamedDirectMemory`, and `sceKernelBatchMap` and `sceKernelBatchMap2` with operation 0 share the check here, which is not measured for them, nor for a title with more privileges than a homebrew one. - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. With `SIG_IGN` an expiry is dropped. With `SIGALRM` blocked it is left on the pending set of `sigtimedwait`, as `kern_psignal` does, where `sigwait`, `sigtimedwait` and `sigwaitinfo` take it; unblocking it runs a guest handler on the unblocking thread, and with the default disposition exits the process with status 142. Unblocked, the default disposition exits the process with status 142 and a handler throws, since guest `SIGALRM` handlers are never run from the timer thread. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified @@ -196,6 +195,7 @@ - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`; FreeBSD keeps a mask per thread +- On macOS the guest runs on a second thread and the main thread serves the main dispatch queue from its run loop: AppKit (SDL's Cocoa windows, Metal surfaces, event pumping and game controllers) only works on the main thread, and a guest never hands its thread back. The macOS entry stub of the relinker starts the guest through `Aps5StartGuest` ([MainThreadMacOs.cpp](../../../core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp)), on a thread with the stack size of the main thread (at least 8 MiB), and the guest ends the process with `exit`; a guest entry that returns aborts. libSceVideoOut sends its SDL calls to the main thread with `Aps5RunOnMainThread` (`OnMainThread` in [MainThread.hpp](../../../core/libs/prx/libSceVideoOut/include/MainThread.hpp)), which waits for them and rethrows their exceptions; a host program that never called `Aps5StartGuest` runs them in place - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs a queued delivery (an APC) in the first alertable wait of libkernel's TimedWait the target enters, or where libkernel drains APCs after releasing its address-wait or park queue lock (both are taken under a delivery deferral). Host code that holds a lock of its own across such a wait keeps it held while a waiting handler runs - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows throws for a target thread that was created suspended and is not resumed yet. FreeBSD 11.4 libthr starts such a thread with every signal but `SIGTRAP` blocked ([`thr_create.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L177-L183)) and restores its mask only once it is resumed ([`thread_start`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_create.c#L252-L294)), so a signal sent to it stays pending until then. The console's behaviour is unverified @@ -249,14 +249,14 @@ - [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` throws, as FreeBSD re-arms it only when the watched kqueue queues a note. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, eight `IPPROTO_TCP` options, `IPV6_V6ONLY` and ten `IPPROTO_IP` options. `SO_ACCEPTFILTER` (0x1000) fails with `EINVAL` for a set and a get on a socket that is not listening, as a payload process on a PS5 (firmware 13.60) did and as FreeBSD 11.4's `do_setopt_accept_filter` and `do_getopt_accept_filter` do; on a listening socket both throw, as accept filters are not modelled; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden -- [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener as FreeBSD 11.4's `syncache_socket` does, which was not measured -- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR`, `SO_REUSEPORT`, `SO_TIMESTAMP` and `SO_BINTIME` change what the socket does. `SO_REUSEADDR` and `SO_REUSEPORT` are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if it is FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener, as FreeBSD 11.4's `sonewconn` copies `so_options`; that was not measured +- [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener, as FreeBSD 11.4's `syncache_socket` does and as measured on that console +- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR`, `SO_REUSEPORT`, `SO_TIMESTAMP` and `SO_BINTIME` change what the socket does. `SO_REUSEADDR` and `SO_REUSEPORT` are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if it is FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener (FreeBSD 11.4's `sonewconn`); measured only for `SO_REUSEADDR` and 0x10000 - [IP_ADD_MEMBERSHIP, IP_DROP_MEMBERSHIP, IP_MULTICAST_IF](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - set on the host socket after the checks a PS5 (firmware 13.60, payload process) showed: `EOPNOTSUPP` on a stream socket, `EINVAL` for a short value or a group outside 224.0.0.0/4, `EADDRINUSE` for a group joined twice and `EADDRNOTAVAIL` for one not joined, tracked per socket. An interface the host lacks fails with the host's error (`EADDRNOTAVAIL` on the console). A group joined through `INADDR_ANY` and through that interface's address counts as two here. Not measured, after FreeBSD 11.4: a drop through `INADDR_ANY` leaves the first such group on any interface (`inp_leave_group`), and the other multicast options fail with `EOPNOTSUPP` on a stream socket (`inp_setmoptions`). An `ip_mreqn` with a non-zero index or address throws, as only a zeroed one was measured, and so does reading `IP_MULTICAST_IF` into 12 bytes, where FreeBSD returns an `ip_mreqn` - [TCP_MAXSEG](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a connected socket reads 536 (1220 for `AF_INET6`) until a segment is set, where a payload process on PS5 firmware 13.60 read the negotiated segment (16344 over loopback, lo0's MTU less 40) on both ends; the segment is not set on the host socket, and a listening socket was not measured. Otherwise it follows that console: a fresh socket reads 536, or 1220 for `AF_INET6`, and fails every set with `EINVAL` (175, 176, 200, 400, 536, 537, 1460 tried); a connected one fails a segment above the current one or below 176, and stores the rest raised to 216 (176 and 200 both read back 216), as FreeBSD 11.4's `tcp_default_ctloutput` with `V_tcp_minmss` does - [TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket and read back as a payload process on a PS5 (firmware 13.60) did: 7200, 75 and 8 at first, 0 restoring a keepalive default, and a count stored as an unsigned value. They are not set on the host socket, so the host's keepalive timers are its own while `SO_KEEPALIVE` is applied there. An interval above 4294967 seconds fails with `EINVAL`, after FreeBSD 11.4's `tcp_default_ctloutput` with `hz` taken as 1000, and an accepted socket takes the keepalive values from its listener as `syncache_socket` copies them; neither was measured - [IP_TTL, IP_TOS, IP_MULTICAST_TTL, IP_MULTICAST_LOOP](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per `AF_INET` socket with the defaults (64, 0, 1, 1), the low-byte storage of `IP_TTL` and `IP_TOS`, the one-byte forms and the `EINVAL` above 255 of the multicast options that a payload process on a PS5 (firmware 13.60) showed. `IP_TTL` and the multicast options are also set on the host socket, except a time to live of 0, which hosts refuse; `IP_TOS` is only kept, so the host's packets carry its own type of service, and the host's default time to live (128 on Windows) is not changed to 64. An accepted socket takes `IP_TTL` and `IP_TOS` from its listener, as FreeBSD's `syncache_socket` does, and an `int` `IP_MULTICAST_LOOP` keeps only its low byte, as FreeBSD 11.4's `inp_setmoptions` does; neither was measured. Winsock applies `IP_MULTICAST_LOOP` on the receiving socket, not the sender, so on Windows the receiver's setting decides loopback -- [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60); the host socket gets the size asked for, so a request above the limit still enlarges the host's buffer. The host's real buffering, FreeBSD's automatic growth of a connected TCP socket's buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) does; that was not measured -- [SO_NOSIGPIPE](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket and read back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal, and an accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. Not measured on a console +- [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60); the host socket gets the size asked for, even above the limit. A connected TCP socket keeps the sizes asked for, where that console rounds a buffer larger than the segment up to a multiple of it, as FreeBSD's `tcp_mss` does (a listener's 100000 and 50000 read 114408 and 65376 on the accepted socket, and the client's defaults 49032 and 81720, with a 16344-byte segment). The host's real buffering, FreeBSD's automatic growth of those buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's `sonewconn` does and as measured for the low-water marks +- [SO_NOSIGPIPE](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket and read back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal, and an accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options and as measured on PS5 firmware 13.60. Whether a write on it raises `SIGPIPE` there was not measured - [SO_SNDTIMEO, SO_RCVTIMEO](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the value checks and the 32.32 fixed-point read-back follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689), as a payload process on a PS5 (firmware 13.60) answered them. A receive or send on a blocking socket waits that long for the host to report the socket readable or writable and fails with `EWOULDBLOCK`, but only before the first transfer: once writable the host send has no limit, where FreeBSD returns the bytes sent so far, and a receive whose data another thread takes after the wait blocks again. `sendfile` ignores the send timeout, the host wait has millisecond steps, and an accepted socket takes both from its listener - [IPV6_V6ONLY](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - the `IPPROTO_IPV6` option 27 is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL`, as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as a payload process on a PS5 (firmware 13.60) reported, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The value checks and the bound-socket answer were not measured - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again, and `getsockname` reports that host address. On Windows a `sendto` to a non-loopback address from such a socket then fails with `ENETUNREACH` (Winsock 10051, measured on Windows 10), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had @@ -292,11 +292,9 @@ - [pthread_mutexattr_setkind_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) throws for a kind outside 1 to 4. FreeBSD 11.4's [`_pthread_mutexattr_setkind_np`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutexattr.c#L108-L120) stores any kind and [`mutex_check_attr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_mutex.c#L207-L218) then makes `pthread_mutex_init` fail with `EINVAL`, but `pthread_mutex_init` here does not check the type - [pthread_cond_signalto_np, scePthreadCondSignalto](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) wake every waiter of the condition variable, so the other waiters see a spurious wakeup, where a payload process on PS5 firmware 13.60 woke only the target and the other waiter of two timed out; waking one thread would need a wait per waiter in the condition variable every wait shares. A thread handle that does not exist returns `EPERM` here, where that console returned `ESRCH`. Otherwise they follow that console: a null thread returns `EINVAL` and a thread that is not waiting on the condition variable `EPERM`, which a registry of each condition variable's waiting threads answers. A thread joins it before it releases the mutex, so a caller holding that mutex wakes it; a caller that does not can find it registered before it blocks and lose the wakeup, as `scePthreadCondSignal` can - [pthread_rwlockattr_gettype_np, scePthreadRwlockattrGettype](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) report the type stored by `scePthreadRwlockattrSettype`, which accepts any value and which read-write locks do not apply; the console's valid types and their effect are unverified. A null or uninitialized attribute throws, as for `pthread_rwlockattr_getpshared` -- [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console - [pthread_mutex_setname_np](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - the console passes `name` to the kernel's `namedobj_create` and stores the returned id instead of the name; here the name is copied and never read back. Once named, a mutex returns `EAGAIN` (35) for every later call and keeps its name, as [firmware 13.42's module run on a PC](https://github.com/boykopovar/AnyPS5/pull/10385#issuecomment-6415603047) does. The kernel's handling of a null `name` is unmeasured: here it returns 0 and leaves the mutex unnamed. A null mutex pointer faults in the module and throws here; a statically initialized or destroyed mutex also throws, and the module's result for those is unmeasured. The module's separate branch for a field holding `0x5eaf00d`, which copies the name into the object, is known only from disassembly and is not reproduced - [pthread_rwlock_setname_np](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - the console passes `name` to the kernel's `namedobj_create` and stores the returned id instead of the name; here the name is copied and never read back. Once named, a lock returns `EAGAIN` (35) for every later call and keeps its name, as [firmware 13.42's module run on a PC](https://github.com/boykopovar/AnyPS5/pull/10385#issuecomment-6415603047) does. The kernel's handling of a null `name` is unmeasured: here it returns `EINVAL`, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/kernel/threads/rwlock.cpp#L79-L87). A null lock pointer, a statically initialized lock and a destroyed one fault in the module and throw here - [pthread_barrier_setname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - the console passes `name` to the kernel's `namedobj_create` and stores the returned id instead of the name; here the name is copied and never read back. Once named, a barrier returns `EAGAIN` (35) for every later call and keeps its name, as [firmware 13.42's module run on a PC](https://github.com/boykopovar/AnyPS5/pull/10385#issuecomment-6415603047) does. The kernel's handling of a null `name` is unmeasured: here it returns 0 and leaves the barrier unnamed. A null barrier pointer, an unset barrier and a destroyed one fault in the module and throw here -- [pthread_mutex_reltimedlock_np](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) takes `(mutex, usec)`, a relative timeout in microseconds, and is `scePthreadMutexTimedlock` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadMutexTimedlock` ([`mutex.cpp`](https://github.com/shadps4-emu/shadPS4/blob/7c960eae136b956cf4c1d72aacd2426b650330df/src/core/libraries/kernel/threads/mutex.cpp#L284-L287)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `pthread_cond_reltimedwait_np`, so a count of 2^32 microseconds or more would be truncated if the console reads 64 bits. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 8192 entries on each side and counts them as a PS5 (firmware 13.60) counted them for a payload process: one cpu entry for each 2 MiB granule that holds a committed mapping, whatever its protection (none included), and one gpu entry for each 1 GiB region that holds a GPU-accessible mapping, plus one for each 2 MiB granule of such a mapping that is not a whole granule of direct memory at a 2 MiB-aligned physical address. A granule or region shared by several mappings counts once, and reservations do not count. On the console the first GPU mapping of a process also takes three gpu entries and one cpu entry that stay after it is unmapped, which is not modelled, and only the ranges whose protection is recorded here are counted. A null pointer returns `SCE_KERNEL_ERROR_EFAULT`, where the console faults. Resonance: A Plague Tale Legacy (PPSA30601) calls it at start-up (#4067) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 2baf982014..63b36702ba 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -5,6 +5,7 @@ ### Unknown function info +- [sceKernelGetHwModelName, sceKernelIsProspero](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) - both throw. On a CFI-2014 PS5 (firmware 13.60) a payload process got 0 from `sceKernelGetHwModelName(char*)`, which wrote "CFI-2014 A01X" and a terminator (14 bytes), and 0 from `sceKernelIsProspero()`. The model string belongs to one console and the buffer size and null behaviour are not known, and 0 from `sceKernelIsProspero` on a PS5 suggests it answers something other than the model, so neither is implemented - [sceKernelGetCompiledSdkVersion](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - which SDK version the console returns is unmeasured. The [PS5 payload SDK](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/crt/kernel.c#L257-L275) declares the process parameters with a 32-bit PS4 SDK version at offset `0x10` and a 32-bit PS5 SDK version at `0x14`; this export returns the field at `0x10`, the one [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reads and the one [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/emulator.cpp#L110-L138) (PS4, a single 64-bit `sdk_version` there) reads from the eboot. The `int (int32_t*)` signature and `EINVAL` (`0x80020016`) for a null output follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/615bcba5e21b82dc59588575f7ecbce8da09ca2c/src/core/libraries/kernel/process.cpp#L56-L62). Closes when: measured on a PS5 - [scePthreadCondattrGetclock](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - the `int (const PthreadCondattr*, KernelClockid*)` signature and checks follow FreeBSD 11.4 libthr's [`_pthread_condattr_getclock`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_condattr.c#L82-L90) with the error code of libkernel's `scePthreadCondattrSetclock`: a null or destroyed attribute returns `SCE_KERNEL_ERROR_EINVAL`, and a null clock pointer throws, since libthr writes through it unchecked. Console behaviour is unverified diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 46a5df8ed5..7850bd1839 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -37,6 +37,7 @@ - Under Rosetta (M5, macOS 27.0.1), a VMASKMOVPS, VMASKMOVPD, VPMASKMOVD or VPMASKMOVQ store or load that faults on an element other than an active first one ends the process (`unexpectedly need to EmulateForward on a synchronous exception`) instead of raising SIGSEGV or SIGBUS, so a masked store into a page the macOS write watch keeps read-only ends the title. `--to-rosetta` moves each masked store to a [stub](../../../core/relinker/codegen/src/x86/MaskedStoreLowering.cpp) that first writes to the first and last byte of every active element with `LOCK OR` of 0, which faults normally and changes nothing; each store then costs up to 16 locked byte writes. A page made read-only again by another thread between those writes and the store still ends the process. A stack-relative displacement that exceeds signed disp32 after the spill frame throws. Masked loads are not converted. - In Windows output, the conversion fails when a code entry lies after the first byte of a guest GS instruction that the [TLS builder](../../../core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp) would replace with `UD2`: `Branch enters a guest GS instruction` for a relative branch target, `GS trap would overwrite an executable entry point` for any other entry the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) follows (a function symbol, a relocated pointer, a RIP-relative `lea` target, an initializer array entry) or for a pointer target it takes as data. Both messages give the position of the entered byte, and no option skips either check. Linux output keeps the instruction, and libkernel's `sysarch` sets the GS base there. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). +- In Windows output, an executable relocation against a named import whose 8 bytes overlap the `PT_TLS` initialization image fails the conversion ([PE patcher](../../../core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp)), as it does in a guest module. ld.so applies such a relocation before it copies the image into a thread's TLS block, so every thread reads the bound value. The `.gtls` template is copied from the image at conversion time, and the loader allocates the main thread's block from it before the entry stub binds imports; binding would require the stub to write the value into both the template and that block. No title with such a relocation is known. - In macOS output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 74294bab9d..5bc7243fe4 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -10,13 +10,13 @@ ### Unknown function info -- [VCC-backed MIMG samplers](../../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp): the s104/s105/vcc_lo/vcc_hi tuple follows LLVM's [SGPR register classes](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/SIRegisterInfo.td#L452) and [gfx10 sampler operand](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/MIMGInstructions.td#L1379), using the existing VCC word reads. Recompiled wave32/wave64 reads are checked by [bilinear and white-border cases](../../../core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleVccSampler.cpp); raw RDNA execution of this tuple on gfx1013 or gfx1036 is unmeasured. +- [VCC-backed MIMG samplers](../../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) - the PS5's gfx1013 is not measured. The sampler at field 26 is s104, s105, vcc_lo and vcc_hi. Measured on RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31) through the [hardware oracle](../HW_ORACLE.md) with raw `image_sample_lz` encodings in wave32 and wave64 on a two-level texture, it samples as the same S# in s[16:19] does, and a border clamp in s104, a `MIN_LOD` of 1.0 in s105, point filtering in vcc_lo and a transparent black border in vcc_hi each change the result. Recompiled reads are checked by [bilinear and white-border cases](../../../core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleVccSampler.cpp). - [MSAA-address image loads on 2D textures](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) - 32-bit `image_load` with DIM 2D_MSAA reads a non-multisampled 2D T# as the same DIM 2D load, ignoring the sample index. Measured on RX 6650 XT (gfx1032), wave32 and wave64, for R32_UINT, R32_FLOAT, RGBA8_UNORM and RGBA8_SRGB, sample indices 0/1/2/7/31/0xffffffff and in-range/out-of-range coordinates; IEEE 0, DX10 clamp 1, denorm32 0, denorm16 3, round32/16 0, fp16 overflow 0. Pinned in ImageMsaaAddressOnPlane.cpp. Not measured on gfx1013. Stores, atomics, mip-bearing or A16 addresses, packed accesses, converted formats, depth-bit views, array-MSAA addresses and image-table accesses are not enabled by this compatibility mode. -- [MUBUF/MTBUF descriptors ending in VCC](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp): the four dword descriptor at s104 is s104, s105, vcc_lo and vcc_hi, as LLVM's [VCCTuple/SReg_128](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/SIRegisterInfo.td#L422) and [buffer resource operands](https://github.com/llvm/llvm-project/blob/d8219b47422f7322db252ad29ca8ea87993fb433/llvm/lib/Target/AMDGPU/BUFInstructions.td) describe. Existing translation maps scalar codes106/107 to the raw VCC words. Recompiled loads/stores, both words, bounds, formats and wave32/wave64 execute on NVIDIA and AMD Vulkan; raw gfx10/gfx1013 hardware is not measured. Higher descriptor bases remain refused; the normal SGPR bound is unchanged. +- [MUBUF/MTBUF descriptors ending in VCC](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp) - the PS5's gfx1013 is not measured; higher descriptor bases remain refused. The descriptor at resource field 26 is s104, s105, vcc_lo and vcc_hi. Measured on RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31) through the [hardware oracle](../HW_ORACLE.md) with raw encodings, which LLVM's assembler refuses, in wave32 and wave64: `buffer_load_dword`, `buffer_load_format_x` with a format in vcc_hi, `tbuffer_load_format_x`, `buffer_store_dword` and `tbuffer_store_format_x` through it act as through the same words in s[8:11], including a nonzero base high half in s105 and a 12-byte `NUM_RECORDS` in vcc_lo. Recompiled accesses are checked by [BufferResourceVcc](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferResourceVcc.cpp). - [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin in a pixel shader the guest iterates per sample (`PS_ITER_SAMPLE` on a multisampled draw), an equality inferred, not measured. In any other pixel shader it is 0, the iterated sample number of a pixel the hardware does not iterate (also inferred), so reading it does not make the host shade per sample. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the two fields, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1, 8-11 and 16-28 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId only in iterated shaders. The whole word has the same render target index at bits 16-28 - [Front-face input with `FRONT_FACE_ALL_BITS`](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (shader recompiler) - with `SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS` set, a back face reads 0 and a front face 0xffffffff. Mesa 22.0 RADV sets the bit ([radv_pipeline.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/vulkan/radv_pipeline.c#L5953)) and ACO tests the input with `v_cmp_lg_u32 0` ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-22.0.0/src/amd/compiler/aco_instruction_selection.cpp#L8123-L8128)), so only back = 0 and front ≠ 0 are sourced; PPSA05684's two-sided cloth shaders test it with `v_cmp_lt_u32 0`. The exact front value is not measured. Without the bit the input is ±1.0, as current Mesa reads it (`fgt 0`, `fsign`; [ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c)) -- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): for formats AnyPS5 can sample as floating-point from GFX10 codes 1–77, `image_sample_c*` returns 0 for every comparison function except codes 7 (`16_UNORM`) and 22 (`32_FLOAT`), which use the native comparison path. The split was measured with point sampling on RDNA2 (gfx1031 and gfx1036) and with point and bilinear sampling on RDNA3 (gfx1100); the PS5's gfx1013 and 2D-array/cube comparisons across these formats remain unverified. Codes 128–130 (8-bit sRGB) also return 0, measured on gfx1035 over references -0.5..1.5 and texel codes 0..255. Scaled, integer and formats requiring converted sampling remain unsupported, although the tested hardware formats also return 0 for them. Anisotropic comparison of the remaining formats has not been measured. +- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): for formats AnyPS5 can sample as floating-point from GFX10 codes 1–77, `image_sample_c*` returns 0 for every comparison function except codes 7 (`16_UNORM`) and 22 (`32_FLOAT`), which use the native comparison path. The split was measured with point sampling on RDNA2 (gfx1031 and gfx1036) and with point and bilinear sampling on RDNA3 (gfx1100); the PS5's gfx1013, cube comparisons and 2D-array comparisons of formats other than 1, 7, 22 and 56 remain unverified (on gfx1036, 1 and 56 return 0 for all eight functions with DIM 2D_ARRAY on 2D-array and 2D T#s). Codes 128–130 (8-bit sRGB) also return 0, measured on gfx1035 over references -0.5..1.5 and texel codes 0..255. Scaled, integer and formats requiring converted sampling remain unsupported, although the tested hardware formats also return 0 for them. Anisotropic comparison of the remaining formats has not been measured. - [s_setkill](../../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified - [s_sethalt](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws - [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. The begin instruction tests for an empty mask as `(exec_lo | exec_hi) == 0`: AMD's Windows driver (32.0.21045.5002 and 32.0.21042.62) compiled `exec_lo == 0 && exec_hi == 0` as a test of the whole 64-bit mask when guest loads use selects, as they do with `robustBufferAccess2` (#7376, #7623). Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled @@ -99,7 +99,7 @@ - A [V#, T# or S#](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) loaded through a null or unmapped pointer read from memory is zero when the work is recorded, and the capture throws if no flat SRT slot records that read. - [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. 2D-array `image_sample_l` and `image_sample_lz` through a 1D T# read as DIM 1D too, ignoring v and the slice: measured on gfx1036 with the hardware oracle (template assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll`) against the same sample with DIM 1D on a 16-texel 8_8_8_8 UNORM T# with 1 or 5 levels, HEIGHT or DEPTH 3, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, slices 0, 1, 2.7, -1 and 5, LOD -1 to 6, and the five filter and address cases above: all 48000 rows matched. A 2D-array `image_load` through a 1D T# is not the same: on gfx1036 it returns the DIM 1D texel only at slice 0, and zeros at slices 1, 2, 5 and -1 even with DEPTH 3, so it, the plain `image_sample` and the other 2D-array accesses still throw. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. - [3D image accesses bound to a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA1 (RX 5700 XT, gfx1010; wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even), not the PS5's gfx1013, on 8x1 R8G8B8A8_UNORM linear 1D T#s with HEIGHT and DEPTH 0 or 3 and point filters. `image_sample_lz` reads the texel u selects for v and r in {0, 0.5, 1, 1.5, -0.25, 3}, with wrap, clamp and border addressing, and v and r never produce the border; u wraps, clamps and borders as with DIM 1D. Mip-less `image_load` reads the x texel for any y in {0, 1, 2, 3, -1} when z is 0, returns zero for z in {1, 2, 3, -1}, whatever DEPTH says, and returns zero for x past the width. A plain `image_sample` (implicit LOD) on the quads of a compute kernel takes its texel and LOD from u alone: on a 64-texel, 7-mip 1D T# steps of 1, 4 and 16 texels per lane pick mips 0, 2 and 4, as DIM 1D `image_sample_l` does, and steps in v or r alone stay at mip 0. Outside the pixel stage a plain `image_sample` is emitted with LOD 0, as for 2D accesses through a 1D T#. Every other 3D access of such an image (`image_load` with mips, storage, explicit LOD, bias, derivatives, offsets, comparison, gathers, queries) keeps throwing. SONIC SUPERSTARS (PPSA06888) samples a 1x1 1D T# this way with plain `image_sample` and `image_load`. -- [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice, as the native modes of a 2D-array access through a 2D T# already do. This follows the T# type deciding the addressing; it is not measured on hardware for comparison samples +- [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice; the PS5's gfx1013 is not measured. On RDNA2 (gfx1036, AMD Windows driver 32.0.21030.31, [hardware oracle](../HW_ORACLE.md)), `image_sample_c_lz` and `image_sample_lz` with DIM 2D_ARRAY on linear 2D T#s of formats 1, 7, 22 and 56 return what the same samples with DIM 2D return, for slices 0, 1, 2, 3, 0.6, 1.4, -1 and 5, all eight comparison functions, point and bilinear filtering and `DEPTH` 0 and 3, while a four-layer 2D-array T# over the same memory returns a different result per slice - [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035). How the console routes a flat address was not measured. Its bases, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), first inferred from PPSA04263's traversal addresses, are measured: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) `src_shared_base` / `src_shared_limit` read 0x80000000_00000000 / 0x80000000_FFFFFFFF and `src_private_base` / `src_private_limit` 0x70000000_00000000 / 0x70000000_FFFFFFFF. The scalar aperture sources read the same two values - [Flat accesses outside the aperture routing](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - only compute stages route flat accesses, so graphics-stage flat accesses to the shared or private aperture still go to BDA. In compute, a 64-bit flat atomic whose address is in the shared aperture takes the workgroup lock (`ldsLock`) for its two-dword update, and every such atomic requests that lock, so a shader with one pays for it even on global addresses (the path is exercised by `agc_driver_global_atomics_float`). Inferred, not measured on LDS or scratch: an aperture access that is not inside one dword is done byte by byte, an unaligned aperture atomic records the BDA unaligned fault, and LDS or scratch accesses past their end are dropped or read zero. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. @@ -161,7 +161,7 @@ - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). - [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 in every f16 denormal mode, measured on RDNA2 (gfx1036 with f16 denormals preserved, gfx1030 in all sixteen combinations of f16 rounding and denormal mode). When bit 7 flushes f16 results, `v_cvt_f16_f32`, `v_cvt_f16_i16` and `v_cvt_f16_u16` write a zero of the result's sign for a value that, rounded in that mode with an unbounded exponent, is below 2^-14, so a value just below 2^-14 that rounds up to it stays 0x0400; `v_ldexp_f16` keeps its rule above. The `clamp` modifier is applied before the rounding. On gfx1030 every row matches: 8,996 f32 inputs around the half denormal, normal and overflow boundaries with and without `clamp`, every 16-bit integer, and 2,470 `v_ldexp_f16` operands. - [v_cvt_pkrtz_f16_f32 and v_pack_b32_f16 denormals](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_cvt_pkrtz_f16_f32` flushes each denormal half result to its signed zero when bit 1 is clear (0 and 1), and `v_pack_b32_f16` flushes each denormal half input to its signed zero when bit 0 is clear (0 and 2). Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 and f32 denormals kept, over 256 pairs of 16 f32 values around the f16 denormal range for `v_cvt_pkrtz_f16_f32` and 256 pairs of 16 halves (zeros, denormals, minimum normals, ±1, the largest finite value, infinities and quiet and signaling NaNs) for `v_pack_b32_f16`. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. -- [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) returns 0 for every comparison function on formats AnyPS5 can sample as floating-point measured on GFX10 (codes 1–77 except native comparison codes 7 and 22) and on the corresponding GFX11 formats measured on gfx1100. The current Vulkan path still lowers these formats through the shader; the compare result is constant zero. Scaled, integer and formats requiring converted sampling remain refused. Only 2D, 2D-array and cube views are accepted; image tables, min/max reduction, unnormalized coordinates and unsupported addressing remain refused. Point and bilinear results match on gfx1100. The PS5's gfx1013, array/cube behavior for the measured formats, and anisotropic comparisons remain unverified. Bilinear weights are full float, not the hardware's fixed-point subtexel weights +- [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) returns 0 for every comparison function on formats AnyPS5 can sample as floating-point measured on GFX10 (codes 1–77 except native comparison codes 7 and 22) and on the corresponding GFX11 formats measured on gfx1100. The current Vulkan path still lowers these formats through the shader; the compare result is constant zero. Scaled, integer and formats requiring converted sampling remain refused. Only 2D, 2D-array and cube views are accepted; image tables, min/max reduction, unnormalized coordinates and unsupported addressing remain refused. Point and bilinear results match on gfx1100. The PS5's gfx1013, cube behavior, array behavior except codes 1 and 56, and anisotropic comparisons remain unverified. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view) are sampled in the shader, the way color comparisons are emulated: `image_sample` of a 2D or 2D array view fetches the one or four texels at LOD 0, decodes them and blends them, for point or bilinear filtering with equal minification and magnification filters and wrap or clamp-to-edge addressing. A shader gets these sample modes only once a converted descriptor reaches one of its sampled images: that capture throws `ConvertedSampleRequired`, the driver records the program and prepares it again with `GuestContext::convertedSamples`, so such a shader is prepared twice per run and its first capture is repeated. The record lives with the registered program and is not kept across runs. Bilinear weights are rounded to the hardware's 8-bit grid (`roundEven(fraction * 256) / 256`, ties to even), as measured with `image_sample_lz` on RDNA2 (gfx1035) for 10_11_11 UNORM texels by adriadam10 in #5958; point sampling, decoding, swizzles and wrap and clamp-to-edge addressing matched the same measurement. A sample that is not `_lz` needs an image with one mip level; gathers, LOD queries, clamp-to-border and mirrored addressing, other filters, min/max reduction, unnormalized coordinates, images selected at run time, 16-bit data and a DST_SEL of the missing W channel throw. - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035; with the workgroup lock, in device memory and in the invocation-private LDS of other stages alike, the recompiler records the `Unaligned` shader fault with the LDS byte address, leaves both dwords unchanged and returns 0, but the lane and the rest of its wave keep running. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. diff --git a/tools/ab_split.py b/tools/ab_split.py new file mode 100644 index 0000000000..4bedf518ea --- /dev/null +++ b/tools/ab_split.py @@ -0,0 +1,215 @@ +import argparse +import json +import math +import re +import sys +from pathlib import Path + +DESCRIPTION = """Per-phase means of an in-run A/B (APS5_AB) log. The driver prints "[ab] phase A|B begins at present + (t s)" when a phase starts and "[ab] phase A|B settled at present (t s)" APS5_AB_SETTLE seconds +later. Each periodic report covers the time since the previous report of its kind, so a report counts for phase k +only when the previous report of its kind came after phase k's settled line and the report came before phase k+1 +began. Presents per 10 s and frame time come from the [ab] lines (settled line to the next phase's begin), GPU busy +and idle gaps per frame from [present], GPU time per present from [gputime] (APS5_PROFILE_GPU), the committer and +drains from [draw] pipelined, unlocked hook waits from [syncwait] (APS5_PROFILE_DRAW). The noise column compares +each B phase with the mean of its neighbouring A phases, which cancels a linear drift: mean difference, standard +error, standard deviation and count.""" + +PHASE = re.compile(r"^\[ab\] phase ([AB]) (\d+) (begins|settled) at present (\d+) \(t ([\d.]+) s\)") +KINDS = (("present", "[present] "), ("gputime", "[gputime] "), ("draw", "[draw] pipelined"), ("syncwait", "[syncwait] ")) + + +def number(pattern, text): + match = re.search(pattern, text) + return float(match.group(1)) if match else None + + +def parse_report(kind, text): + if kind == "present": + presents, dropped = number(r"^\[present\] (\d+) presents", text), number(r"\((\d+) dropped", text) + busy, gaps = number(r"GPU busy ([\d.]+) ms", text), number(r"idle gaps ([\d.]+) ms", text) + if None in (presents, dropped, busy, gaps): + return None + return {"frames": presents + dropped, "busy": busy, "gaps": gaps} + if kind == "gputime": + total, presents = number(r"^\[gputime\] ([\d.]+) ms of GPU time", text), number(r"; (\d+) presents;", text) + if total is None or presents is None or "; by class:" not in text: + return None + programs_text, classes_text = text.split("by program:", 1)[-1].split("; by class:", 1) + programs, classes = {}, {} + for key, ms in re.findall(r" (0x[0-9a-f]+) x\d+ ([\d.]+)ms", programs_text): + programs[key] = programs.get(key, 0.0) + float(ms) + for name, ms in re.findall(r" ([a-z][a-z-]*) x\d+ ([\d.]+)ms [\d.]+MiB \(per present", classes_text): + classes[name] = classes.get(name, 0.0) + float(ms) + return {"total": total, "presents": presents, "programs": programs, "classes": classes} + if kind == "draw": + commits, per_commit = number(r": (\d+) commits at", text), number(r"commits at ([\d.]+) us each", text) + enqueued, room = number(r"; (\d+) enqueued behind", text), number(r"waiting ([\d.]+) us per draw for room", text) + if None in (commits, per_commit, enqueued, room) or "; drains:" not in text: + return None + drains = text.split("; drains:", 1)[1].split(";", 1)[0] + return {"commits": commits, "per_commit": per_commit, "enqueued": enqueued, "room": room, + "drain_ms": sum(float(ms) for ms in re.findall(r"\(([\d.]+) ms\)", drains))} + if kind == "syncwait": + if "):" not in text: + return None + sites = text.split("):", 1)[1].split("; batches ahead", 1)[0] + return {"wait_ms": sum(float(ms) for ms in re.findall(r" \d+/([\d.]+)ms", sites))} + return None + + +def read_log(lines): + config, phases, current = None, [], None + previous = {kind: None for kind, _ in KINDS} + for line in lines: + text = line.rstrip("\r\n") + if text.startswith("[ab] in-run A/B:"): + config = text + continue + match = PHASE.match(text) + if match: + kind, index, event, present, seconds = match.group(1), int(match.group(2)), match.group(3), int(match.group(4)), float(match.group(5)) + if event == "begins": + if current is not None: + current["end"] = (present, seconds) + current = {"kind": kind, "index": index, "begin": (present, seconds), "settled": None, "end": None, + "reports": {name: [] for name, _ in KINDS}} + phases.append(current) + elif current is not None and current["index"] == index: + current["settled"] = (present, seconds) + continue + for name, prefix in KINDS: + if text.startswith(prefix): + state = (current["index"], current["settled"] is not None) if current is not None else None + if state is not None and state[1] and previous[name] == state: + report = parse_report(name, text) + if report is not None: + current["reports"][name].append(report) + previous[name] = state + break + return config, phases + + +def weighted(rows, value, weight): + total = sum(row[weight] for row in rows) + return sum(row[value] * row[weight] for row in rows) / total if total else None + + +def metrics(phase, programs, classes): + out = {} + if phase["settled"] is not None and phase["end"] is not None and phase["end"][1] > phase["settled"][1]: + presents, seconds = phase["end"][0] - phase["settled"][0], phase["end"][1] - phase["settled"][1] + out["presents/10s"] = presents / seconds * 10 + if presents > 0: + out["frame ms"] = seconds / presents * 1000 + rows = phase["reports"]["present"] + out["GPU busy ms/frame"], out["GPU idle gaps ms/frame"] = weighted(rows, "busy", "frames"), weighted(rows, "gaps", "frames") + rows = phase["reports"]["gputime"] + presents = sum(row["presents"] for row in rows) + if presents: + out["gputime ms/present"] = sum(row["total"] for row in rows) / presents + for key in programs: + out[f"{key} ms/present"] = sum(row["programs"].get(key, 0.0) for row in rows) / presents + for name in classes: + out[f"{name} ms/present"] = sum(row["classes"].get(name, 0.0) for row in rows) / presents + rows = phase["reports"]["draw"] + out["committer us/commit"], out["room wait us/draw"] = weighted(rows, "per_commit", "commits"), weighted(rows, "room", "enqueued") + out["drain ms/report"] = sum(row["drain_ms"] for row in rows) / len(rows) if rows else None + rows = phase["reports"]["syncwait"] + out["syncwait GPU ms/report"] = sum(row["wait_ms"] for row in rows) / len(rows) if rows else None + return {name: value for name, value in out.items() if value is not None} + + +def noise(values, phases): + differences = [] + for index, phase in enumerate(phases): + if phase["kind"] != "B" or values[index] is None: + continue + neighbours = [values[other] for other in (index - 1, index + 1) + if 0 <= other < len(phases) and phases[other]["kind"] == "A" and values[other] is not None] + if neighbours: + differences.append(values[index] - sum(neighbours) / len(neighbours)) + if not differences: + return None + mean = sum(differences) / len(differences) + if len(differences) == 1: + return {"mean": mean, "se": None, "sd": None, "n": 1} + sd = math.sqrt(sum((value - mean) ** 2 for value in differences) / (len(differences) - 1)) + return {"mean": mean, "se": sd / math.sqrt(len(differences)), "sd": sd, "n": len(differences)} + + +def split(lines, programs=(), classes=()): + config, phases = read_log(lines) + if not phases: + raise ValueError("no [ab] phase lines (was APS5_AB set?)") + table = [metrics(phase, programs, classes) for phase in phases] + names = list(dict.fromkeys(name for row in table for name in row)) + rows = [] + for name in names: + values = [row.get(name) for row in table] + a = [value for phase, value in zip(phases, values) if phase["kind"] == "A" and value is not None] + b = [value for phase, value in zip(phases, values) if phase["kind"] == "B" and value is not None] + if a and b: + mean_a, mean_b = sum(a) / len(a), sum(b) / len(b) + rows.append({"metric": name, "a": mean_a, "b": mean_b, "change": mean_b - mean_a, + "change_pct": (mean_b / mean_a - 1) * 100 if mean_a else None, "noise": noise(values, phases)}) + return {"config": config, + "phases": [{"kind": phase["kind"], "index": phase["index"], "begin_present": phase["begin"][0], + "settled_present": phase["settled"][0] if phase["settled"] else None, + "end_present": phase["end"][0] if phase["end"] else None, + "clean_reports": {name: len(reports) for name, reports in phase["reports"].items()}, + "metrics": row} for phase, row in zip(phases, table)], + "metrics": rows} + + +def text_report(result): + lines = [result["config"] or "[ab] configuration line missing", "", + "phase begin / settled / end clean reports (present/gputime/draw/syncwait) presents/10s"] + for phase in result["phases"]: + settled = "-" if phase["settled_present"] is None else phase["settled_present"] + end = "-" if phase["end_present"] is None else phase["end_present"] + reports = "/".join(str(phase["clean_reports"][name]) for name, _ in KINDS) + rate = phase["metrics"].get("presents/10s") + lines.append(f"{phase['kind']} {phase['index']:<3} {phase['begin_present']:>7} / {settled:>7} / {end:>7} {reports:<45} " + + ("unfinished" if rate is None else f"{rate:.2f}")) + lines.extend(["", f"{'metric':<30} {'A':>10} {'B':>10} {'B-A':>10} {'B/A-1':>8} B vs neighbouring A: mean +- SE (SD, n)"]) + for row in result["metrics"]: + spread = row["noise"] + if spread is None: + text = "-" + elif spread["n"] == 1: + text = f"{spread['mean']:+.2f} (n 1)" + else: + text = f"{spread['mean']:+.2f} +- {spread['se']:.2f} (SD {spread['sd']:.2f}, n {spread['n']})" + percent = "n/a" if row["change_pct"] is None else f"{row['change_pct']:+.1f}%" + lines.append(f"{row['metric']:<30} {row['a']:>10.2f} {row['b']:>10.2f} {row['change']:>+10.2f} {percent:>8} {text}") + return "\n".join(lines) + "\n" + + +def program_key(value): + if not re.fullmatch(r"0x[0-9a-fA-F]+", value): + raise argparse.ArgumentTypeError("must be a hexadecimal program key such as 0x1000") + return "0x" + value[2:].lower().lstrip("0").rjust(1, "0") + + +def main(arguments=None): + parser = argparse.ArgumentParser(description=DESCRIPTION) + parser.add_argument("log", help="the run's standard error log with the [ab] lines") + parser.add_argument("--program", action="append", type=program_key, default=[], help="also report this [gputime] program key per present (repeatable)") + parser.add_argument("--class", dest="classes", action="append", default=[], help="also report this [gputime] command class per present (repeatable)") + parser.add_argument("--json", action="store_true", help="print the phases and metrics as JSON") + options = parser.parse_args(arguments) + with Path(options.log).open(encoding="utf-8", errors="replace") as source: + result = split(source, options.program, options.classes) + sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else text_report(result)) + return 0 + + +if __name__ == "__main__": + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + try: + sys.exit(main()) + except (OSError, ValueError) as error: + print(f"FAIL: {error}", file=sys.stderr) + sys.exit(2) diff --git a/tools/build_mingw_host_only_tests.py b/tools/build_mingw_host_only_tests.py new file mode 100644 index 0000000000..15b58d7eda --- /dev/null +++ b/tools/build_mingw_host_only_tests.py @@ -0,0 +1,175 @@ +import argparse +import json +from pathlib import Path +import re +import subprocess +import sys + + +def read_reply(build_dir): + reply_dir = build_dir / ".cmake" / "api" / "v1" / "reply" + indexes = list(reply_dir.glob("index-*.json")) + if not indexes: + raise ValueError(f"{reply_dir}: no CMake File API index; configure with the codemodel-v2 query first") + index = max(indexes, key=lambda path: path.stat().st_mtime_ns) + document = json.loads(index.read_text(encoding="utf-8")) + codemodel = document.get("reply", {}).get("codemodel-v2") + if not codemodel: + raise ValueError(f"{index}: missing codemodel-v2 reply") + model = json.loads((reply_dir / codemodel["jsonFile"]).read_text(encoding="utf-8")) + configurations = model.get("configurations", []) + if len(configurations) != 1: + raise ValueError(f"{index}: expected one CMake configuration, found {len(configurations)}") + targets = {} + for target in configurations[0].get("targets", []): + targets[target["id"]] = json.loads((reply_dir / target["jsonFile"]).read_text(encoding="utf-8")) + return Path(model["paths"]["source"]).resolve(), targets + + +def link_command(target): + link = target.get("link", {}) + fragments = link.get("commandFragments", []) + return " ".join(fragment.get("fragment", "") for fragment in fragments) + + +def is_libc_link(name, target): + if name == "libc": + return True + command = link_command(target) + return re.search(r"(?i)(?:^|[/\\\s])libc(?:\.prx|\.dll\.a)(?:$|[\s\"'])", command) is not None + + +def links_guest_libc(target): + return is_libc_link(target["name"], target) + + +def is_product_library(target, source_root): + if target.get("type") not in {"STATIC_LIBRARY", "SHARED_LIBRARY", "MODULE_LIBRARY"}: + return False + source_dir = (source_root / target["paths"]["source"]).resolve() + try: + source_dir.relative_to(source_root / "core" / "libs") + except ValueError: + return False + try: + source_dir.relative_to(source_root / "core" / "libs" / "tests") + except ValueError: + return True + return False + + +def link_command_mentions_target(command, target): + link_names = set() + for artifact in target.get("artifacts", []): + name = Path(artifact.get("path", "")).name + if name and name in command: + return True + for suffix in (".dll.a", ".dylib", ".prx", ".so", ".a", ".lib"): + if name.casefold().endswith(suffix): + name = name[:-len(suffix)] + break + if name.startswith("lib") and target.get("name", "").casefold() != "libc": + name = name[3:] + if name: + link_names.add(name) + target_name = target.get("name", "") + if target_name: + link_names.add(target_name) + return any( + re.search(rf"(?i)(?:^|\s)-l{re.escape(name)}(?=$|\s)", command) is not None + for name in link_names + ) + + +def links_product_library(target, targets, source_root): + command = link_command(target) + if not command: + return False + for linked_target in targets.values(): + if is_product_library(linked_target, source_root) and link_command_mentions_target(command, linked_target): + return True + return False + + +def source_relative_path(source_root, source): + try: + return source.relative_to(source_root).as_posix() + except ValueError: + return None + + +def host_only_test_targets(source_root, targets, changed_paths=None): + selected = [] + for target in targets.values(): + if target.get("type") != "EXECUTABLE": + continue + target_source_dir = (source_root / target["paths"]["source"]).resolve() + try: + target_source_dir.relative_to(source_root / "core" / "libs") + except ValueError: + continue + sources = [source_root / source["path"] for source in target.get("sources", []) if not source.get("isGenerated")] + if not any("tests" in source.parts and source.is_relative_to(source_root / "core" / "libs") for source in sources): + continue + if links_guest_libc(target) or links_product_library(target, targets, source_root): + continue + selected.append(target["name"]) + selected = sorted(set(selected), key=str.casefold) + if changed_paths is None: + return selected + changed = {Path(path).as_posix() for path in changed_paths if path} + broad_change = any( + path in {"CMakeLists.txt", ".github/workflows/build.yml", "tools/build_mingw_host_only_tests.py"} + or path.startswith("core/libs/") and ( + path.endswith((".h", ".hpp", ".hh", ".inl")) or Path(path).name == "CMakeLists.txt" + ) + for path in changed + ) + if broad_change: + return selected + changed_source = { + relative + for target in targets.values() + if target.get("name") in selected + for item in target.get("sources", []) if not item.get("isGenerated") + for relative in [source_relative_path(source_root, source_root / item["path"])] + if relative is not None + } + matched_sources = changed & changed_source + if matched_sources: + return [ + target["name"] for target in targets.values() + if target.get("name") in selected + and any( + source_relative_path(source_root, source_root / item["path"]) in matched_sources + for item in target.get("sources", []) if not item.get("isGenerated") + ) + ] + return [] + + +def main(argv=None): + parser = argparse.ArgumentParser(description="build core/libs test executables that do not link guest libc") + parser.add_argument("build_dir", type=Path) + parser.add_argument("changed_paths", nargs="?", type=Path) + parser.add_argument("--list-only", action="store_true") + args = parser.parse_args(argv) + try: + source_root, targets = read_reply(args.build_dir.resolve()) + changed_paths = args.changed_paths.read_text(encoding="utf-8").splitlines() if args.changed_paths else None + selected = host_only_test_targets(source_root, targets, changed_paths) + if not selected: + print("No changed host-only core/libs test executables require a link check") + return 0 + print("Host-only core/libs test executables:") + print("\n".join(selected)) + if not args.list_only: + subprocess.run(["cmake", "--build", str(args.build_dir), "--parallel", "3", "--target", *selected], check=True) + except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError, subprocess.CalledProcessError) as error: + print(f"{parser.prog}: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/select_tests.py b/tools/select_tests.py index 1fa013a3a9..587684ed8a 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -154,6 +154,14 @@ def paths(arguments): yield value +def changed_paths(path): + data = Path(path).read_bytes() + if b"\0" in data: + return [name for name in data.decode("utf-8").split("\0") if name] + text = data.decode("utf-8-sig") + return [line.strip() for line in text.splitlines() if line.strip()] + + def chunks(names): batch, size = [], 0 for name in names: @@ -305,10 +313,10 @@ def main(argv=None): s = sub.add_parser("select", help="write a ninja file and a ctest index list for a configured tree") s.add_argument("build") s.add_argument("graph") - s.add_argument("changed", help="file with one changed path per line") + s.add_argument("changed", help="file with newline-separated or Git -z NUL-separated changed paths") e = sub.add_parser("explain", help="explain saved-graph selections without a configured build; CTest may add fixtures") e.add_argument("graph", type=Path) - e.add_argument("changed", type=Path, help="file with one changed path per line") + e.add_argument("changed", type=Path, help="file with newline-separated or Git -z NUL-separated changed paths") c = sub.add_parser("ctest", help="run ctest on a tree, limited to the selected tests if select wrote any") c.add_argument("build") c.add_argument("arguments", nargs=argparse.REMAINDER, help="ctest arguments") @@ -320,7 +328,7 @@ def main(argv=None): if args.command == "explain": try: graph = load_graph(args.graph) - changed = [line.strip() for line in args.changed.read_text(encoding="utf-8-sig").splitlines() if line.strip()] + changed = changed_paths(args.changed) reasons = {} picked, full = select(graph, changed, reasons) except (OSError, ValueError) as error: @@ -343,7 +351,7 @@ def main(argv=None): return 0 try: graph = load_graph(args.graph) - changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8-sig").splitlines() if line.strip()] + changed = changed_paths(args.changed) except (OSError, ValueError) as error: parser.error(str(error)) picked, full = select(graph, changed) diff --git a/tools/tests/test_ab_split.py b/tools/tests/test_ab_split.py new file mode 100644 index 0000000000..319872dc3d --- /dev/null +++ b/tools/tests/test_ab_split.py @@ -0,0 +1,136 @@ +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import ab_split + + +def present(busy, gaps, presents=40, dropped=0): + return (f"[present] {presents} presents over 10 s ({dropped} dropped: no free swapchain image; 1 may trail on the GPU); " + f"inflight_wait p50 0.00 ms, p90 0.18 ms, max 0.37 ms over {presents}; per frame, presented or dropped: " + f"GPU busy {busy:.1f} ms, idle gaps {gaps:.1f} ms (0 batches without a completion record); per flip: 3.0 batches " + "recorded (0.0 unsignaled at the flip); per present: 1.0 batches ahead of the blit, 0.0 after it; after the flip " + "packet: last submit 0.1 ms, blit submit 0.2 ms; in-place reads overwritten before execution: 0 of 0 checked") + + +def gputime(total, program, staging, presents=40): + return (f"[gputime] {total:.0f} ms of GPU time in 90 batches over 10 s (batch 900 ms first to last command; classes 300 ms, " + f"all ranges 800 ms, untimed 100 ms outside every range; {presents} presents; 0 ranges dropped at the 512 cap); " + f"by program: 0x1000 x10 {program:.0f}ms 0x2000 x4 5ms; by class: staging-in x80 {staging:.1f}ms 2.0MiB " + "(per present x2.0 9.99ms 0.05MiB) present-blit x40 4.0ms 0.0MiB (per present x1.0 0.10ms 0.00MiB)") + + +def draw(per_commit, room, drain, commits=1000, enqueued=1000): + return (f"[draw] pipelined (10 s, depth 64): {commits} commits at {per_commit:.1f} us each on the committer, 0 failed; " + f"{enqueued} enqueued behind 2.00 on average, waiting {room:.1f} us per draw for room; 0 read ranges, 0 stores held; " + f"drains: packet 10 ({drain:.1f} ms) submission 2 (1.0 ms); packet drains by opcode: 0x49 x3 0 read ranges, 0 stores held") + + +def syncwait(first, second): + return ("[syncwait] unlocked hook waits by read site (10 s; count/GPU wait, avg batches up to the target, avg of them " + "unsignaled at the start, target was the newest submission, avg target submit-to-signal ms):" + f" copy 3/{first:.0f}ms 1.0 0.0 100% 2.0 fill 1/{second:.0f}ms 1.0 0.0 0% 1.0; batches ahead by queue: 0x0 4") + + +def reports(busy, total, program, staging, per_commit, wait): + return [present(busy, 50.0), gputime(total, program, staging), draw(per_commit, 2.0, 5.0), syncwait(wait, 10.0)] + + +def phase_lines(kind, index, begin, seconds, busy, total, program, staging, per_commit, wait, presents=40): + lines = [f"[ab] phase {kind} {index} begins at present {begin} (t {seconds:.3f} s): APS5_NO_BARRIER_BATCH {'set' if kind == 'A' else 'unset'}"] + lines += reports(999.0, 999.0, 999.0, 999.0, 999.0, 999.0) + lines.append(f"[ab] phase {kind} {index} settled at present {begin + 2} (t {seconds + 2:.3f} s)") + lines += reports(888.0, 888.0, 888.0, 888.0, 888.0, 888.0) + lines += reports(busy, total, program, staging, per_commit, wait) + lines += reports(busy, total, program, staging, per_commit, wait) + return lines + + +def log(): + lines = ["[gpu] startup", "[ab] in-run A/B: phases of 30.0 s from present 100, settle 2.0 s; A: APS5_NO_BARRIER_BATCH set; B: APS5_NO_BARRIER_BATCH unset"] + lines += reports(777.0, 777.0, 777.0, 777.0, 777.0, 777.0) + lines += phase_lines("A", 0, 100, 10.0, 100.0, 400.0, 40.0, 8.0, 200.0, 30.0) + lines += phase_lines("B", 1, 140, 40.0, 130.0, 480.0, 60.0, 9.0, 180.0, 50.0) + lines += phase_lines("A", 2, 180, 70.0, 110.0, 420.0, 44.0, 8.0, 200.0, 34.0) + lines.append("[ab] phase B 3 begins at present 220 (t 100.000 s): APS5_NO_BARRIER_BATCH unset") + return [line + "\n" for line in lines] + + +class AbSplitTests(unittest.TestCase): + def test_phases_count_only_reports_inside_a_settled_phase(self): + result = ab_split.split(log(), ["0x1000"], ["staging-in"]) + self.assertTrue(result["config"].startswith("[ab] in-run A/B: phases of 30.0 s")) + self.assertEqual([(phase["kind"], phase["index"], phase["begin_present"], phase["settled_present"], phase["end_present"]) + for phase in result["phases"]], + [("A", 0, 100, 102, 140), ("B", 1, 140, 142, 180), ("A", 2, 180, 182, 220), ("B", 3, 220, None, None)]) + self.assertEqual(result["phases"][0]["clean_reports"], {"present": 2, "gputime": 2, "draw": 2, "syncwait": 2}) + self.assertEqual(result["phases"][3]["clean_reports"], {"present": 0, "gputime": 0, "draw": 0, "syncwait": 0}) + first = result["phases"][0]["metrics"] + self.assertAlmostEqual(first["presents/10s"], 38 / 28 * 10) + self.assertAlmostEqual(first["frame ms"], 28 / 38 * 1000) + self.assertAlmostEqual(first["GPU busy ms/frame"], 100.0) + self.assertAlmostEqual(first["GPU idle gaps ms/frame"], 50.0) + self.assertAlmostEqual(first["gputime ms/present"], 10.0) + self.assertAlmostEqual(first["0x1000 ms/present"], 1.0) + self.assertAlmostEqual(first["staging-in ms/present"], 0.2) + self.assertAlmostEqual(first["committer us/commit"], 200.0) + self.assertAlmostEqual(first["room wait us/draw"], 2.0) + self.assertAlmostEqual(first["drain ms/report"], 6.0) + self.assertAlmostEqual(first["syncwait GPU ms/report"], 40.0) + + def test_b_is_compared_with_its_neighbouring_a_phases(self): + rows = {row["metric"]: row for row in ab_split.split(log())["metrics"]} + busy = rows["GPU busy ms/frame"] + self.assertAlmostEqual(busy["a"], 105.0) + self.assertAlmostEqual(busy["b"], 130.0) + self.assertAlmostEqual(busy["change"], 25.0) + self.assertAlmostEqual(busy["change_pct"], 25 / 105 * 100) + self.assertEqual(busy["noise"], {"mean": 25.0, "se": None, "sd": None, "n": 1}) + self.assertAlmostEqual(rows["syncwait GPU ms/report"]["noise"]["mean"], 60.0 - 42.0) + + def test_classes_are_read_without_their_per_present_figures(self): + row = ab_split.parse_report("gputime", gputime(400.0, 40.0, 8.0)) + self.assertEqual(row["classes"], {"staging-in": 8.0, "present-blit": 4.0}) + self.assertEqual(row["programs"], {"0x1000": 40.0, "0x2000": 5.0}) + + def test_dropped_presents_count_as_frames(self): + row = ab_split.parse_report("present", present(90.0, 10.0, presents=30, dropped=10)) + self.assertEqual(row, {"frames": 40, "busy": 90.0, "gaps": 10.0}) + + def test_unrecognised_reports_are_skipped(self): + self.assertIsNone(ab_split.parse_report("gputime", "[gputime] disabled")) + self.assertIsNone(ab_split.parse_report("draw", "[draw] pipelined depth 64")) + self.assertIsNone(ab_split.parse_report("present", "[present] flip 3")) + + def test_a_log_without_phases_is_refused(self): + with self.assertRaisesRegex(ValueError, "no \\[ab\\] phase lines"): + ab_split.split([present(1.0, 1.0) + "\n"]) + + def test_command_line(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "run.err.log" + path.write_text("".join(log()), encoding="utf-8") + run = lambda *args: subprocess.run([sys.executable, str(Path(ab_split.__file__)), *args], capture_output=True, text=True, + encoding="utf-8", timeout=10, env=dict(os.environ, PYTHONIOENCODING="ascii")) + text = run(str(path), "--program", "0x01000") + self.assertEqual(text.returncode, 0, text.stderr) + self.assertIn("0x1000 ms/present", text.stdout) + self.assertRegex(text.stdout, r"GPU busy ms/frame +105\.00 +130\.00 +\+25\.00 +\+23\.8% +\+25\.00 \(n 1\)") + self.assertIn("unfinished", text.stdout) + data = json.loads(run(str(path), "--json").stdout) + self.assertEqual(len(data["phases"]), 4) + self.assertEqual(run(str(path), "--program", "1000").returncode, 2) + empty = Path(directory) / "empty.log" + empty.write_text("[gpu] nothing\n", encoding="utf-8") + failed = run(str(empty)) + self.assertEqual(failed.returncode, 2) + self.assertIn("FAIL: no [ab] phase lines", failed.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_mingw_host_only_test_targets.py b/tools/tests/test_mingw_host_only_test_targets.py new file mode 100644 index 0000000000..f1927be0fa --- /dev/null +++ b/tools/tests/test_mingw_host_only_test_targets.py @@ -0,0 +1,162 @@ +import json +from pathlib import Path +import sys +import tempfile +import unittest + + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import build_mingw_host_only_tests + + +class HostOnlyTargets(unittest.TestCase): + def target(self, name, target_type="EXECUTABLE", source="core/libs/tests/HostOnly.cpp", dependencies=(), fragment=""): + return { + "artifacts": [{"path": f"tests/{name}.exe"}], + "dependencies": [{"id": dependency} for dependency in dependencies], + "id": name, + "link": {"commandFragments": [{"fragment": fragment}] if fragment else []}, + "name": name, + "paths": {"build": "core/libs", "source": "core/libs"}, + "sources": [{"path": source}], + "type": target_type, + } + + def fixture(self, targets): + directory = Path(self.temporary.name) + reply = directory / "build" / ".cmake" / "api" / "v1" / "reply" + reply.mkdir(parents=True) + entries = [] + for target in targets: + filename = f"target-{target['name']}.json" + (reply / filename).write_text(json.dumps(target), encoding="utf-8") + entries.append({"id": target["id"], "jsonFile": filename, "name": target["name"]}) + model = {"configurations": [{"targets": entries}], "paths": {"source": str(directory)}} + (reply / "codemodel.json").write_text(json.dumps(model), encoding="utf-8") + index = {"reply": {"codemodel-v2": {"jsonFile": "codemodel.json"}}} + (reply / "index-0001.json").write_text(json.dumps(index), encoding="utf-8") + return directory / "build" + + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + + def tearDown(self): + self.temporary.cleanup() + + def test_selects_core_libs_test_executables_without_guest_libc(self): + host = self.target("host_tests") + guest = self.target("guest_tests", dependencies=("libc-id",), + fragment="C:/build/core/libs/prx/libc/libc.dll.a") + libc = self.target("libc", target_type="SHARED_LIBRARY") + libc["id"] = "libc-id" + other = self.target("outside_tests", source="core/relinker/tests/Outside.cpp") + utility = self.target("not_a_test", source="core/libs/nid/src/main.cpp") + build_dir = self.fixture([host, guest, libc, other, utility]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), ["host_tests"]) + + def test_excludes_a_transitive_guest_libc_dependency(self): + helper = self.target("guest_helper", target_type="STATIC_LIBRARY", source="core/libs/tests/GuestHelper.cpp", + dependencies=("libc-id",)) + guest = self.target("guest_tests", dependencies=("helper-id",), + fragment="C:/build/core/libs/prx/libc/libc.dll.a") + libc = self.target("libc", target_type="SHARED_LIBRARY") + helper["id"] = "helper-id" + libc["id"] = "libc-id" + build_dir = self.fixture([guest, helper, libc]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), []) + + def test_excludes_a_direct_libc_link_but_ignores_the_full_libraries_build_target(self): + direct_file = self.target("direct_file_tests", fragment="/build/libs/libc.prx") + all_libraries = self.target("all_libraries_tests", dependencies=("libs-id",)) + libraries = self.target("libs", target_type="UTILITY") + libraries["id"] = "libs-id" + build_dir = self.fixture([direct_file, all_libraries, libraries]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), ["all_libraries_tests"]) + + def test_excludes_product_library_dependencies_but_keeps_test_helpers(self): + product = self.target("product_lib", target_type="STATIC_LIBRARY", source="core/libs/utility/src/Utility.cpp") + product["artifacts"] = [{"path": "core/libs/utility/libproduct_lib.a"}] + product["id"] = "product-id" + production_dependent = self.target("production_dependent", dependencies=("product-id",), + fragment="/build/core/libs/utility/libproduct_lib.a") + helper = self.target("test_helper", target_type="STATIC_LIBRARY", source="core/libs/tests/GuestHelper.cpp") + helper["paths"]["source"] = "core/libs/tests" + helper["id"] = "helper-id" + helper_dependent = self.target("helper_dependent", dependencies=("helper-id",), source="core/libs/tests/HostOnly.cpp") + external = self.target("external", target_type="STATIC_LIBRARY", source="3rdparty/sdl/src/SDL.cpp") + external["paths"]["source"] = "3rdparty/sdl/src" + external["id"] = "external-id" + external_dependent = self.target("external_dependent", dependencies=("external-id",)) + build_dir = self.fixture([product, production_dependent, helper, helper_dependent, external, external_dependent]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual(build_mingw_host_only_tests.host_only_test_targets(source_root, targets), ["external_dependent", "helper_dependent"]) + + def test_ignores_build_order_dependencies_that_are_not_linked(self): + host = self.target( + "guest_sigvec_modules_tests", + dependencies=("libc-id", "kernel-id", "internal-id", "patcher-id"), + ) + libc = self.target("libc", target_type="SHARED_LIBRARY", source="core/libs/libc/src/libc.cpp") + kernel = self.target("libkernel", target_type="SHARED_LIBRARY", source="core/libs/libkernel/src/kernel.cpp") + internal = self.target("libSceLibcInternal", target_type="SHARED_LIBRARY", source="core/libs/internal/src/internal.cpp") + patcher = self.target("nid_patcher", target_type="STATIC_LIBRARY", source="core/libs/nid_patcher/src/patcher.cpp") + libc["id"], kernel["id"], internal["id"], patcher["id"] = "libc-id", "kernel-id", "internal-id", "patcher-id" + build_dir = self.fixture([host, libc, kernel, internal, patcher]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual( + build_mingw_host_only_tests.host_only_test_targets(source_root, targets), + ["guest_sigvec_modules_tests"], + ) + + def test_selects_only_changed_host_test_sources(self): + first = self.target("first_tests") + second = self.target("second_tests", source="core/libs/tests/Second.cpp") + build_dir = self.fixture([first, second]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + selected = build_mingw_host_only_tests.host_only_test_targets( + source_root, targets, ["core/libs/tests/Second.cpp"] + ) + self.assertEqual(selected, ["second_tests"]) + + def test_cmake_or_header_changes_check_all_host_targets(self): + first = self.target("first_tests") + second = self.target("second_tests", source="core/libs/tests/Second.cpp") + build_dir = self.fixture([first, second]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual( + build_mingw_host_only_tests.host_only_test_targets( + source_root, targets, ["core/libs/CMakeLists.txt"] + ), + ["first_tests", "second_tests"], + ) + self.assertEqual( + build_mingw_host_only_tests.host_only_test_targets( + source_root, targets, ["core/libs/tests/HostOnly.hpp"] + ), + ["first_tests", "second_tests"], + ) + + def test_header_listed_as_a_target_source_still_selects_all_host_targets(self): + first = self.target("first_tests") + second = self.target("second_tests", source="core/libs/tests/Second.cpp") + second["sources"].append({"path": "core/libs/tests/Shared.hpp"}) + build_dir = self.fixture([first, second]) + source_root, targets = build_mingw_host_only_tests.read_reply(build_dir) + self.assertEqual( + build_mingw_host_only_tests.host_only_test_targets( + source_root, targets, ["core/libs/tests/Shared.hpp"] + ), + ["first_tests", "second_tests"], + ) + + def test_fails_when_cmake_did_not_write_a_file_api_index(self): + with tempfile.TemporaryDirectory() as directory: + with self.assertRaisesRegex(ValueError, "no CMake File API index"): + build_mingw_host_only_tests.read_reply(Path(directory)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index cbe8248990..9362bf40de 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -108,6 +108,15 @@ class SelectTests(unittest.TestCase): self.assertIn("CMakeLists.txt", output) self.assertFalse((self.build / "select.tests").exists()) + def test_nul_changed_paths_select_from_the_configured_build(self): + changed = self.build / "changed-nul.txt" + changed.write_bytes(b"alpha/alpha.h\0") + self.tool("select", self.build, self.graph, changed) + indices = (self.build / "select.tests").read_text(encoding="utf-8").strip() + shown = subprocess.run(["ctest", "--test-dir", str(self.build), "-N", "-I", indices], + check=True, capture_output=True, text=True).stdout + self.assertEqual(set(re.findall(r"Test +#\d+: (\S+)", shown)), {"alpha", "alpha_data", "script"}) + def test_missing_graph_runs_everything(self): self.assertIsNone(self.select("alpha/alpha.h", graph=self.build / "missing.json")[1]) @@ -423,6 +432,54 @@ class BuildErrorTests(unittest.TestCase): self.assertEqual(data["tests"], ["unit"]) +class GitChangedPathTests(unittest.TestCase): + @unittest.skipUnless(shutil.which("git"), "needs git") + def test_git_nul_preserves_a_leading_bom_in_the_first_filename(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + subprocess.run(["git", "init", "-q", str(root)], check=True, capture_output=True) + name = "\ufeffleading.h" + source = root / name + source.write_text("before\n", encoding="utf-8") + subprocess.run(["git", "add", "."], cwd=root, check=True, capture_output=True) + source.write_text("after\n", encoding="utf-8") + changed = root / "changed" + changed.write_bytes(subprocess.check_output(["git", "diff", "--name-only", "-z", "--"], cwd=root)) + self.assertEqual(select_tests.changed_paths(changed), [name]) + + @unittest.skipUnless(shutil.which("git"), "needs git") + def test_git_nul_paths_select_the_changed_headers(self): + names = ["größe.h", " leading.h", "with space.h"] + if os.name != "nt": + names += ['quote".h', "tab\t.h", "line\n.h", "carriage\r.h", "trailing.h "] + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + subprocess.run(["git", "init", "-q", str(root)], check=True, capture_output=True) + source = root / "src" + source.mkdir() + for name in names: + (source / name).write_text("before\n", encoding="utf-8") + subprocess.run(["git", "add", "."], cwd=root, check=True, capture_output=True) + for name in names: + (source / name).write_text("after\n", encoding="utf-8") + changed = root / "changed" + changed.write_bytes(subprocess.check_output(["git", "diff", "--name-only", "-z", "--"], cwd=root)) + graph = {"closures": {}, "tests": {}} + for index, name in enumerate(names): + target = f"unit_{index}" + graph["closures"][target] = ["src/" + name] + graph["tests"][target] = {"target": target, "registered": ["src/CMakeLists.txt"], + "arguments": [], "requires": [], "sets": [], "depends": []} + graph_path = root / "graph.json" + graph_path.write_text(json.dumps(graph), encoding="utf-8") + result = subprocess.run([sys.executable, str(TOOL), "explain", str(graph_path), str(changed)], + check=True, capture_output=True, text=True) + report = json.loads(result.stdout) + self.assertEqual(report["tests"], sorted(graph["tests"])) + for index, name in enumerate(names): + self.assertEqual(report["reasons"][f"unit_{index}"], ["input: src/" + name]) + + class CaseInsensitivePathTests(unittest.TestCase): def build(self): build = object.__new__(select_tests.Build)